--- description: s402 Wire Format Specification v1 — the formal, field-by-field definition of the s402 HTTP 402 payment protocol. For implementors building in Go, Python, Rust, or any language. --- # s402 Wire Format Specification **Version**: 1  ·  **Status**: Draft  ·  **Date**: March 2026 This document defines the s402 wire format — the exact encoding, field definitions, validation rules, and error semantics for the s402 HTTP 402 payment protocol. It is the authoritative reference for any implementation in any language. The TypeScript reference implementation lives at [github.com/s402-protocol/core](https://github.com/s402-protocol/core). Machine-readable conformance test vectors ship in the npm package (196 vectors across 14 files). ## 1. Terminology The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). | Term | Definition | |------|-----------| | **Resource Server** | The HTTP server that hosts paid resources and returns 402 responses. | | **Client** | The HTTP client (typically an AI agent) that requests resources and submits payments. | | **Facilitator** | A service that verifies and settles payment transactions on-chain on behalf of the Resource Server. Optional — direct settlement bypasses the Facilitator. | | **Requirements** | The JSON object sent by the server in a 402 response describing what payment is needed. | | **Payload** | The JSON object sent by the client containing a signed payment transaction. | | **Scheme** | A payment pattern (exact, upto, prepaid, stream, escrow, unlock) defining the on-chain lifecycle. | | **Base units** | The smallest denomination of a currency (e.g., MIST for SUI, wei for ETH, lamports for SOL). | ## 2. Protocol Overview s402 uses the HTTP 402 status code to negotiate payment between a client and a server. The protocol has three phases: ``` Phase 1: Discovery Client ─── GET /resource ──────────────> Server Client <── 402 + payment-required ────── Server Phase 2: Payment Client ─── GET /resource + x-payment ──> Server Server ─── payload + requirements ──────> Facilitator Server <── settlement result ─────────── Facilitator Phase 3: Delivery Client <── 200 + payment-response ────── Server ``` The 402 leg is an x402 V2 `PaymentRequired` envelope. What distinguishes an **s402-profile** 402 from a plain x402 one is the presence of `extensions.s402` — and a 402 without it is still payable by an s402 client. ## 3. Transport ### 3.1 Header Transport (default) s402 uses three HTTP headers. All header names are lowercase per [RFC 9113 §8.2.1](https://www.rfc-editor.org/rfc/rfc9113#section-8.2.1). | Header | Direction | Content | |--------|-----------|---------| | `payment-required` | Server → Client | Base64-encoded x402 V2 `PaymentRequired` envelope | | `x-payment` | Client → Server | Base64-encoded Payment Payload JSON | | `payment-response` | Server → Client | Base64-encoded Settlement Response JSON | These header names are identical to x402 V1 for wire compatibility. The 402 *document* is x402 V2's, so an unmodified x402 V2 client reads it as well. ### 3.2 Encoding Header transport uses **Unicode-safe base64** encoding: 1. JSON-serialize the object: `JSON.stringify(object)` 2. UTF-8 encode the string to bytes 3. Base64 encode the bytes using standard base64 (RFC 4648 §4) For ASCII-only content (the common case), this produces identical output to applying base64 directly to the JSON string. The UTF-8 step ensures that Unicode characters in the `extensions` field or error messages survive the round-trip. Implementations MUST decode in the reverse order: base64 decode → UTF-8 decode → JSON parse. ### 3.3 Header Size Limit Implementations SHOULD enforce a maximum header size of **65,536 bytes** (64 KiB) on decoded headers. Headers exceeding this limit SHOULD be rejected before base64 decoding. This is a defense-in-depth measure. Most HTTP servers enforce smaller limits (Node.js: 16 KiB, Cloudflare Workers: 128 KiB). A wire format library should not rely on runtime enforcement alone. ### 3.4 Body Transport (large payloads) When a payment payload exceeds header size limits (e.g., complex DeFi PTBs exceeding 128 KiB after base64), implementations MAY use body transport instead. - **Content-Type**: `application/s402+json` - **Encoding**: Raw JSON (no base64) - **Size limits**: Set by the application (Express default: 100 KiB, Nginx default: 1 MiB) Implementations MUST apply the same validation and key-stripping rules to body-transported objects as to header-transported objects. To detect which transport a request uses, check: 1. If `Content-Type` includes `application/s402+json` → body transport 2. If the `x-payment` header is present → header transport 3. Otherwise → unknown ### 3.5 Protocol Detection To determine whether a 402 response is an s402-profile 402 or a plain x402 one: 1. Read the `payment-required` header 2. Base64 decode and JSON parse 3. If the decoded object contains `extensions.s402` → **s402** 4. Otherwise, if it contains `x402Version` → **x402** 5. Otherwise → **unknown** This names the presence of s402's extensions, never "not for us": both are x402 V2 envelopes, and an s402 client pays either. ## 4. Payment Requirements The 402 document is an **x402 V2 `PaymentRequired` envelope**, sent by the Resource Server in the `payment-required` header (and body) of a 402 response. ```json { "x402Version": 2, "error": "Payment Required", "resource": { "url": "https://api.example.com/data", "mimeType": "application/json" }, "accepts": [ { "scheme": "exact", "network": "sui:mainnet", "asset": "0x2::sui::SUI", "amount": "1000000", "payTo": "0x00…01", "maxTimeoutSeconds": 60, "extra": {} } ], "extensions": { "s402": { "version": "2" } } } ``` Three levels, and which level a field sits at is the whole design: - **the envelope** — x402's, byte-compatible with its decoder - **each `accepts[]` entry** — one offered scheme, x402's `PaymentRequirements` - **`extra` and `extensions.s402`** — everything s402 adds, in the slots x402 leaves for it ### 4.1 Envelope Fields | Field | Type | Required | Constraints | Description | |-------|------|----------|-------------|-------------| | `x402Version` | number | Yes | MUST be `2` | Envelope version. | | `resource` | object | Yes | MUST carry a string `url`. Fields per §4.1.2. | What is being paid for. | | `accepts` | object[] | Yes | Non-empty. Each entry per §4.2. | One entry per offered scheme. `exact` MUST be listed first whenever it is offered — an x402 client pays the first entry it has a handler for. | | `error` | string | No | — | Human-readable reason, surfaced by x402 clients. | | `extensions` | object | No | Opaque bag | Envelope-level extensions. s402's own live under the `s402` key; see §4.1.1. | #### 4.1.1 `extensions.s402` | Field | Type | Required | Constraints | Description | |-------|------|----------|-------------|-------------| | `version` | string | No | If present, MUST be `"2"` | s402 wire version. Its presence marks the 402 as an s402-profile 402. | | `mandate` | object | No | See §4.5. | AP2 mandate requirements. Envelope-level: a mandate authorizes the AGENT, not one price line, so it cannot differ per entry. | A 402 carrying no `extensions.s402` is a plain x402 402. Implementations MUST decode it and MUST NOT treat its absence as an error. #### 4.1.2 `resource` fields x402 V2's `ResourceInfo`. The bounds are upstream's, not s402's, and an emitted 402 that breaks one is a document the pinned `@x402/core` decoder refuses to parse. | Field | Type | Required | Constraints | Description | |-------|------|----------|-------------|-------------| | `url` | string | Yes | MUST be a string on decode. MUST be non-empty on emission. | URL of the resource being paid for. | | `description` | string | No | — | Human-readable description. | | `mimeType` | string | No | — | Media type of the paid response. | | `serviceName` | string | No | 1–32 characters, printable ASCII (`U+0020`–`U+007E`) only. | Display name of the service. | | `tags` | string[] | No | At most 5 entries; each 1–32 characters, printable ASCII only. | Discovery tags. | | `iconUrl` | string | No | At most 2048 characters. | Icon for the service. | The asymmetry on `url` is deliberate: emission is held to upstream's schema because that is what the interop claim is about, while a decoder has no business refusing a peer's payable 402 over empty metadata. ### 4.2 `accepts[]` Entry Fields Each entry is an x402 V2 `PaymentRequirements`. | Field | Type | Required | Constraints | Description | |-------|------|----------|-------------|-------------| | `scheme` | string | Yes | Non-empty. No control characters. | The single scheme this entry offers. Implementations MUST NOT reject an unrecognized scheme name — a client SKIPS an offer it cannot pay. | | `network` | string | Yes | MUST be CAIP-2: at least 3 characters and containing `":"` (e.g., `"sui:mainnet"`, `"eip155:8453"`). No control characters. | Network identifier. x402 V2 requires CAIP-2, so an emitted 402 carrying anything else is one the pinned `@x402/core` decoder refuses; implementations MUST reject it with `INVALID_PAYLOAD`. The single exception is read-only intake: a document lifted out of a retired flat shape (§11.3) predates the rule — x402 V1's own schema is a non-empty string — and ADR-013 makes reading it an obligation. Such a document MUST be readable and MUST NOT be re-emitted. | | `asset` | string | Yes | Non-empty. No control characters. | Asset/coin type identifier. Chain-specific, opaque to s402. | | `amount` | string | Yes | Canonical non-negative integer. See §4.3. | Payment amount in base units. | | `payTo` | string | Yes | Non-empty. No control characters. | Recipient address. Chain-specific, opaque to s402. | | `maxTimeoutSeconds` | number | No | Positive finite number. Zero and negatives MUST be rejected with `INVALID_PAYLOAD`. | Seconds the facilitator will wait before rejecting. Emitters SHOULD always send it; `60` is the default when a requirement does not name one. Upstream's schema is positive, and an offer good for zero seconds was never payable. | | `extra` | object | No | Opaque bag | Scheme-specific requirement fields. s402's own are listed in §4.2.1. Keys s402 does not name MUST be preserved on decode — this bag is x402's and open by specification. | The fields in §4.2.1 are defined **only for the six s402 schemes**. On an entry naming any other scheme, `extra` is that scheme's own bag: implementations MUST NOT validate it against §4.2.1, MUST NOT lift fields out of it, and MUST carry it through unchanged. One unreadable offer must not make a whole 402 unreadable — the offers a client can pay are validated to the letter, and the rest are skipped. #### 4.2.1 s402 fields inside an entry's `extra` | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `facilitatorUrl` | string | Valid URL. Protocol MUST be `https:` or `http:`. No control characters. No embedded credentials. | URL of the Facilitator service. Omit for direct settlement. | | `expiresAt` | number | Positive finite number. | Unix timestamp in milliseconds. Facilitators MUST reject requirements after this time. | | `protocolFeeBps` | number | Integer, 0–10000. | Protocol fee in basis points. Advisory only — the authoritative fee is set by the Facilitator's on-chain config. | | `protocolFeeAddress` | string | Non-empty. No control characters. | Address that receives the protocol fee. Advisory only. | | `receiptRequired` | boolean | — | Whether the server requires an on-chain receipt. | | `settlementMode` | string | `"facilitator"` or `"direct"` | Settlement mode preference. | | `upto` | object | See §4.6. | Upto-specific parameters. Present when `scheme` is `"upto"`. | | `settlementOverrides` | object | See §4.6. | Settlement overrides for the upto scheme. | | `stream` | object | See §4.7. | Stream-specific parameters. Present when `scheme` is `"stream"`. | | `escrow` | object | See §4.8. | Escrow-specific parameters. Present when `scheme` is `"escrow"`. | | `unlock` | object | See §4.9. | Unlock-specific parameters. Present when `scheme` is `"unlock"`. | | `prepaid` | object | See §4.10. | Prepaid-specific parameters. Present when `scheme` is `"prepaid"`. | | `extensions` | object | Opaque bag | Per-requirement forward-compatible extensibility. Consumers MUST treat extension values as untrusted input. | ### 4.3 Amount Format The `amount` field MUST be a **canonical non-negative integer string**: - MUST match the regular expression `^(0|[1-9][0-9]*)$` - MUST NOT have leading zeros (except the string `"0"` itself) - MUST NOT contain decimals, negative signs, or whitespace - MAY be arbitrarily large (no upper magnitude bound at the wire format level) Examples of valid amounts: `"0"`, `"1"`, `"1000000"`, `"18446744073709551616"` Examples of invalid amounts: `"-1"`, `"007"`, `"1.5"`, `"abc"`, `"1,000"`, `""` ::: info Chain-specific magnitude bounds The wire format does not enforce chain-specific magnitude limits (e.g., u64 for Sui, u256 for Ethereum). Chain adapters SHOULD validate that amounts fit within their chain's native integer type before constructing transactions. ::: ### 4.4 Control Character Rejection The following fields MUST NOT contain ASCII control characters (U+0000–U+001F) or the DEL character (U+007F): - `scheme` - `network` - `asset` - `payTo` - `facilitatorUrl` - `protocolFeeAddress` Control characters in these fields could enable HTTP header injection (CRLF) or log injection (null bytes). Implementations MUST reject requirements containing control characters in these fields with error code `INVALID_PAYLOAD`. ### 4.5 Mandate Sub-Object Used for AP2 (Agent Payment Authorization) mandate requirements. | Field | Type | Required | Constraints | Description | |-------|------|----------|-------------|-------------| | `required` | boolean | Yes | — | Whether a mandate is required (`true`) or optional (`false`). | | `minPerTx` | string | No | Amount format (§4.3) | Minimum per-transaction spending limit the mandate must allow. | | `coinType` | string | No | — | Coin type the mandate must authorize. Must match `asset`. | ### 4.6 Upto Sub-Object Required when `accepts` includes `"upto"`. | Field | Type | Required | Constraints | Description | |-------|------|----------|-------------|-------------| | `maxAmount` | string | Yes | Amount format (§4.3) | Maximum authorized amount in base units. Client deposits this; actual may be less. | | `settlementDeadlineMs` | string | Yes | Amount format (§4.3). Must be in the future. | Deadline for settlement (ms since epoch). After this, payer can reclaim via `expire()`. | | `estimatedAmount` | string | No | Amount format (§4.3). Must be ≤ `maxAmount`. | Server's estimated cost (advisory). Helps clients set a tight `settlementCeiling`. | | `usageReportUrl` | string | No | — | URL where the client can query usage/metering data (informational). | **Settlement Overrides Sub-Object** (used at settle-time): | Field | Type | Required | Constraints | Description | |-------|------|----------|-------------|-------------| | `actualAmount` | string | Yes | Amount format (§4.3). Must be ≤ `maxAmount`. | Actual amount to settle, based on observed usage. | ### 4.7 Stream Sub-Object Required when `accepts` includes `"stream"`. | Field | Type | Required | Constraints | Description | |-------|------|----------|-------------|-------------| | `ratePerSecond` | string | Yes | Amount format (§4.3) | Rate in base units per second. | | `budgetCap` | string | Yes | Amount format (§4.3) | Maximum total budget in base units. | | `minDeposit` | string | Yes | Amount format (§4.3) | Minimum initial deposit in base units. | | `streamSetupUrl` | string | No | — | URL for stream status checks. | ### 4.8 Escrow Sub-Object Required when `accepts` includes `"escrow"`. | Field | Type | Required | Constraints | Description | |-------|------|----------|-------------|-------------| | `seller` | string | Yes | — | Seller/payee address. | | `deadlineMs` | string | Yes | Amount format (§4.3) | Escrow deadline as Unix timestamp in milliseconds. | | `arbiter` | string | No | — | Arbiter address for dispute resolution. | ### 4.9 Unlock Sub-Object Required when `accepts` includes `"unlock"`. | Field | Type | Required | Constraints | Description | |-------|------|----------|-------------|-------------| | `packageId` | string | Yes | — | Move package implementing `pay_and_mint` and the `seal_approve` policy; also the Seal identity namespace. | | `keyServers` | array | Yes | Non-empty | Key-server set the seller encrypted to. Each entry is an object with `objectId` (string, on-chain registered key server) and `weight` (number). | | `threshold` | number | Yes | Integer >= 1 | Threshold `t` in the t-of-n threshold encryption. | | `contentDigest` | string | No | `sha256-` | Commitment to the plaintext, for off-chain/reputational evidence. | > **No identity field appears in requirements, and this is structural rather than an > omission.** The Seal identity is `receiptId || nonce`, where `receiptId` is the object ID > of the `UnlockReceipt` minted by `pay_and_mint`. That receipt does not exist when the > server writes the 402 — it is created by the *buyer's* transaction, in response to this > very response. The identity therefore travels in the unlock **fulfillment**, not here. ### 4.10 Prepaid Sub-Object Required when `accepts` includes `"prepaid"`. | Field | Type | Required | Constraints | Description | |-------|------|----------|-------------|-------------| | `ratePerCall` | string | Yes | Amount format (§4.3) | Maximum base units per API call (rate cap). | | `minDeposit` | string | Yes | Amount format (§4.3) | Minimum deposit amount in base units. | | `withdrawalDelayMs` | string | Yes | Amount format (§4.3). Value MUST be ≥ 60000 (1 min) and ≤ 604800000 (7 days). | Withdrawal delay in milliseconds. Agent must wait this long after the last provider claim before withdrawing remaining funds. | | `maxCalls` | string | No | Amount format (§4.3) | Maximum number of API calls. Omit for unlimited. | | `providerPubkey` | string | No | — | Provider's Ed25519 public key (hex, 32 bytes). Enables v0.2 signed receipt mode. | | `disputeWindowMs` | string | No | — | Dispute window in milliseconds. Min 60000, max 86400000. | **Pairing invariant**: `providerPubkey` and `disputeWindowMs` MUST both be present (v0.2 mode) or both absent (v0.1 mode). Implementations MUST reject requirements where only one is present. ### 4.11 Extensions Field The `extensions` field is an opaque key-value bag for forward-compatible extensibility. Implementations: - MUST pass `extensions` through without content validation - MUST NOT use `extensions` for security-critical fields - MUST treat extension values as untrusted input Scheme implementations that consume specific extension keys SHOULD validate those keys independently. The `extensions` field is intended as a proving ground for features that may be promoted to first-class typed fields in a future protocol version. ## 5. Payment Payload The Payment Payload is sent by the client in the `x-payment` header (or request body for body transport). It contains a signed transaction for the selected payment scheme. ### 5.1 Common Fields | Field | Type | Required | Constraints | Description | |-------|------|----------|-------------|-------------| | `s402Version` | string | No | If present, MUST be `"1"` | Protocol version. Optional on payloads for x402 interop. | | `scheme` | string | Yes | One of: `"exact"`, `"upto"`, `"stream"`, `"escrow"`, `"unlock"`, `"prepaid"` | The payment scheme being used. MUST be in the server's `accepts` array. | | `network` | string | No | Non-empty. No control characters. SHOULD be the CAIP-2 network of the entry it answers (§4.2). | Names which `accepts[]` entry this payment answers. A decoder MUST carry it through (§10.2); a gate uses it to disambiguate two offers that share a scheme. Omitting it is valid — a payload identifies its offer on scheme alone whenever the route's entries for that scheme are the same contract. | | `payload` | object | Yes | Scheme-specific inner fields. See below. | The scheme-specific payment data. | ### 5.2 Payload Inner Fields by Scheme **Exact** (`scheme: "exact"`): | Field | Type | Required | Description | |-------|------|----------|-------------| | `payload.transaction` | string | Yes | Base64-encoded signed transaction bytes. | | `payload.signature` | string | Yes | Base64-encoded signature. | **Upto** (`scheme: "upto"`): | Field | Type | Required | Description | |-------|------|----------|-------------| | `payload.transaction` | string | Yes | Base64-encoded signed deposit transaction (creates UptoDeposit on-chain). | | `payload.signature` | string | Yes | Base64-encoded signature. | | `payload.maxAmount` | string | Yes | Maximum authorized amount. Must match `requirements.upto.maxAmount`. | | `payload.settlementCeiling` | string | No | Client-chosen settlement ceiling (on-chain enforced). Must be ≤ `maxAmount`. | **Stream** (`scheme: "stream"`): | Field | Type | Required | Description | |-------|------|----------|-------------| | `payload.transaction` | string | Yes | Base64-encoded stream creation transaction. | | `payload.signature` | string | Yes | Base64-encoded signature. | **Escrow** (`scheme: "escrow"`): | Field | Type | Required | Description | |-------|------|----------|-------------| | `payload.transaction` | string | Yes | Base64-encoded escrow creation transaction. | | `payload.signature` | string | Yes | Base64-encoded signature. | **Unlock** (`scheme: "unlock"`): | Field | Type | Required | Description | |-------|------|----------|-------------| | `payload.transaction` | string | Yes | Base64-encoded signed `pay_and_mint` transaction. Single-transaction scheme — there is no TX1/TX2 split. | | `payload.signature` | string | Yes | Base64-encoded signature. | **Prepaid** (`scheme: "prepaid"`): | Field | Type | Required | Description | |-------|------|----------|-------------| | `payload.transaction` | string | Yes | Base64-encoded deposit transaction. | | `payload.signature` | string | Yes | Base64-encoded signature. | | `payload.ratePerCall` | string | Yes | Committed rate per call. Must match requirements. | | `payload.maxCalls` | string | No | Committed max calls cap. Must match requirements if present. | ## 6. Settlement Response The Settlement Response is sent by the server in the `payment-response` header of the 200 response after successful payment, or in a non-200 response on failure. | Field | Type | Required | Constraints | Description | |-------|------|----------|-------------|-------------| | `success` | boolean | Yes | — | Whether settlement succeeded. | | `txDigest` | string | No | — | On-chain transaction digest/hash. | | `receiptId` | string | No | — | On-chain receipt object ID. | | `finalityMs` | number | No | Finite number. | Time to finality in milliseconds. | | `actualAmount` | string | No | — | Actual amount settled in base units (upto scheme). | | `depositId` | string | No | — | UptoDeposit object ID (upto scheme). | | `streamId` | string | No | — | Stream object ID (stream scheme). | | `escrowId` | string | No | — | Escrow object ID (escrow scheme). | | `balanceId` | string | No | — | PrepaidBalance object ID (prepaid scheme). | | `error` | string | No | — | Human-readable error message (on failure). | | `errorCode` | string | No | One of the codes in §8. | Machine-readable error code (on failure). | ## 7. Signed Usage Receipts For the prepaid scheme (v0.2 mode), providers sign each API response with a receipt header. This enables cryptographic fraud proofs. ### 7.1 Receipt Header | Header | Direction | Content | |--------|-----------|---------| | `X-S402-Receipt` | Server → Client | Colon-separated receipt fields | ### 7.2 Receipt Format ``` v2:::: ``` | Part | Type | Constraints | Description | |------|------|-------------|-------------| | Version | string | MUST be `"v2"` | Receipt format version. | | Signature | base64 | Decoded length MUST be exactly 64 bytes | Ed25519 signature over the BCS-encoded receipt message. | | Call number | integer string | Positive (> 0) | Sequential call number, 1-indexed. | | Timestamp | integer string | Positive (> 0) | Unix timestamp in milliseconds when the response was generated. | | Response hash | base64 | Decoded length MUST be exactly 32 bytes | SHA-256 hash of the response body. | Implementations MUST reject receipts where: - The header is empty - The number of colon-separated parts is not exactly 5 - The version is not `"v2"` - The call number or timestamp is not a valid positive integer - The signature does not decode to exactly 64 bytes - The response hash does not decode to exactly 32 bytes ## 8. Error Codes Every s402 error carries three fields: `code` (machine-readable), `retryable` (boolean), and `suggestedAction` (human-readable guidance). This design enables autonomous agents to handle errors programmatically. | Code | Retryable | Suggested Action | |------|:---------:|-----------------| | `INSUFFICIENT_BALANCE` | No | Top up wallet balance or try with a smaller amount | | `MANDATE_EXPIRED` | No | Request a new mandate from the delegator | | `MANDATE_LIMIT_EXCEEDED` | No | Request mandate increase or split across transactions | | `STREAM_DEPLETED` | Yes | Top up the stream deposit | | `ESCROW_DEADLINE_PASSED` | No | Create a new escrow with a later deadline | | `UNLOCK_DECRYPTION_FAILED` | Yes | Re-request decryption key with a fresh session key | | `FINALITY_TIMEOUT` | Yes | Transaction submitted but not confirmed — retry finality check | | `FACILITATOR_UNAVAILABLE` | Yes | Fall back to direct settlement if signer is available | | `INVALID_PAYLOAD` | No | Check payload format and re-sign the transaction | | `SCHEME_NOT_SUPPORTED` | No | Use the "exact" scheme (always supported for x402 compat) | | `NETWORK_MISMATCH` | No | Ensure client and server are on the same network | | `SIGNATURE_INVALID` | No | Re-sign the transaction with the correct keypair | | `REQUIREMENTS_EXPIRED` | Yes | Re-fetch payment requirements from the server | | `VERIFICATION_FAILED` | No | Check payment amount and transaction structure | | `SETTLEMENT_FAILED` | Yes | Transient RPC failure during settlement — retry in a few seconds | ## 9. Discovery Servers MAY advertise s402 support at `/.well-known/s402.json`: ```json { "s402Version": "1", "schemes": ["exact", "upto", "stream", "escrow", "unlock", "prepaid"], "networks": ["sui:mainnet"], "assets": ["0x2::sui::SUI"], "facilitatorUrl": "https://facilitator.example.com", "directSettlement": true, "mandateSupport": true, "protocolFeeBps": 50, "protocolFeeAddress": "0x..." } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `s402Version` | string | Yes | MUST be `"1"`. | | `schemes` | string[] | Yes | Supported payment schemes. | | `networks` | string[] | Yes | Supported network identifiers. | | `assets` | string[] | Yes | Supported asset/coin type identifiers. | | `facilitatorUrl` | string | No | Default Facilitator URL. | | `directSettlement` | boolean | Yes | Whether direct settlement (no Facilitator) is supported. | | `mandateSupport` | boolean | Yes | Whether AP2 mandates are supported. | | `protocolFeeBps` | number | Yes | Default protocol fee in basis points (0–10000). | | `protocolFeeAddress` | string | No | Address that receives the protocol fee. | ## 10. Key Stripping (Trust Boundary) All three decode functions (requirements, payload, settlement response) MUST strip unknown top-level keys from decoded objects. Only the keys listed in this specification SHOULD survive decoding. **One deliberate exception**: an `accepts[]` entry's `extra` is x402's bag and open by specification, so unknown keys inside it MUST be preserved. A whitelist there is where the next upstream field (`paymentFlow`, the EIP-712 `name` / `version`) goes missing without erroring. This is a defense-in-depth measure at the HTTP trust boundary — it prevents untrusted fields from propagating into application logic. ### 10.1 Known Requirements Keys Envelope: `x402Version`, `resource`, `error`, `accepts`, `extensions` `resource`: `url`, `description`, `mimeType`, `serviceName`, `tags`, `iconUrl` Each `accepts[]` entry: `scheme`, `network`, `asset`, `amount`, `payTo`, `maxTimeoutSeconds`, `extra` s402 keys lifted out of an entry's `extra`: `facilitatorUrl`, `protocolFeeBps`, `protocolFeeAddress`, `receiptRequired`, `settlementMode`, `expiresAt`, `upto`, `settlementOverrides`, `prepaid`, `stream`, `escrow`, `unlock`, `extensions` — any OTHER key in `extra` is preserved verbatim. Sub-object known keys: - **mandate**: `required`, `minPerTx`, `coinType` - **upto**: `maxAmount`, `settlementDeadlineMs`, `estimatedAmount`, `usageReportUrl` - **settlementOverrides**: `actualAmount` - **stream**: `ratePerSecond`, `budgetCap`, `minDeposit`, `streamSetupUrl` - **escrow**: `seller`, `arbiter`, `deadlineMs` - **unlock**: `packageId`, `keyServers`, `threshold`, `contentDigest` - **prepaid**: `ratePerCall`, `maxCalls`, `minDeposit`, `withdrawalDelayMs`, `providerPubkey`, `disputeWindowMs` ### 10.2 Known Payload Keys Top-level: `s402Version`, `scheme`, `network`, `payload` Inner payload keys per scheme: - **exact, stream, escrow**: `transaction`, `signature` - **upto**: `transaction`, `signature`, `maxAmount`, `settlementCeiling` - **unlock**: `transaction`, `signature` - **prepaid**: `transaction`, `signature`, `ratePerCall`, `maxCalls` ### 10.3 Known Settlement Response Keys `success`, `txDigest`, `receiptId`, `finalityMs`, `actualAmount`, `depositId`, `streamId`, `escrowId`, `balanceId`, `error`, `errorCode` ## 11. x402 Compatibility s402 is a **profile of x402**: the 402 leg is x402 V2's own document, and the payment and receipt legs share x402 V1's header names. **The pin.** Every constraint this document attributes to x402 is read from [`x402-foundation/x402`](https://github.com/x402-foundation/x402) at commit `2cc7e9a6` (`@x402/core` 2.25.0), `typescript/packages/core/src/schemas/index.ts` — `ResourceInfoSchema`, `NetworkSchemaV2`, `PaymentRequirementsV2Schema`, `PaymentRequiredV2Schema`. "The pinned `@x402/core` decoder" elsewhere in this document means that commit. An s402 implementation is free to be stricter than upstream and MUST NOT be looser on emission. ### 11.1 Header Names s402 uses the same HTTP header names as x402 V1. An x402 V1 client sending an `exact` payment can interact with an s402 server without modification. x402 V2 renamed the client header to `payment-signature`. Servers that need to accept x402 V2 clients SHOULD also check the `payment-signature` header. ### 11.2 Protocol Discrimination Both are x402 V2 envelopes carrying `x402Version: 2`. `extensions.s402` is what marks the s402 profile, and its absence is not a reason to refuse a 402. ### 11.3 Retired shapes (intake only) Two flat 402 shapes are no longer emitted by anything and MUST still be readable on intake: x402 V1 (`x402Version: 1` with the requirement fields at the top level), and **s402 wire v1** (`s402Version: "1"` with `accepts` as an array of scheme NAMES). A v1 `accepts` list expands to one `accepts[]` entry per scheme, each carrying the document's shared network, asset, amount, payTo and per-requirement fields, with `exact` hoisted to the front. ### 11.4 Conversion Implementations MAY provide bidirectional conversion between x402 and s402 formats: - **x402 V2 → s402**: no conversion. That envelope is s402's own document; lift the s402 keys out of each entry's `extra` and read `extensions.s402`. - **x402 V1 → s402**: wrap the flat requirement as a single `accepts[]` entry under a V2 envelope. Use `amount` (V2) or `maxAmountRequired` (V1) for the amount field. - **s402 → x402**: every s402 scheme is expressible, one `accepts[]` entry each. An x402 client without a handler for `prepaid` or `unlock` skips that entry — which is what `accepts[]` is for. Conversion MUST validate the `facilitatorUrl` field using the same protocol-only check (§4.2.1) to prevent SSRF via dangerous URL schemes. ## 12. Security Considerations ### 12.1 HTTPS Required s402 payment data (requirements, payloads, settlement responses) travels in HTTP headers as base64-encoded JSON. Without TLS, this data is visible to any network observer. All production deployments MUST use HTTPS. ### 12.2 Requirements Expiration Servers SHOULD set `expiresAt` on payment requirements to prevent replay of stale 402 responses. Facilitators MUST reject requirements where `Date.now() > expiresAt`. A 402 carrying no `extensions.s402` states no `expiresAt`, because the field is s402's. On decode, implementations SHOULD derive one for each offer naming an s402 scheme, as `now + maxTimeoutSeconds × 1000`, and MUST NOT overwrite an expiry the peer stated. Without this, an expiration guard reading an undefined `expiresAt` skips, and every stale-payment defence is silently absent on exactly the traffic that arrived from outside. ### 12.3 Facilitator URL Validation The `facilitatorUrl` field is validated for protocol only (`https:` or `http:`). Implementations that fetch the Facilitator URL MUST apply their own hostname and IP address restrictions (block RFC 1918 private addresses, link-local 169.254.x.x, loopback, cloud metadata endpoints) to prevent SSRF attacks. ### 12.4 Extensions Trust Boundary The `extensions` field is an opaque bag. Implementations MUST NOT trust data in `extensions` for security-critical decisions. Security-critical data SHOULD use first-class typed fields with explicit validation. ### 12.5 Concurrent Payment Deduplication Facilitators SHOULD deduplicate concurrent identical payment requests to prevent double resource access. Deduplication keys SHOULD be derived from the scheme name and transaction/signature fields, not from JSON serialization (which is not canonical). ### 12.6 Key Ordering in JSON JSON key ordering is not guaranteed by the JSON specification. Implementations that serialize s402 objects MUST NOT depend on specific key ordering for correctness. However, for conformance vector compatibility, implementations SHOULD preserve insertion order as described in the [conformance test guide](/guide/conformance). ## 13. Conformance An implementation is **s402-conformant** if it: 1. Correctly encodes and decodes all three message types (requirements, payload, settlement response) using the encoding specified in §3.2 2. Validates all required fields per §4.1, §5.1, and §6 3. Rejects malformed input with the appropriate error code from §8 4. Strips unknown keys on decode per §10 5. Passes **every** machine-readable conformance test vector shipped in the `s402` npm package (196 vectors across 14 files as of v0.9.0) The conformance vectors cover: encode, decode, body transport, x402 compat normalization, receipt format/parse, settlement verification, validation rejection, key stripping, and roundtrip identity. See the [Conformance Vectors guide](/guide/conformance) for the vector format and implementation instructions. --- *This specification is maintained at [s402-protocol.org/specification](https://s402-protocol.org/specification) and [github.com/s402-protocol/core](https://github.com/s402-protocol/core). Contributions and corrections are welcome via GitHub issues or pull requests.*