HTTP 402 challenge
An unpaid gateway resource returns status 402. One response may contain both
of these carriers:
HTTP/1.1 402 Payment Required
WWW-Authenticate: Payment method="sui.charge", challenge="...", amount="...", ...
PAYMENT-REQUIRED: <base64 of the JSON body below>
Content-Type: application/json
{
"x402Version": 2,
"error": "payment_required",
"resource": {
"url": "https://gateway.example/api/quote",
"description": "...",
"mimeType": "application/json"
},
"accepts": [
{
"scheme": "exact",
"network": "sui:testnet",
"amount": "...",
"asset": "0x2::sui::SUI",
"payTo": "0x...",
"maxTimeoutSeconds": 60,
"extra": {
"paymentFlow": "upfront",
"suipay": { "challengeId": "...", "packageId": "0x...", "digest": "...", "...": "..." }
}
}
]
}The example is structural. Deployments supply real addresses, amounts, package IDs, revisions, and signed extra fields. Do not fabricate omitted values.
x402 v2 carries the challenge in the PAYMENT-REQUIRED header (base64 JSON);
the JSON body is the same object, kept for readability. extra.paymentFlow is
always upfront: Sui Agent Payments settles before it serves. extra.suipay carries the
signed offer terms a Sui Agent Payments-aware payer needs; a stock payer echoes the whole
accept back as accepted and pays with a native Coin<T> transfer
(SplitCoins + TransferObjects) or with payment::settle.
The facilitator routes POST /v1/x402/verify and POST /v1/x402/settle are
not public. Each requires Authorization: Bearer <SUIPAY_SECRET>, the gateway's
challenge secret, and answers 401 without it. They use the same facilitator
the gateway settles direct-rail offers with. GET /v1/x402/supported needs no
auth. MCP and A2A carriage is in @suipay/seller-runtime.
Failure response
A payment that is refused answers 402 again with a fresh PAYMENT-REQUIRED
and a PAYMENT-RESPONSE in the x402 v2 failure form, so a client can tell
"pay" from "your payment failed":
{ "success": false, "errorReason": "insufficient_funds", "transaction": "", "network": "sui:testnet", "payer": "0x..." }A payment header that does not decode answers 400 with errorReason: "invalid_payload" and no PAYMENT-REQUIRED. A broadcast whose outcome is
unknown answers 503 with errorReason: "settlement_pending" and the broadcast
digest in transaction; reconcile that digest before paying again. The Sui Agent Payments
code for every case is in X-Suipay-Error.
Both carriers encode one internal offer (same challenge ID, amount, asset, recipient, network, request binding). A buyer uses one advertised dialect.
Canonical offer fields
| Field | Meaning |
|---|---|
challengeId | Single-use ID shared by both dialects |
resource | Absolute paid resource URL |
amount | Gross amount as an atomic-unit decimal string |
netAmount, platformFee | Explicit fee split when present |
asset, decimals, network | Fully qualified coin type, display decimals, Sui network |
payTo, packageId | Recipient and settlement package |
issuedAt, expiresAt | Epoch-millisecond validity window |
settlementRail | On-chain engine: spend account for agents (delegate on the wire) |
serviceId, serviceRevision | Immutable service snapshot |
method, path | Bound HTTP request target |
bodyHash, contentType | Binding for a body-bearing request |
digest | HMAC over the versioned canonical offer |
Additional signed fields exist for subscription and refundable offers. Decode the advertised format and validate all present terms rather than dropping unfamiliar signed fields.
Settlement rails on the offer
| Value | Notes |
|---|---|
delegate | Spend-account escrow (agent spends from a funded account under a policy); MPP proof by default |
direct | Non-escrow baseline |
recurring | Recurring mandate (intent: subscription; Move module subscription) |
Credentials and receipts
| Dialect | Retry credential | Receipt |
|---|---|---|
| MPP | Authorization: Payment … | Payment-Receipt |
| x402 | PAYMENT-SIGNATURE: <base64 payload> | PAYMENT-RESPONSE |
Never send both credentials. Never forward either across an origin-changing redirect. The retry must preserve URL, method, headers relevant to the resource, and body bytes. A POST offer requires the body binding.
Spend-account MPP proof
MPP pay-then-prove credentials for the spend-account rail carry the finalized transaction digest, payer, challenge, and complete offer. They do not carry spendable transaction bytes. Current spend-account resources reject transaction credentials and require an MPP proof.
Binding hashes used on chain:
paymentIdHash = sha256(challengeId)
termsHash = sha256(offer.digest)Agent payment sketch
- Unpaid request → 402.
- Validate offer.
- Build and sign
spend_account::settle_policyas the registered delegate (or use MCPpay/ SDK payer). - Execute; wait for finality.
- Retry with MPP proof → resource + receipt.
See Pay with REST and Pay with the SDK.