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. 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.
| 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 ────── ServerThe 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.
| 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:
- JSON-serialize the object:
JSON.stringify(object) - UTF-8 encode the string to bytes
- 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:
- If
Content-Typeincludesapplication/s402+json→ body transport - If the
x-paymentheader is present → header transport - Otherwise → unknown
3.5 Protocol Detection
To determine whether a 402 response is an s402-profile 402 or a plain x402 one:
- Read the
payment-requiredheader - Base64 decode and JSON parse
- If the decoded object contains
extensions.s402→ s402 - Otherwise, if it contains
x402Version→ x402 - 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.
{
"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'sPaymentRequirements extraandextensions.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", ""
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):
schemenetworkassetpayTofacilitatorUrlprotocolFeeAddress
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-<base64url> | 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, wherereceiptIdis the object ID of theUnlockReceiptminted bypay_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
extensionsthrough without content validation - MUST NOT use
extensionsfor 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:<base64(signature)>:<callNumber>:<timestampMs>:<base64(responseHash)>| 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:
{
"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 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
extraand readextensions.s402. - x402 V1 → s402: wrap the flat requirement as a single
accepts[]entry under a V2 envelope. Useamount(V2) ormaxAmountRequired(V1) for the amount field. - s402 → x402: every s402 scheme is expressible, one
accepts[]entry each. An x402 client without a handler forprepaidorunlockskips that entry — which is whataccepts[]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.
13. Conformance
An implementation is s402-conformant if it:
- Correctly encodes and decodes all three message types (requirements, payload, settlement response) using the encoding specified in §3.2
- Validates all required fields per §4.1, §5.1, and §6
- Rejects malformed input with the appropriate error code from §8
- Strips unknown keys on decode per §10
- Passes every machine-readable conformance test vector shipped in the
s402npm 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 for the vector format and implementation instructions.
This specification is maintained at s402-protocol.org/specification and github.com/s402-protocol/core. Contributions and corrections are welcome via GitHub issues or pull requests.