x402

package
v1.801.472 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package x402 is pay-per-request over HTTP 402: quote a price, take the payment, serve the resource. It speaks x402 PROTOCOL VERSION 2 (the CAIP-2 / PAYMENT-* header generation), as specified by github.com/x402-foundation/x402/specs/x402-specification-v2.md and its HTTP transport binding in specs/transports-v2/http.md.

The full cycle is challenge → the client pays → payload submitted → verify → serve → settle, native to the Hanzo cloud binary.

FLOW. A priced resource answers 402 with a PaymentRequired carrying one or more PaymentRequirements (what to pay, in what asset, on what network, to whom). The client signs an EIP-3009 transferWithAuthorization over exactly those terms and retries with the PAYMENT-SIGNATURE header. The subsystem VERIFIES the EIP-712 signature (secp256k1 recovery via luxfi/crypto — the SAME primitive the wallets custody signs with), rejects a REPLAYED authorization (nonce dedup), SETTLES exactly once (payer debit through the metering spine so paid usage appears in billing/usage like any metered spend, plus a credit to the recipient wallet's ledger), and serves — answering with a SettlementResponse on PAYMENT-RESPONSE.

WHAT IS ON THE WIRE IS THE SPEC'S, NOT OURS. Every field name, header name and error reason in this file is the one the specification prints, because the wire is a contract with other people's clients: an @x402/fetch or x402[httpx] client that has never heard of Hanzo must be able to pay us, and our private nouns would make that impossible. Internal Go names stay idiomatic only where they do not leak (Terms, Settlement, gate).

SEAMS. The marketplace registry (another subsystem) owns the mapping resource→Terms (price + recipient wallet); x402 only enforces it (Registry + Publish). Recipient resolution rides the wallets subsystem (wallets.ResolvePaymentTarget). On-chain broadcast of the authorization is a Settler seam; the LIVE default is ledger settlement.

Index

Constants

View Source
const (
	// Version is the x402 protocol version this subsystem speaks. It is a NUMBER on
	// the wire (`x402Version: 2`), and it is the only thing that decides how a
	// message is read — there is no configuration switch for the protocol version,
	// because a wire whose shape depends on an operator's flag is two wires.
	Version = 2

	// SchemeExact is the payment scheme: the buyer authorizes EXACTLY the advertised
	// amount. It is the only scheme this rail implements.
	SchemeExact = "exact"

	// TransferEIP3009 is the exact-scheme EVM asset transfer method: the token's own
	// transferWithAuthorization, signed off-chain (EIP-712). It is the spec's default
	// when `extra.assetTransferMethod` is absent, and the only one we verify.
	TransferEIP3009 = "eip3009"

	// HeaderPaymentRequired carries the PaymentRequired on a 402 response.
	HeaderPaymentRequired = plane.HeaderPaymentRequired
	// HeaderPaymentSignature carries the client's PaymentPayload on the retry.
	HeaderPaymentSignature = plane.HeaderPaymentSignature
	// HeaderPaymentResponse carries the SettlementResponse on the answered request.
	HeaderPaymentResponse = plane.HeaderPaymentResponse

	// DefaultMaxTimeoutSeconds is the default `maxTimeoutSeconds` advertised on a
	// challenge — how long the client has to complete the payment.
	DefaultMaxTimeoutSeconds = 300
)
View Source
const (
	// DefaultNetwork pins the challenge's settlement network when neither the
	// resource's Terms nor the operator config names one. It is CAIP-2 and it names
	// the Hanzo L1 (whose EVM chain id IS its network id), matching wallets.
	DefaultNetwork = "eip155:36963"

	// DefaultAssetName / DefaultAssetVersion are the EIP-712 domain fields of the
	// settlement asset. USDC uses ("USD Coin", "2"); operators pin them per asset in
	// Config, and whatever they are, the CHALLENGE states them so the client signs
	// over exactly the domain this rail verifies against.
	DefaultAssetName    = "USD Coin"
	DefaultAssetVersion = "2"

	// DefaultAssetDecimals is USDC's precision (the atomic-unit scale the client
	// signs over). Operators override per asset via CLOUD_X402_ASSET_DECIMALS.
	DefaultAssetDecimals = 6
)

Variables

View Source
var (
	// ErrPaymentRequired — the caller has not paid: no payment, an authorization
	// that does not verify, or a replayed nonce. The CHALLENGE is on the response's
	// PAYMENT-REQUIRED header, so a client can pay and retry.
	ErrPaymentRequired = errors.New("x402: payment required")
	// ErrUnavailable — payment could not be enforced at all: x402 is not mounted in
	// this process, the price table or the recipient wallet did not resolve, the
	// caller has no billable identity, or settlement failed.
	ErrUnavailable = errors.New("x402: payment enforcement unavailable")
)

Sentinel outcomes of Settle — the typed half of the same flow Enforce renders as an HTTP response. BOTH are fail-closed: a caller that gets either must refuse the call, never serve it. A price that cannot be enforced is not a free price.

Functions

func ChainID added in v1.801.414

func ChainID(network string) (int64, error)

ChainID is the EIP-155 chain id a CAIP-2 network identifier names.

The network is ONE value on the wire — "eip155:36963" — and the chain id is read out of it rather than carried beside it. They were two fields once, which is one fact with two spellings and therefore one fact that can disagree with itself: a challenge whose network said one chain and whose chainId said another signs an EIP-712 domain no client can reproduce.

func DecodeHeader added in v1.801.414

func DecodeHeader(header string, v any) error

DecodeHeader reads a base64 JSON header value into v. It accepts unpadded and URL-safe base64 too: those are the spellings a client library reaches for by accident, and every one of them is unambiguous here.

func EncodeHeader added in v1.801.414

func EncodeHeader(v any) string

EncodeHeader renders v as the base64 JSON an x402 v2 header carries. Standard (padded) base64 is what the specification's own examples decode as.

It is exported for the same reason Sign is: a client has to put a PaymentPayload on PAYMENT-SIGNATURE, and a codec it cannot reach would leave the signing half of this package unusable from outside it.

func Enforce

func Enforce() zip.Handler

Enforce is the pay-per-use middleware a priced route group applies, keyed on the request PATH.

Applying it is a DECLARATION that the group is for sale, so anything that leaves x402 unable to answer what the group costs is a refusal, never a passthrough. It refuses when x402 is not mounted in this process and when no price table has been published — the two ways "I cannot enforce payment here" arises — because the alternative renders both as "free", permanently and silently.

That is not hypothetical. The published Registry is a process-global installed by marketplace.Mount, and the shipped topology is one binary per app (manifest/apps.go; Dockerfile builds a plugin per row; cmd/cloud loads each as a child process), so a process that mounts x402 does NOT have marketplace in it and the table is nil. The fleet has been bitten by exactly this once already — see resource_billing_peer.go: "Splitting apps into their own binaries turned every priced create free without changing a line of billing code."

The TOOL path asks a different question — it offers EVERY dispatch to the seam, free ones included, so it must be able to answer "this costs nothing" — but it reaches the same safe answer by asking the process that OWNS the table (peer.go) rather than by reading its own absence as free. A middleware is applied only to what is FOR SALE and can refuse on sight; the tool seam is applied to everything and has to look. Neither ever renders "I cannot tell" as "free".

func InWindow added in v1.801.414

func InWindow(a Authorization, now int64) error

InWindow reports whether an authorization may be ACCEPTED at now — the one check whose answer changes with the clock.

func Mount

func Mount(app cloud.Router, deps cloud.Deps) error

Mount wires /v1/x402 and the settlement store. Direct construction (not cloud.Mount) because it holds the package singleton the middleware reaches.

func Publish

func Publish(r Registry)

Publish installs the marketplace registry x402 enforces against. Passing nil detaches it (Enforce reverts to passthrough). One registry, process-wide.

func Reconcile added in v1.801.414

func Reconcile(ctx context.Context, olderThan time.Duration) (completed int, err error)

Reconcile finishes every settlement that was CLAIMED but never completed — the in-doubt window of any two-process money movement, made recoverable by the claim being written before the money moves.

It is exactly the sweep the previous implementation described and did not have, and claiming first is what made it cheap: it reads THIS store's own unsettled rows rather than needing a cross-process reader of commerce's usage rows to discover which debits had no settlement. Every row carries the payer, the payee and the amount, and both money writes are idempotent on its id — so replaying them either completes the settlement or costs nothing, and no signature is involved, which is why an expired authorization cannot block it.

olderThan skips rows still on a live request's path, so the sweep never races the flow that owns them. Mount runs it once at startup; it is safe to run at any time and any number of times.

func Settle added in v1.801.350

func Settle(ctx context.Context, resource string) error

Settle enforces payment for one resource against the REQUEST bound to ctx — the same flow Enforce runs, for a caller that identifies the priced thing itself rather than by path (the tool plane prices a TOOL, and every tool call arrives on the one /v1/tools/call route).

Free resource → nil, and no request is needed: an unpriced call off the HTTP path (the CLI's LocalInvoke) still runs. A PRICED one always needs the request, because the payer is the attested principal on it and the proof rides its headers.

Unpaid → the challenge is written to the response's PAYMENT-REQUIRED header and ErrPaymentRequired is returned, so the caller refuses 402 and the client can pay and retry. Paid → settled exactly once, the settlement is on PAYMENT-RESPONSE, nil.

func Shutdown

func Shutdown() error

Shutdown closes the settlement store. Idempotent.

func Verify

func Verify(req PaymentRequirements, pay PaymentPayload) error

Verify checks everything about pay that is true or false FOREVER: that it speaks this protocol version, that the terms it echoes are the terms we offered, and that the signature recovers to the payer it claims.

It deliberately does NOT check the time window. That is InWindow, and it is separate because the two answers have different lifetimes: a bad signature is bad for all time, while an expired authorization was VALID when we accepted it. The flow needs to tell those apart to finish a settlement it already started (see settle), and a single Verify that folded them together made "we took your money five minutes ago" indistinguishable from "this was never a payment".

Types

type Authorization added in v1.801.414

type Authorization struct {
	From        string `json:"from"`
	To          string `json:"to"`
	Value       string `json:"value"`
	ValidAfter  epoch  `json:"validAfter"`
	ValidBefore epoch  `json:"validBefore"`
	Nonce       string `json:"nonce"`
}

Authorization is the EIP-3009 TransferWithAuthorization. Nonce is a client-chosen 32-byte value and is the REPLAY ANCHOR — on-chain the token contract itself refuses a second transfer for one (from, nonce), and this rail enforces the same pair off-chain so a ledger settlement inherits the identical guarantee.

type Config

type Config struct {
	Asset         string // EIP-3009 token contract (EIP-712 verifyingContract)
	AssetName     string // EIP-712 domain name (default "USD Coin")
	AssetVersion  string // EIP-712 domain version (default "2")
	AssetDecimals int    // asset atomic-unit scale (default 6)
	Network       string // CAIP-2 settlement network (default "eip155:36963")
	MaxTimeout    int64  // advertised maxTimeoutSeconds (default 300)
}

Config is the operator-pinned x402 settlement config. Asset + its EIP-712 domain (name/version/decimals) MUST match the asset the client signs against, or every signature fails to recover. Values are read from env in Mount; tests inject Config directly.

type ExactPayload added in v1.801.414

type ExactPayload struct {
	Signature     string        `json:"signature"`
	Authorization Authorization `json:"authorization"`
}

ExactPayload is the exact-scheme EVM `payload`: the signature and the parameters needed to reconstruct the message it signed.

type Extensions added in v1.801.414

type Extensions map[string]json.RawMessage

Extensions is the protocol's extension map, carried through VERBATIM. We advertise none, so this exists to preserve what a client sends rather than to interpret it: dropping an unknown extension silently is how a client that thinks it negotiated something discovers otherwise only in production.

type Extra added in v1.801.414

type Extra struct {
	AssetTransferMethod string `json:"assetTransferMethod,omitempty"`
	Name                string `json:"name"`
	Version             string `json:"version"`
}

Extra is the exact-scheme EVM `extra`: the token's EIP-712 domain, which is what makes a signature verifiable at all, plus the transfer method it was signed for.

The domain lives on the CHALLENGE rather than in this process's config because the client signs over what it was OFFERED. A server that verified against its own config instead would accept a signature the client never made over these terms.

type Invalid added in v1.801.414

type Invalid struct {
	Reason string
	Detail string
}

Invalid is a verification failure carrying the specification's own error reason, which is what the wire reports and what a client matches on.

func (*Invalid) Error added in v1.801.414

func (e *Invalid) Error() string

type PaymentPayload added in v1.801.414

type PaymentPayload struct {
	X402Version int                 `json:"x402Version"`
	Resource    *ResourceInfo       `json:"resource,omitempty"`
	Accepted    PaymentRequirements `json:"accepted"`
	Payload     ExactPayload        `json:"payload"`
	Extensions  Extensions          `json:"extensions,omitempty"`
}

PaymentPayload is the client's payment, carried BASE64-ENCODED on the PAYMENT-SIGNATURE header. Accepted is the client ECHOING which of the offered requirements it chose; it is checked against ours and never trusted as the terms.

func ParsePayment added in v1.801.414

func ParsePayment(header string) (*PaymentPayload, error)

ParsePayment decodes a PaymentPayload from a PAYMENT-SIGNATURE header value.

func Sign added in v1.801.350

func Sign(req PaymentRequirements, key *ecdsa.PrivateKey, nonce string, validAfter, validBefore int64) (PaymentPayload, error)

Sign is the CLIENT half of the protocol and the exact mirror of Verify: it produces the PaymentPayload a payer submits on PAYMENT-SIGNATURE, bound to exactly the requirements it was challenged with.

It lives here, beside Verify, because the EIP-712 encoding is ONE encoding: a signer that wrote it out a second time would be free to drift from the verifier and would fail only in production, where a real payer's signature stops recovering. One encoding, two directions.

type PaymentRequired added in v1.801.414

type PaymentRequired struct {
	X402Version int                   `json:"x402Version"`
	Error       string                `json:"error,omitempty"`
	Resource    ResourceInfo          `json:"resource"`
	Accepts     []PaymentRequirements `json:"accepts"`
	Extensions  Extensions            `json:"extensions,omitempty"`
}

PaymentRequired is the 402 challenge: what the resource is, and every way it may be paid for. It is carried BASE64-ENCODED on the PAYMENT-REQUIRED header, which the HTTP transport names as its canonical location.

type PaymentRequirements

type PaymentRequirements struct {
	Scheme            string `json:"scheme"`
	Network           string `json:"network"` // CAIP-2, e.g. "eip155:36963"
	Amount            string `json:"amount"`  // atomic units of Asset, decimal string
	Asset             string `json:"asset"`   // EIP-3009 token contract
	PayTo             string `json:"payTo"`   // recipient wallet address
	MaxTimeoutSeconds int64  `json:"maxTimeoutSeconds"`
	Extra             *Extra `json:"extra,omitempty"`
}

PaymentRequirements is ONE acceptable way to pay: the scheme, the network, and the exact amount of the exact asset that must reach exactly this payee.

type Receipt

type Receipt struct {
	ID         string `json:"id"`
	Resource   string `json:"resource"`
	Payer      string `json:"payer"` // payer ORG (the debited ledger)
	From       string `json:"from"`  // payer address
	Payee      string `json:"payee"` // recipient address
	PayeeOrg   string `json:"payeeOrg"`
	Amount     string `json:"amount"` // exact 18-dp USD (money.Amount string)
	Nonce      string `json:"nonce"`
	Network    string `json:"network"`
	SettledVia string `json:"settledVia"` // "ledger" (live) | "chain" (seam)
	TxHash     string `json:"txHash,omitempty"`
	SettledAt  int64  `json:"settledAt"`
}

Receipt is the settlement record this subsystem's OWN api answers with at GET /v1/x402/settlements/:id. It is deliberately not the wire type: the wire carries the spec's SettlementResponse, which has no room for the payer org or the resource, and those are exactly what a tenant reading its own settlements needs.

type Registry

type Registry interface {
	Price(ctx context.Context, resource string) (terms Terms, ok bool, err error)
}

Registry resolves a resource's payment Terms. ok=false means the resource is FREE — no enforcement. err is a real lookup failure (fail closed). The marketplace subsystem implements this and injects it via Publish.

type ResourceInfo added in v1.801.414

type ResourceInfo struct {
	URL         string `json:"url"`
	Description string `json:"description,omitempty"`
	MimeType    string `json:"mimeType,omitempty"`
}

ResourceInfo describes the protected resource. URL is the resource's identity — for a priced ROUTE the request path, for a priced TOOL its `tool:` id — which is also the key the price table and the settlement row are written under.

type Settlement

type Settlement struct {
	ID       string // deterministic: settlementID(from, nonce)
	PayerOrg string
	From     string
	Nonce    string
	Resource string
	Payee    string
	PayeeOrg string
	// PayeeSubject is the recipient WALLET's ledger subject — where the credit
	// actually lands. It is on the row, and not re-resolved from wallets when a
	// settlement is completed later, because a claim has to be sufficient on its
	// own: re-resolving would make the sweep depend on the wallets subsystem being
	// reachable, and on the wallet still resolving to the same subject it did when
	// the payer was charged. Neither is guaranteed, and both would silently pay the
	// wrong account.
	PayeeSubject string
	Amount       string // exact 18-dp USD (money.Amount.IntString)
	Network      string // CAIP-2
	SettledVia   string
	TxHash       string
	Settled      bool
	CreatedAt    int64
}

Settlement is one x402 authorization this rail has accepted: claimed when the row is written, Settled once both halves of the money have moved.

type SettlementResponse added in v1.801.414

type SettlementResponse struct {
	Success     bool       `json:"success"`
	ErrorReason string     `json:"errorReason,omitempty"`
	Payer       string     `json:"payer,omitempty"`
	Transaction string     `json:"transaction"`
	Network     string     `json:"network"`
	Amount      string     `json:"amount,omitempty"`
	Extensions  Extensions `json:"extensions,omitempty"`
}

SettlementResponse is what a request answers with once payment has been settled — or failed to — carried BASE64-ENCODED on the PAYMENT-RESPONSE header. The spec requires it on BOTH the success and the failure leg.

Transaction is the identifier of the transaction that settled this payment on whichever rail settled it: the chain's tx hash when the authorization is broadcast, and the deterministic settlement id when it is settled on the ledger (the live default). Either way it is the handle that finds this settlement again, which is the one thing the field is for; an empty string means nothing settled.

type Terms

type Terms struct {
	Amount            money.Amount // exact 18-dp USD price
	RecipientOrg      string       // the recipient wallet's org
	RecipientWalletID string       // the wallet that receives payment
	Asset             string       // optional per-resource asset override
	Network           string       // optional per-resource CAIP-2 network override
}

Terms are a priced resource's payment terms: the price and the wallet that receives it. The marketplace registry OWNS the resource→Terms mapping; x402 resolves the recipient wallet (via wallets) and enforces payment against these.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL