x402

package
v1.801.388 Latest Latest
Warning

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

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

Documentation

Overview

Package x402 is pay-per-request over HTTP 402: quote a price, take the payment, serve the resource.

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

FLOW. A priced resource answers 402 with PaymentRequirements (what to pay + the recipient wallet). The client signs an ERC-3009 transferWithAuthorization over those terms and retries with the X-Payment 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.

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.
	Version = "1"

	// Scheme is the payment scheme: an ERC-3009 transferWithAuthorization the
	// client signs off-chain (EIP-712), settled on the ledger or on-chain.
	Scheme = "erc3009"

	// HeaderRequirements carries the PaymentRequirements on a 402 response.
	HeaderRequirements = plane.HeaderRequirements
	// HeaderProof carries the client's signed Proof on the retry.
	HeaderProof = plane.HeaderProof
	// HeaderReceipt carries the settlement Receipt on a served (2xx) response.
	HeaderReceipt = plane.HeaderReceipt

	// DefaultValidFor is the default authorization validity window, in seconds.
	DefaultValidFor = 300
)
View Source
const (
	DefaultTokenName    = "USD Coin"
	DefaultTokenVersion = "2"
)

TokenName / TokenVersion are the ERC-3009 EIP-712 domain fields. USDC uses ("USD Coin", "2"); operators pin them per token in Config.

View Source
const (
	// DefaultNetwork / DefaultChainID pin the challenge's settlement network when
	// neither the resource's Terms nor the operator config names one. Default to
	// the Hanzo L1 (its EVM chainID == its network id), matching wallets.
	DefaultNetwork = "hanzo"
	DefaultChainID = 36963

	// DefaultTokenDecimals is USDC's precision (the smallest-unit scale the client
	// signs over). Operators override per token via CLOUD_X402_TOKEN_DECIMALS.
	DefaultTokenDecimals = 6
)

Variables

View Source
var (
	// ErrPaymentRequired — the caller has not paid: no proof, an authorization that
	// does not verify, or a replayed nonce. The CHALLENGE is on the response's
	// X-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 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 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 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 X-Payment-Required header and ErrPaymentRequired is returned, so the caller refuses 402 and the client can pay and retry. Paid → settled exactly once, the receipt is on X-Payment-Receipt, nil.

func Shutdown

func Shutdown() error

Shutdown closes the settlement store. Idempotent.

func Verify

func Verify(req PaymentRequirements, p Proof, tokenName, tokenVersion string, now int64) error

Verify checks that p is a well-formed, in-window, unaltered authorization for the requirements req: the To/Value match, the time window holds at now, and the EIP-712 signature recovers to p.From. It does NOT check replay (the store does) nor settle (the settler does) — one concern each.

Types

type Config

type Config struct {
	Token         string // ERC-3009 token contract (EIP-712 verifyingContract)
	TokenName     string // EIP-712 domain name (default "USD Coin")
	TokenVersion  string // EIP-712 domain version (default "2")
	TokenDecimals int    // token smallest-unit scale (default 6)
	Network       string // settlement network label (default "hanzo")
	ChainID       int64  // settlement chain id (default 36963)
	ValidFor      int64  // authorization validity window, seconds (default 300)
}

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

type PaymentRequirements

type PaymentRequirements struct {
	Version  string `json:"version"`
	Scheme   string `json:"scheme"`
	Resource string `json:"resource"` // the priced resource id (canonical path)
	Network  string `json:"network"`
	ChainID  int64  `json:"chainId"`
	Token    string `json:"token"`    // ERC-3009 token contract (e.g. USDC)
	Payee    string `json:"payee"`    // recipient wallet address
	Amount   string `json:"amount"`   // token smallest-unit amount (decimal string)
	ValidFor int64  `json:"validFor"` // seconds the authorization may remain valid
}

PaymentRequirements is the server's 402 challenge: exactly what the client must pay, and to whom, to access the resource. It is emitted both as the response body and the X-Payment-Required header.

type Proof

type Proof struct {
	From        string `json:"from"`        // payer address (recovers from Signature)
	To          string `json:"to"`          // must equal the requirements Payee
	Value       string `json:"value"`       // must equal the requirements Amount
	ValidAfter  int64  `json:"validAfter"`  // unix seconds
	ValidBefore int64  `json:"validBefore"` // unix seconds
	Nonce       string `json:"nonce"`       // 32-byte hex (0x optional)
	Signature   string `json:"signature"`   // 65-byte EIP-712 signature (hex)
}

Proof is the client's signed ERC-3009 authorization — the "payment". Nonce is a client-chosen 32-byte value and is the REPLAY ANCHOR: an authorization settles at most once per (From, Nonce).

func ParseProof

func ParseProof(header string) (*Proof, error)

ParseProof decodes a Proof from the X-Payment header value.

func Sign added in v1.801.350

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

Sign is the CLIENT half of the protocol and the exact mirror of Verify: it produces the ERC-3009 authorization a payer submits on the X-Payment header, bound to exactly the requirements it was challenged with. Empty tokenName / tokenVersion take the USDC defaults, the same substitution Config makes.

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 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"`
	SettledVia string `json:"settledVia"` // "ledger" (live) | "chain" (seam)
	TxHash     string `json:"txHash,omitempty"`
	SettledAt  int64  `json:"settledAt"`
}

Receipt is the settlement record returned on the X-Payment-Receipt header and looked up via GET /v1/x402/settlements/:id.

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 Settlement

type Settlement struct {
	ID         string // deterministic: settlementID(from, nonce)
	PayerOrg   string
	From       string
	Nonce      string
	Resource   string
	Payee      string
	PayeeOrg   string
	Amount     string // exact 18-dp USD (money.Amount.IntString)
	SettledVia string
	TxHash     string
	CreatedAt  int64
}

Settlement is the recorded outcome of one settled x402 authorization.

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
	Token             string       // optional per-resource token override
	Network           string       // optional per-resource network override
	ChainID           int64        // optional per-resource chain 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