**Official Go port of @blockchain0x/x402
blockchain0x-x402(Python).** Ships the wire primitives + a 402-aware HTTP client wrapper. Sibling module togithub.com/tosh-labs/blockchain0x-go; import only when your service either issues x402-aware HTTP calls (a payer) or verifies inbound x402 payments (a recipient).
Pre-release:
v0.0.1-alpha.0ships the wire primitives + the 402-awareX402Client. Per-framework server adapters (net/http middleware, Gin, Echo, Fiber) land in a follow-up row.
go get github.com/tosh-labs/blockchain0x-x402-go@v0.0.1-alpha.0For the payer path you also need the main SDK
(github.com/tosh-labs/blockchain0x-go) - the X402Client takes
that SDK via the duck-typed Sdk interface so the modules stay
decoupled at the build-graph level.
import (
"net/http"
x402 "github.com/tosh-labs/blockchain0x-x402-go"
)
func handler(w http.ResponseWriter, r *http.Request) {
payment, err := x402.ParsePaymentHeader(r.Header.Get("X-Payment"))
if err != nil {
we, _ := x402.IsWireError(err)
http.Error(w, we.Code, http.StatusBadRequest)
return
}
// payment.PaymentRequestID, payment.TxHash, payment.Network ...
}The verifier:
- Accepts only
exact-usdc:<base64>scheme; anything else rejects withheader.unknown_scheme. - Validates
txHash,payerAddress,amountUsdc, andnetworkshape; any drift rejects withheader.payload_malformed. - Lowercases hex fields so downstream comparisons against on-chain transaction logs are deterministic.
import (
"context"
"net/http"
x402 "github.com/tosh-labs/blockchain0x-x402-go"
)
// sdkAdapter wraps the main blockchain0x-go client to fit
// x402.Sdk's duck-typed interface. The wrapper isolates the
// build dependency so a recipient-only service does not have to
// pull in the full main SDK.
type sdkAdapter struct{ sdk *blockchain0x.Client }
func (s *sdkAdapter) Network() string { return string(s.sdk.Network()) }
func (s *sdkAdapter) PaymentsCreate(ctx context.Context, agentID, to, amountWei string) (string, error) {
p, err := s.sdk.Payments.Create(ctx, blockchain0x.PaymentInput{
AgentID: agentID, To: to, AmountWei: amountWei,
})
if err != nil {
return "", err
}
return p.ID, nil
}
func (s *sdkAdapter) TransactionsGet(ctx context.Context, id string) (x402.Payment, error) {
t, err := s.sdk.Transactions.Get(ctx, id)
if err != nil {
return x402.Payment{}, err
}
return x402.Payment{ID: t.ID, Status: t.Status, TxHash: t.TxHash, FromAddress: t.FromAddress}, nil
}
client := x402.NewClient(&sdkAdapter{sdk: main}, x402.ClientOptions{AgentID: "agt_..."})
req, _ := http.NewRequest(http.MethodPost, "https://service-b.com/llm-query", nil)
resp, err := client.Do(req)The wrapper handles a 402 response transparently:
- Parses the 402 body and picks the requirement matching the SDK's network.
- Calls
sdk.PaymentsCreate(...)to settle on-chain. The main SDK auto-attaches anIdempotency-Keyso a flaky retry does not double-spend. - Polls
sdk.TransactionsGet(paymentID)every 1s for up to 30s until the transaction confirms. - Rebuilds the request with the
X-Paymentheader and re-issues it once. The 200 response is returned to the caller.
Failures surface as *x402.ClientError with stable codes:
no_matching_requirement- the 402 had noacceptsentry for the SDK's network mode.settlement_timeout- the on-chain payment did not confirm withinConfirmTimeout.chain_failed- the payment row flipped tofailedstatus before confirming.
The Middleware helper drops into any stdlib net/http chain (or
chi / gorilla / gin's stdlib bridge) and gates routes against a
static pricing table.
import (
"net/http"
x402 "github.com/tosh-labs/blockchain0x-x402-go"
)
pricing := x402.PricingTable{
"POST /llm-query": {
AmountUSDC: "0.10",
PayToAddress: "0xabc...",
PaymentRequestID: "pr_demo",
},
}
guarded := x402.Middleware(x402.ServerOptions{
Sdk: serverSdk, // implements x402.ServerSdkLike
Pricing: pricing,
})(myLLMHandler)
http.Handle("/llm-query", guarded)
http.ListenAndServe(":8080", nil)A miss in the pricing table is a no-op (the route is free). A hit
with no valid X-Payment short-circuits the response with HTTP 402
and the canonical accepts[] body. A hit with a valid payment
calls Sdk.PaymentRequestsSettle() to anchor trust, attaches the
parsed payment to the request context, and forwards to the inner
handler.
Read the verified payment downstream:
func myLLMHandler(w http.ResponseWriter, r *http.Request) {
payment, ok := x402.PaymentFromContext(r.Context())
if !ok {
// route was free OR the middleware was not mounted
}
// payment.PaymentRequestID, payment.TxHash, etc.
}BuildPaymentHeader produces the same base64 string as the Node
- Python implementations for the same input. A Go payer can pay a Node + Python server; a Node + Python payer can pay a Go server. The canonical JSON shape is:
{
"scheme": "exact-usdc",
"version": 1,
"paymentRequestId": "...",
"txHash": "...",
"payerAddress": "...",
"amountUsdc": "...",
"network": "..."
}Keys are emitted in this exact order (the Go code pins it
explicitly via a manual strings.Builder rather than relying on
struct-field order). Hex fields (txHash, payerAddress) are
lowercased before encoding.
| Code | When |
|---|---|
response.not_402 |
A non-402 response was passed to Parse402Response. |
response.body_missing |
402 response body was empty. |
response.body_malformed |
402 body failed JSON parse or shape validation. |
header.missing |
X-Payment header was absent. |
header.malformed |
X-Payment header was not <scheme>:<base64-payload>. |
header.unknown_scheme |
The scheme prefix was not exact-usdc. |
header.payload_malformed |
The decoded payload failed JSON parse or shape validation. |
Branch on (*x402.WireError).Code (or use x402.IsWireError(err)
for the typed-extract pattern).
Apache-2.0.