x402
Package x402 is pay-per-request over HTTP 402: quote a price, take the payment, serve the resource.
Package x402 is pay-per-request over HTTP 402: quote a price, take the payment, serve the resource.
| Base URL | https://api.hanzo.ai |
| Operations | 1 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1163 · x402 — Pay Per Request — Draft · read the specification →
x402 is pay-per-request over HTTP 402: quote a price, take the payment, serve
the resource. It speaks x402 protocol version 2 — challenge, EIP-3009
transferWithAuthorization signed over the advertised terms, verify, settle
exactly once, serve — native to the cloud binary. It is a rail and nothing
else: the marketplace declares what is priced and who is paid, wallets says
where the money lands, and this capability enforces. The implementation is
hanzoai/cloud apps/x402.
Motivation
A marketplace where agents buy tools and data per call needs a payment that works without an account relationship: the buyer may be a client that has never heard of the seller, and the price may be a fraction of a cent. HTTP 402 with a signed transfer authorization is the shape that fits — the challenge carries the terms, the retry carries the proof, and no session, invoice or card vault sits between them. What it demands in exchange is exactness: replay, double-settlement and misdirected credit are all one-request attacks if the rail is loose anywhere.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 The wire is the spec's, not ours
The protocol is x402 version 2 as specified by x402-foundation/x402
(specs/x402-specification-v2.md and its HTTP transport binding). Every field
name, header name and error reason on the wire MUST be the one the
specification prints (apps/x402/protocol.go): the wire is a contract with
other people's clients, and a client that has never heard of Hanzo must be
able to pay us. The version is a number on the wire (x402Version: 2) and the
only thing that decides how a message is read — a wire whose shape depends on
an operator flag is two wires. One scheme (exact), one transfer method
(EIP-3009); signature verification is secp256k1 recovery over the EIP-712
domain the challenge itself stated. The default settlement network is
eip155:36963, the Hanzo L1, matching wallets.
§2 The store, and settle-once
One SQLite store, x402 (apps/x402/store.go:55): one row per claimed
authorization, keyed by a deterministic id derived from (payer address,
nonce). That id is both the replay-dedup key — a primary key, so a second
insert of the same authorization is atomically refused — and the idempotency
ref on both money writes, so the fast in-pod dedup and the ledger's own
idempotency agree on one key. A row is written at claim and flipped at settle;
an interrupted settlement is recoverable because the claim names payer, payee,
amount and the payee's ledger subject, and startup Reconcile completes
anything the last process left in doubt (apps/x402/x402.go:181). The payee
subject is on the row and MUST NOT be re-resolved at completion time — a claim
has to be sufficient on its own, or the sweep silently pays whatever the
wallet resolves to later.
§3 The address, and the three callers
The addressed surface is one typed operation, GET /v1/x402/settlements/{id} (manifest/apps.go:174) — the receipt lookup,
scoped by principal.Ledger, which folds in the SuperAdmin masquerade so an
admin inspecting another org still reads their own settlements
(apps/x402/x402.go:791). The flow itself is written once (run) and reached
three ways: Enforce, a handler a priced route group applies; Settle, the
same flow for a resource named in a request body; and the plane op
/x402/settle for the fleet's one-binary-per-app shape, where the payer is
the caller's org resolved at the edge and never a field
(apps/x402/rpc.go). On the plane the outcome is data, not a transport error
— a 402 carries the terms the client must read to pay — so a transport error
means the rail did not answer, and callers fail closed on it, never free.
§4 Money
The surface declares cloud.Free (plugin/x402/main.go): the rail itself
charges nothing per request. The money it moves is the settlement — a payer
debit through the metering spine under provider label x402 with the
settlement id as Ref (apps/x402/x402.go:732), which is how paid usage
appears in billing/usage like any metered spend, and a payee credit deposited
into the ledger subject of the wallet the publisher's listing named, resolved
through wallets (HIP-1161). What a resource costs is the marketplace
registry's table; when the price cannot be learned the answer MUST be an
error, never "free" — a rail that reads its own outage as "nothing is priced"
sells the whole catalogue for nothing (apps/x402/peer.go). Unknown payee is
503; unreachable ledger is no settlement, no receipt, no dispatch.
§5 Events and telemetry
It publishes nothing on the bus; a customer's webhooks receive no x402.*
events. Beyond the request span, each settlement appends an audit record
(resource type x402.settlement, redacted; apps/x402/x402.go:899), and the
startup reconcile logs what it completed.
§6 Upstream
It implements the x402 specification version 2 (x402-foundation/x402) — a
wire specification, not a code fork; no upstream implementation is embedded.
Signature recovery rides github.com/luxfi/crypto v1.20.5 (Lux Ecosystem
License 1.2), the same primitive wallets custody signs with. Persistence is
github.com/hanzoai/sqlite (MIT / Apache-2.0 dual).
§7 Stage
beta: the manifest row declares it (manifest/apps.go:174, Stage: Beta;
HIP-0139 §8), reached by flag until promoted.
Rationale
The alternative to a rail-plus-registry seam is a payments module inside each priced subsystem. That is N copies of verify-and-settle, N nonce stores that do not share a dedup key, and a marketplace that cannot price a resource without shipping code into its owner. One rail, inert until a registry is published, keeps enforcement in one place and pricing where the catalogue is.
Security Considerations
The attacks are the classic payment ones, and each is closed structurally.
Replay: the (payer, nonce) primary key refuses a second claim atomically, and
the same id is the ledger's idempotency key, so even a bug in the fast path
cannot double-move money. Redirection: the payee org comes off the listing
row and the payer off the edge-resolved principal — neither is a request
field, so a buyer can neither choose who is paid nor who pays. Free-fall: the
priced set must fail closed (§4); the one exception this rail ever held —
reading an unreachable marketplace as "no marketplace, nothing priced" — is
recorded in apps/x402/peer.go as the defect that sold everything for
nothing, and is why the price has no exception now.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | x402 at its own prefix | 1 operation |
| CLI | hanzo x402 … | 1 of 1 |
| SDK | X402Api in every published client | 1 method |
| MCP | tool x402 on https://api.hanzo.ai/v1/mcp | 1 operation, 0 under the document's own id — ask describe for the rest |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call. This one takes arguments — the placeholders are yours to fill. GET /v1/x402/settlements/{id}, operation get_x402_settlements_by_id:
hanzo x402 settlements get <id>import { Configuration, X402Api } from 'hanzoai';
const api = new X402Api(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getX402SettlementsById({ id: 'id' });from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import X402Api
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = X402Api(client).get_x402_settlements_by_id(id='id')cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.X402API.GetX402SettlementsById(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, x402_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = x402_api::get_x402_settlements_by_id(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.X402Api;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new X402Api(client).getX402SettlementsById();curl https://api.hanzo.ai/v1/x402/settlements/<id> \
-H "Authorization: Bearer $HANZO_API_KEY"The door reaches x402 through the x402 tool, which names its 1 operations with its own verbs — this one among them, under a name only the door declares. describe explains any of them:
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "describe",
"arguments": {
"op": "get_x402_settlement"
}
}
}'Answers 200 with object — ok.
Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/x402/settlements/{id} | Settlement reads one x402 payment receipt by id. |
How is this guide?