Skip to content

tosh-labs/blockchain0x-x402-go

Repository files navigation

blockchain0x-x402-go

Go reference License: Apache-2.0 Go ≥ 1.22

**Official Go port of @blockchain0x/x402

  • blockchain0x-x402 (Python).** Ships the wire primitives + a 402-aware HTTP client wrapper. Sibling module to github.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.0 ships the wire primitives + the 402-aware X402Client. Per-framework server adapters (net/http middleware, Gin, Echo, Fiber) land in a follow-up row.

Install

go get github.com/tosh-labs/blockchain0x-x402-go@v0.0.1-alpha.0

For 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.

Verify an inbound x402 payment (recipient)

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 with header.unknown_scheme.
  • Validates txHash, payerAddress, amountUsdc, and network shape; any drift rejects with header.payload_malformed.
  • Lowercases hex fields so downstream comparisons against on-chain transaction logs are deterministic.

Issue x402-aware HTTP calls (payer)

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:

  1. Parses the 402 body and picks the requirement matching the SDK's network.
  2. Calls sdk.PaymentsCreate(...) to settle on-chain. The main SDK auto-attaches an Idempotency-Key so a flaky retry does not double-spend.
  3. Polls sdk.TransactionsGet(paymentID) every 1s for up to 30s until the transaction confirms.
  4. Rebuilds the request with the X-Payment header 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 no accepts entry for the SDK's network mode.
  • settlement_timeout - the on-chain payment did not confirm within ConfirmTimeout.
  • chain_failed - the payment row flipped to failed status before confirming.

Expose a paid HTTP route (recipient, server-side)

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.
}

Wire-format cross-compatibility

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.

Failure-mode codes

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).

License

Apache-2.0.

About

Official Go port of @blockchain0x/x402 - HTTP 402 wire primitives + pay-side client + receive-side server adapters. USDC on Base.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages