diff --git a/specs/CONTRIBUTING.md b/specs/CONTRIBUTING.md new file mode 100644 index 00000000..56b17daf --- /dev/null +++ b/specs/CONTRIBUTING.md @@ -0,0 +1,108 @@ +# Specs Contributing Guide + +Use this guide when changing the normative x402 v2 documentation in this repository. + +## Directory Structure + +```text +specs/ +├── x402-specification-v2.md +├── transports-v2/ +│ ├── http.md +│ └── mcp.md +├── schemes/ +│ ├── exact/ +│ ├── upto/ +│ ├── batch-settlement/ +│ ├── auth-capture/ +│ └── exact-gasfree/ +├── extensions/ +├── scheme_template.md +├── scheme_impl_template.md +└── transport_template.md +``` + +## Specification Types + +### Core Protocol + +[`x402-specification-v2.md`](x402-specification-v2.md) defines the shared v2 message schemas, +facilitator API, discovery interface, and cross-cutting security rules. + +### Schemes + +Each scheme has a transport-independent overview named `scheme_.md`. Each implemented network +family has a binding named `scheme__.md`. + +Current bindings are: + +| Scheme | EVM | TRON | +| --- | --- | --- | +| `exact` | Yes | Yes | +| `upto` | Yes | Yes | +| `batch-settlement` | Yes | Yes | +| `auth-capture` | Yes | No | +| `exact_gasfree` | No | Yes | + +### Transports + +Transport documents define where the shared protocol objects are carried. This repository specifies +HTTP and MCP under `transports-v2/`. + +### Extensions + +Extension documents define optional data and lifecycle behavior layered on the core protocol. An +extension belongs in this directory only when the current SDK implements it. + +## Proposing a Change + +1. Explain the problem and why an existing scheme, transport, or extension does not cover it. +2. Update the appropriate overview and every affected network binding. +3. Update [`README.md`](README.md) when the support matrix or document set changes. +4. Update the TypeScript implementation and tests in the same change when behavior changes. +5. Verify all relative links and examples before submitting the change. + +Use these templates: + +| Change | Template | +| --- | --- | +| New scheme overview | [`scheme_template.md`](scheme_template.md) | +| New network binding | [`scheme_impl_template.md`](scheme_impl_template.md) | +| New transport | [`transport_template.md`](transport_template.md) | + +## Writing Rules + +- Use **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** in their RFC 2119 sense. +- Use `x402Version: 2` as a JSON number, never a string. +- Use CAIP-2 identifiers: `eip155:` for EVM and `tron:` for TRON. +- Express token amounts as base-10 strings in atomic units. +- Keep Base58Check TRON addresses on the wire unless a binding explicitly requires 20-byte hex for + typed-data construction. +- Include payload examples, verification rules, settlement rules, error behavior, and security + considerations in every network binding. +- Do not document planned functionality as supported behavior. + +## Implementation Sources of Truth + +The public behavior is implemented under: + +- `typescript/packages/core/src/` +- `typescript/packages/mechanisms/evm/src/` +- `typescript/packages/mechanisms/tron/src/` +- `typescript/packages/extensions/src/` +- `typescript/packages/http/` +- `typescript/packages/mcp/src/` + +EVM and core are upstream-derived; TRON is maintained in this repository. Specifications should +describe observable behavior and wire contracts without exposing private keys, RPC credentials, or +deployment secrets. + +## Review Checklist + +- The document describes implemented v2 behavior. +- All required and optional fields agree with exported TypeScript types. +- Signature domains, primary types, and field order agree with the signer and verifier. +- Verification predicts settlement and fails closed where required. +- Replay protection, time bounds, recipient binding, and amount bounds are explicit. +- Contract addresses and supported network IDs agree with in-tree constants. +- Relative Markdown links resolve inside `specs/`. diff --git a/specs/README.md b/specs/README.md new file mode 100644 index 00000000..ac2edec8 --- /dev/null +++ b/specs/README.md @@ -0,0 +1,42 @@ +# BANK OF AI x402 Specifications + +This directory is the normative protocol documentation for the current BANK OF AI TypeScript SDK. +It profiles x402 protocol version 2 for the transports, schemes, networks, and extensions implemented +in this repository. + +## Scope + +- Protocol: x402 v2 +- Networks: EVM (`eip155:*`, currently BSC) and TRON (`tron:*`) +- Transports: HTTP and MCP +- Schemes: `exact`, `upto`, `batch-settlement`, `auth-capture` (EVM), and + `exact_gasfree` (TRON) +- Extensions: `bazaar`, `builder-code`, `eip2612GasSponsoring`, + `erc20ApprovalGasSponsoring`, `offer-receipt`, `payment-identifier`, and + `sign-in-with-x` + +Legacy protocol revisions, unimplemented transports, and unimplemented network bindings are outside +the scope of this directory. + +## Documents + +- [Core protocol](x402-specification-v2.md) +- Transports: [HTTP](transports-v2/http.md), [MCP](transports-v2/mcp.md) +- Schemes: + - [`exact`](schemes/exact/scheme_exact.md): + [EVM](schemes/exact/scheme_exact_evm.md), [TRON](schemes/exact/scheme_exact_tron.md) + - [`upto`](schemes/upto/scheme_upto.md): + [EVM](schemes/upto/scheme_upto_evm.md), [TRON](schemes/upto/scheme_upto_tron.md) + - [`batch-settlement`](schemes/batch-settlement/scheme_batch_settlement.md): + [EVM](schemes/batch-settlement/scheme_batch_settlement_evm.md), + [TRON](schemes/batch-settlement/scheme_batch_settlement_tron.md) + - [`auth-capture`](schemes/auth-capture/scheme_auth_capture.md): + [EVM](schemes/auth-capture/scheme_auth_capture_evm.md) + - [`exact_gasfree`](schemes/exact-gasfree/scheme_exact_gasfree.md): + [TRON](schemes/exact-gasfree/scheme_exact_gasfree_tron.md) +- [Extensions](extensions/) +- [Contributing](CONTRIBUTING.md) + +The protocol separates transport (how messages move), scheme (how value moves), and network binding +(how a scheme is implemented on a chain). A conforming implementation MUST satisfy the core protocol, +one transport specification, and the selected scheme's network binding. diff --git a/specs/extensions/bazaar.md b/specs/extensions/bazaar.md new file mode 100644 index 00000000..d97374ca --- /dev/null +++ b/specs/extensions/bazaar.md @@ -0,0 +1,562 @@ +# Extension: `bazaar` + +## Summary + +The `bazaar` extension enables **resource discovery and cataloging** for x402-enabled endpoints and MCP tools. Resource servers declare their endpoint specifications (HTTP method or MCP tool name, input parameters, and output format) so that facilitators can catalog and index them in a discovery service. + +--- + +## `PaymentRequired` + +A resource server advertises its endpoint specification by including the `bazaar` extension in the `extensions` object of the **402 Payment Required** response. + +The extension follows the standard v2 pattern: +- **`info`**: Contains the actual discovery data (HTTP method or MCP tool name, input parameters, and output format) +- **`schema`**: JSON Schema that validates the structure of `info` + +The `info.input` object uses a discriminated union type, distinguished by the `type` field: +- `input.type: "http"` — HTTP endpoints (further discriminated by `method` into query parameter methods vs body methods) +- `input.type: "mcp"` — MCP (Model Context Protocol) tools + +### Example: GET Endpoint + +```jsonc +{ + "x402Version": 2, + "error": "Payment required", + "resource": { + "url": "https://api.example.com/weather", + "description": "Weather data endpoint", + "mimeType": "application/json", + "serviceName": "Example Weather", + "tags": ["weather", "forecast"], + "iconUrl": "https://api.example.com/icon.png" + }, + "accepts": [ ... ], + "extensions": { + "bazaar": { + "info": { + "input": { + "type": "http", + "method": "GET", + "queryParams": { + "city": "San Francisco" + } + }, + "output": { + "type": "json", + "example": { + "city": "San Francisco", + "weather": "foggy", + "temperature": 60 + } + } + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "input": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "http" }, + "method": { "type": "string", "enum": ["GET", "HEAD", "DELETE"] }, + "queryParams": { + "type": "object", + "properties": { + "city": { "type": "string" } + }, + "required": ["city"] + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["type", "method"], + "additionalProperties": false + }, + "output": { + "type": "object", + "properties": { + "type": { "type": "string" }, + "example": { "type": "object" } + }, + "required": ["type"] + } + }, + "required": ["input"] + } + } + } +} +``` + +### Example: POST Endpoint + +```jsonc +{ + "x402Version": 2, + "error": "Payment required", + "resource": { + "url": "https://api.example.com/search", + "description": "Search endpoint", + "mimeType": "application/json" + }, + "accepts": [ ... ], + "extensions": { + "bazaar": { + "info": { + "input": { + "type": "http", + "method": "POST", + "bodyType": "json", + "body": { + "query": "example" + } + }, + "output": { + "type": "json", + "example": { + "results": [] + } + } + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "input": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "http" }, + "method": { "type": "string", "enum": ["POST", "PUT", "PATCH"] }, + "bodyType": { "type": "string", "enum": ["json", "form-data", "text"] }, + "body": { "type": "object" }, + "queryParams": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["type", "method", "bodyType", "body"], + "additionalProperties": false + }, + "output": { + "type": "object", + "properties": { + "type": { "type": "string" }, + "example": { "type": "object" } + }, + "required": ["type"] + } + }, + "required": ["input"] + } + } + } +} +``` + +### Example: MCP Tool + +```jsonc +{ + "x402Version": 2, + "error": "Payment required", + "resource": { + "url": "https://api.example.com/mcp", + "description": "Advanced AI-powered financial tools", + "mimeType": "application/json" + }, + "accepts": [ ... ], + "extensions": { + "bazaar": { + "info": { + "input": { + "type": "mcp", + "toolName": "financial_analysis", + "description": "Advanced AI-powered financial analysis", + "inputSchema": { + "type": "object", + "properties": { + "ticker": { "type": "string" }, + "analysis_type": { "type": "string", "enum": ["quick", "deep"] } + }, + "required": ["ticker"] + }, + "example": { + "ticker": "AAPL", + "analysis_type": "deep" + } + }, + "output": { + "type": "json", + "example": { + "summary": "Strong fundamentals...", + "score": 8.5 + } + } + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "input": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "mcp" }, + "toolName": { "type": "string" }, + "description": { "type": "string" }, + "transport": { "type": "string", "enum": ["streamable-http", "sse"] }, + "inputSchema": { "type": "object" }, + "example": { "type": "object" } + }, + "required": ["type", "toolName", "inputSchema"], + "additionalProperties": false + }, + "output": { + "type": "object", + "properties": { + "type": { "type": "string" }, + "example": { "type": "object" } + }, + "required": ["type"] + } + }, + "required": ["input"] + } + } + } +} +``` + +--- + +## Discovery Info Structure + +### Input Types + +The `info.input` object describes how to call the endpoint or tool. + +#### Query Parameter Methods (GET, HEAD, DELETE) + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | string | Yes | Always `"http"` | +| `method` | string | Yes | One of `"GET"`, `"HEAD"`, `"DELETE"` | +| `queryParams` | object | No | Query parameter examples | +| `headers` | object | No | Custom header examples | + +#### Body Methods (POST, PUT, PATCH) + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | string | Yes | Always `"http"` | +| `method` | string | Yes | One of `"POST"`, `"PUT"`, `"PATCH"` | +| `bodyType` | string | Yes | One of `"json"`, `"form-data"`, `"text"` | +| `body` | object/string | Yes | Request body example | +| `queryParams` | object | No | Query parameter examples | +| `headers` | object | No | Custom header examples | + +#### MCP Tools + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | string | Yes | Always `"mcp"` | +| `toolName` | string | Yes | MCP tool name (matches what's passed to `tools/call`) | +| `description` | string | No | Human-readable description of the tool | +| `inputSchema` | object | Yes | JSON Schema for the tool's `arguments`, following the MCP [`Tool.inputSchema`](https://spec.modelcontextprotocol.io/) format (a JSON Schema subset with `type: "object"`, `properties`, and `required`). Servers should reuse the same schema their MCP tool already declares. | +| `transport` | string | No | MCP transport protocol. One of `"streamable-http"` or `"sse"`. Defaults to `"streamable-http"` if omitted. | +| `example` | object | No | Example `arguments` object | + +> **Note:** For MCP tools, the unique resource identifier is the tuple (`resource.url`, `input.toolName`). Since MCP multiplexes multiple tools over a single server endpoint, `resource.url` alone may not be unique. Facilitators **must** use both fields when cataloging MCP tools. + +### Output Types + +The `info.output` object (optional) describes the expected response format: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | string | Yes | Response content type (e.g., `"json"`, `"text"`) | +| `format` | string | No | Additional format information | +| `example` | any | No | Example response value | + +> **Note:** For MCP tools, if `output` is omitted, facilitators should assume arbitrary text content (MCP's default response type). + +### Input Type Discriminator + +The `input.type` field acts as a discriminator for the discovery info structure: + +| `input.type` | Structure | Description | +|--------------|-----------|-------------| +| `"http"` | QueryDiscoveryInfo | HTTP GET/HEAD/DELETE with query parameters | +| `"http"` | BodyDiscoveryInfo | HTTP POST/PUT/PATCH with request body (has `bodyType`) | +| `"mcp"` | MCPDiscoveryInfo | MCP tool invocation | + +Facilitators should use `input.type` to determine which validation rules apply. For HTTP inputs, the presence of `bodyType` further distinguishes between query and body methods. + +--- + +## Schema Validation + +The `schema` field contains a JSON Schema (Draft 2020-12) that validates the structure of `info`. + +**Requirements:** +- Must use JSON Schema Draft 2020-12 +- Must define an `input` property (required) +- May define an `output` property (optional) +- Must validate that `input.type` equals `"http"` (for HTTP endpoints) or `"mcp"` (for MCP tools) +- For HTTP endpoints: Must validate the appropriate `method` enum based on operation type +- For MCP tools: Must require `toolName` and `inputSchema` fields + +Facilitators **must** validate `info` against `schema` before cataloging. + +### MCP Schema Example + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "input": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "mcp" }, + "toolName": { "type": "string" }, + "description": { "type": "string" }, + "transport": { "type": "string", "enum": ["streamable-http", "sse"] }, + "inputSchema": { "type": "object" }, + "example": { "type": "object" } + }, + "required": ["type", "toolName", "inputSchema"], + "additionalProperties": false + }, + "output": { + "type": "object", + "properties": { + "type": { "type": "string" }, + "example": {} + }, + "required": ["type"] + } + }, + "required": ["input"] +} +``` + +--- + +## Service Metadata on `resource` + +Resource servers MAY publish provider-level metadata describing the service that +hosts the resource. Facilitators use these fields to enrich Bazaar search results +with a human-readable name, topical tags, and an icon, without any out-of-band +admin step. The fields live on the **top-level `resource` object** of the +`PaymentRequired` response (alongside `url`, `description`, `mimeType`) and are +echoed by clients in the `PaymentPayload.resource` exactly like `description` +and `mimeType`. + +All fields are optional and purely additive. Servers that omit them produce +byte-identical 402 bodies; clients that don't recognize them ignore them. + +| Field | Type | Required | Description | +|---------------|-----------------|----------|----------------------------------------------------------------------------------------------| +| `serviceName` | string | No | Human-readable name for the service (the authority that hosts the resource). | +| `tags` | array of string | No | Short topical tags describing the service. Used for facilitator-side filtering and search. | +| `iconUrl` | string | No | Absolute `https`/`http` URL to an icon image representing the service. | + +### Validation Rules + +The facilitator is a trust boundary: clients echo the `resource` block from +`PaymentRequired` into `PaymentPayload`, so a malicious client could submit +hostile metadata to poison the catalog. SDKs and facilitators MUST apply the +following soft-drop rules during extraction. A field that fails its rule is +discarded; the surrounding metadata is preserved. + +| Field | Rule | On violation | +|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------| +| `serviceName` | Non-empty string of printable ASCII (U+0020–U+007E), length ≤ 32 characters; contains no Unicode control characters (category Cc). | Drop the field. | +| `tags` | Array of strings; at most 5 entries; each entry non-empty, printable ASCII (U+0020–U+007E), length ≤ 32 characters, no Unicode control characters; entries deduplicated case-insensitively (first occurrence wins). | Truncate to the first 5 valid entries; drop individual invalid entries. | +| `iconUrl` | String of length ≤ 2048; parses as an absolute `http://` or `https://` URL; no `data:` / `file:` / other non-http schemes; no userinfo (`user@`); host is IDN-normalized (UTS #46) before checks; not an IP literal (v4 or v6), not in the loopback set (`localhost`, `localhost.localdomain`, `ip6-localhost`, `ip6-loopback`), not an all-digit hostname (decimal IP encodings like `2130706433`), and not a hex literal (`0x7f000001`); contains no control characters. | Drop the field. | + +Implementations MUST percent-decode the iconUrl host before applying the IP / +`localhost` checks (parallel to how `routeTemplate` is decoded before its `..` +and `://` checks). + +The `serviceName` and `tags` ASCII restriction follows the same convention as +`paymentidentifier.id`: bounding the character set to printable ASCII makes +length checks unambiguous and avoids non-ASCII display ambiguity in catalog +UIs. Providers that need localized names should rely on the consuming UI for +internationalization rather than encoding non-ASCII characters in these +fields. + +The TypeScript implementation exposes `isValidServiceName`, `sanitizeTags`, +`isValidIconUrl`, and the combined `sanitizeResourceServiceMetadata` helper. +These helpers apply the rules above, including percent-decoding before the IP +and `localhost` checks for `iconUrl`. + +Hard rejection only happens at the JSON envelope level (handled by existing +extraction error paths). Image content-type, size, and dimension validation +are out of scope for the SDK helpers and remain the facilitator's +responsibility (e.g. via Cloudinary at serve time). + +--- + +## Facilitator Behavior + +When a facilitator receives a `PaymentPayload` containing the `bazaar` extension, it should: + +1. **Validate** the `info` field against the provided `schema` +2. **Extract** the discovery information (resource URL, HTTP method or MCP tool name, input/output specs) + +How a facilitator stores, indexes, and exposes discovered resources is an implementation detail. Facilitators may choose to catalog resources in a database, expose them via a discovery API, or process them in any manner they see fit. + +### Optional Discovery Endpoints + +Facilitators that implement Bazaar discovery may expose discovery APIs to let clients browse and search cataloged resources. + +#### `GET /discovery/resources` + +Lists discoverable x402 resources. + +| Parameter | Type | Required | Description | +| --------- | -------- | -------- | ------------------------------------------- | +| `type` | `string` | Optional | Filter by resource type (for example, `http` or `mcp`) | +| `payTo` | `string` | Optional | Filter by payment recipient address | +| `scheme` | `string` | Optional | Filter by payment scheme (for example, `exact`) | +| `network` | `string` | Optional | Filter by payment network (for example, `eip155:8453`) | +| `extensions` | `string` | Optional | Filter by extension key present on each resource (for example, `bazaar`) | +| `limit` | `number` | Optional | Maximum number of results to return | +| `offset` | `number` | Optional | Number of results to skip for pagination | + +#### `GET /discovery/search` + +Searches discoverable x402 resources using a natural-language query. Response shape mirrors the list endpoint with a `resources` array and optional `pagination`. + +| Parameter | Type | Required | Description | +| --------- | -------- | -------- | ------------------------------------------- | +| `query` | `string` | Yes | Natural-language search query | +| `type` | `string` | Optional | Filter by resource type (for example, `http` or `mcp`) | +| `payTo` | `string` | Optional | Filter by payment recipient address | +| `scheme` | `string` | Optional | Filter by payment scheme (for example, `exact`) | +| `network` | `string` | Optional | Filter by payment network (for example, `eip155:8453`) | +| `extensions` | `string` | Optional | Filter by extension key present on each resource (for example, `bazaar`) | +| `limit` | `number` | Optional | Advisory maximum number of results; facilitator may return fewer or ignore | +| `cursor` | `string` | Optional | Advisory continuation cursor from a previous page | + +Search responses may include: + +| Field | Type | Required | Description | +| ----- | ---- | -------- | ----------- | +| `partialResults` | `boolean` | No | `true` when additional matches were truncated | +| `pagination` | `object` or `null` | No | Pagination details for a paginated response | +| `pagination.limit` | `number` | Yes (when `pagination` is an object) | Number of results in this page | +| `pagination.cursor` | `string` or `null` | Yes (when `pagination` is an object) | Cursor for the next page, or `null` if unavailable | + +### Verify and Settlement Response Header + +After processing a `PaymentPayload`, a facilitator **MAY** append an `EXTENSION-RESPONSES` HTTP header to the verify or settlement response to communicate extension-specific outcomes to the client. + +**Header name:** `EXTENSION-RESPONSES` + +**Header value:** A base64-encoded JSON object keyed by extension name. The `bazaar` key contains the bazaar extension's response: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `bazaar.status` | string | Yes | One of `"success"`, `"processing"`, or `"rejected"` | +| `bazaar.rejectedReason` | string | No | Human-readable explanation. Only present when `status` is `"rejected"` | + +**Status values:** + +| Value | Meaning | +|-------|---------| +| `"success"` | The discovery info was validated and successfully cataloged | +| `"processing"` | The discovery info was accepted and is being cataloged asynchronously | +| `"rejected"` | The discovery info was rejected (e.g., failed schema validation). See `rejectedReason` for details | + +**Example (success):** + +``` +EXTENSION-RESPONSES: eyJiYXphYXIiOnsic3RhdHVzIjoic3VjY2VzcyJ9fQ== +``` +*(base64 of `{"bazaar":{"status":"success"}}`)* + +**Example (rejected):** + +``` +EXTENSION-RESPONSES: eyJiYXphYXIiOnsic3RhdHVzIjoicmVqZWN0ZWQiLCJyZWplY3RlZFJlYXNvbiI6ImluZm8gZmFpbGVkIHNjaGVtYSB2YWxpZGF0aW9uIn19 +``` +*(base64 of `{"bazaar":{"status":"rejected","rejectedReason":"info failed schema validation"}}`)* + +Clients that understand the `bazaar` extension SHOULD read the `bazaar` key of this header to confirm cataloging succeeded and surface any rejection reason for debugging. + +--- + +## Client Behavior + +Clients are expected to echo the `bazaar` extension from `PaymentRequired` into their `PaymentPayload`. If the extension is omitted, discovery cataloging will not occur. + +--- + +## Dynamic Routes and `routeTemplate` + +HTTP endpoints can use parameterized route patterns (e.g. `/users/[userId]`). When a route has +parameter segments, the server extension enriches the extension with two additional fields: + +- **`info.input.pathParams`** — concrete parameter values for this specific request (e.g. `{ "userId": "123" }`) +- **`routeTemplate`** — the canonical template with `:param` syntax (e.g. `/users/:userId`) + +The `routeTemplate` field at the **top level** of the extension object is the catalog key contract between +server and facilitator. Facilitators use it to map all concrete requests (e.g. `/users/123`, `/users/456`) +to a single canonical catalog entry. + +### `routeTemplate` Wire Format + +- The server writes patterns using `[paramName]` syntax internally (matches the route framework convention). +- The extension delivers `routeTemplate` externally using `:paramName` syntax, consistent with REST conventions. +- The field is **absent** for static routes; facilitators MUST treat an absent `routeTemplate` as "use the concrete URL path". + +Example of an enriched extension for a dynamic route: + +```jsonc +{ + "info": { + "input": { + "type": "http", + "method": "GET", + "pathParams": { "userId": "123" } + } + }, + "schema": { ... }, + "routeTemplate": "/users/:userId" +} +``` + +### `routeTemplate` Validation Rules + +The facilitator MUST validate `routeTemplate` before using it as a catalog key. The expected format +uses colon-prefixed parameter identifiers (e.g. `/users/:userId`, `/weather/:country/:city`). +The TypeScript facilitator uses `isValidRouteTemplate`, which applies the +following rules. + +| Rule | Reason | +|------|--------| +| Must be a non-empty string | Empty/absent means "no template" | +| Must start with `/` | Prevents relative paths and external URLs | +| Must match `^/[a-zA-Z0-9_/:.\-~%]+$` | Only allows safe URL path characters and `:param` identifiers | +| Must not contain `..` | Prevents path traversal (`/users/../admin`) | +| Must not contain `://` | Prevents URL injection (`http://evil.com`) | + +All implementations decode percent-encoding (e.g. `%2e%2e` -> `..`) before applying the traversal +and scheme checks. A value that fails any rule is discarded; the facilitator falls back to the +concrete URL path for cataloging. diff --git a/specs/extensions/builder_code.md b/specs/extensions/builder_code.md new file mode 100644 index 00000000..5c77859c --- /dev/null +++ b/specs/extensions/builder_code.md @@ -0,0 +1,297 @@ +# Extension: `builder-code` + +## Summary + +The `builder-code` extension enables **on-chain attribution tracking** for x402 payments by appending [ERC-8021](https://eip.tools/eip/8021) Schema 2 builder codes to settlement transaction calldata. It attributes which application exposed the paid endpoint and which facilitator settled the payment. + +This extension implements **Schema 2** (CBOR-encoded) of ERC-8021. The `m` (custom metadata) and `r` (custom registries) fields are not supported. + +--- + +## ERC-8021 Schema 2 Overview + +ERC-8021 defines a structured data suffix appended to transaction calldata for entity attribution. Schema 2 uses CBOR encoding for extensibility. + +### Suffix Format + +The complete suffix appended to calldata is (ordered end of calldata backwards): + +| Component | Size | Description | +| ------------ | -------- | ------------------------------------------------------- | +| `ercMarker` | 16 bytes | Constant identifier: `80218021802180218021802180218021` | +| `schemaId` | 1 byte | `0x02` for Schema 2 | +| `cborLength` | 2 bytes | Length of CBOR data (big-endian) | +| `cborData` | variable | CBOR-encoded map of attribution fields | + +Wire order: `[cborData][cborLength (2B)][schemaId (1B)][ercMarker (16B)]` + +### CBOR Map Fields + +| Key | Type | Description | +| --- | --------------- | --------------------------------------------------------------- | +| `a` | string | App code — the application that exposed the paid endpoint | +| `w` | string | Wallet code — the facilitator that settled the payment on-chain | +| `s` | string or array of strings | Service code(s) — client-provided attribution | + +All fields are optional. + +### Builder Code Format + +Codes must match the pattern `^[a-z0-9_]{1,32}$`: + +- **Length**: 1-32 characters +- **Characters**: lowercase alphanumeric and underscores only + +--- + +## `PaymentRequired` + +The application declares its builder code per-route in the payment middleware configuration. + +```jsonc +{ + "x402Version": 2, + "error": "Payment required", + "accepts": [ ... ], + "extensions": { + "builder-code": { + "info": { + "a": "my_app" + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "a": { + "type": "string", + "pattern": "^[a-z0-9_]{1,32}$", + "description": "App builder code" + }, + "w": { + "type": "string", + "pattern": "^[a-z0-9_]{1,32}$", + "description": "Wallet builder code" + }, + "s": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9_]{1,32}$" + }, + "description": "Service builder codes" + } + }, + "additionalProperties": false + } + } + } +} +``` + +--- + +## `PaymentPayload` + +The client echoes the server's app code (`a`) and attaches its own service code(s) (`s`). + +```json +{ + "extensions": { + "builder-code": { + "info": { + "a": "my_app", + "s": ["my_client"] + }, + "schema": { "...": "echoed unchanged from PaymentRequired" } + } + } +} +``` + +Layered clients (e.g. an MCP server acting as middleware) can attribute multiple participants by listing several codes as an array: + +```json +{ + "extensions": { + "builder-code": { + "info": { + "a": "my_app", + "s": ["bankofai_mcp", "demo_app"] + }, + "schema": { "...": "echoed unchanged from PaymentRequired" } + } + } +} +``` + +The SDK client normalizes a single service code to an array. The `w` (wallet) field is **not** set by +the client or added to the `PaymentPayload`; the facilitator inserts it only in the encoded calldata +suffix at settlement time. + +--- + +## Builder Code Fields + +| Field | Set by | When | Description | +| ----- | ----------- | ---------------------------------- | -------------------------------------------------------- | +| `a` | Application | Per-route middleware configuration | Identifies the application exposing the paid endpoint | +| `w` | Facilitator | Settlement | Identifies the facilitator settling the payment on-chain | +| `s` | Client | Payment payload construction | Identifies the client or intermediary that participated | + +--- + +## Facilitator Behavior + +When a facilitator settles a payment containing the `builder-code` extension, it: + +1. The resource server verifies that `PaymentPayload.extensions["builder-code"].info.a` preserves `PaymentRequired.extensions["builder-code"].info.a` +2. The facilitator reads `a` (app code) and `s` (service codes) from the payment payload extension's `info` object +3. Adds its own builder code as the `w` (wallet) field +4. Encodes the combined data as an ERC-8021 Schema 2 CBOR suffix +5. Appends the suffix to the settlement transaction calldata + +The facilitator's builder code is configured at initialization and validated against the same `^[a-z0-9_]{1,32}$` pattern. + +### Calldata Suffix Construction + +The facilitator builds the suffix as follows: + +1. CBOR-encode a map containing all present fields (`a`, `s`, `w`) +2. Compute `cborLength` as the byte length of the CBOR data (2 bytes, big-endian) +3. Append: `[cborData][cborLength][0x02][80218021802180218021802180218021]` +4. Return the hex-encoded result for the settlement mechanism to append to calldata + +--- + +## Protocol Flow + +``` +Client (App) Resource Server Facilitator + | | | + 1. |--- request ----------------->| | + | | | + 2. |<-- 402 PaymentRequired ------| | + | extensions.builder-code: | | + | { info: { a: "my_app" }}| | + | | | + 3. | (sign payment, echo extensions) | + | | | + 4. |--- request + payment ------->| | + | extensions.builder-code: | | + | { info: { a: "my_app", | | + | s: ["my_client"] } } | | + | | | + 5. | |--- verify/settle ----------->| + | | extensions.builder-code: | + | | { info: { a: "my_app", | + | | s: ["my_client"] } } | + | | | + 6. | | Facilitator adds w, | + | | encodes CBOR suffix, | + | | appends to calldata: | + | | [cbor({a:"my_app", | + | | s:["my_client"], | + | | w:"my_fac"})] | + | | [cborLen][0x02][mark] | + | | | + 7. |<-- 200 OK + resource data ---| | + | | | +``` + +--- + +## Examples + +### Single App Attribution + +Application declares its builder code: + +```jsonc +{ + "extensions": { + "builder-code": { + "info": { + "a": "bc_myapp" + }, + "schema": { ... } + } + } +} +``` + +Settlement calldata suffix (hex): + +``` +{original_calldata} a161616862635f6d79617070 000c 02 80218021802180218021802180218021 +``` + +Decoded: + +- CBOR: `{"a": "bc_myapp"}` +- cborLength: `0x000c` (12 bytes) +- schemaId: `0x02` +- marker: `80218021802180218021802180218021` + +### App + Facilitator Attribution + +After facilitator adds its `w` code at settlement: + +``` +{original_calldata} a261616862635f6d7961707061777062635f6d79666163696c697461746f72 001f 02 80218021802180218021802180218021 +``` + +Decoded: + +- CBOR: `{"a": "bc_myapp", "w": "bc_myfacilitator"}` +- cborLength: `0x001f` (31 bytes) +- schemaId: `0x02` +- marker: `80218021802180218021802180218021` + +--- + +## Validation + +### Builder Code Validation + +All builder codes (`a`, `w`, and each entry in `s`) must: + +- Match `^[a-z0-9_]{1,32}$` +- Be 1-32 characters long +- Contain only lowercase letters, digits, and underscores + +Invalid codes must be rejected at declaration time (application) and at construction time (facilitator). The facilitator validates each entry in `s` for format only — `s` is client self-reported and cannot be verified against any authoritative source. + +### App Code Echo Validation + +The resource server MUST verify that the `a` field echoed by the client in +`PaymentPayload.extensions["builder-code"].info` exactly matches the field declared in +`PaymentRequired.extensions["builder-code"].info`. A mismatch indicates tampering and the payment +MUST be rejected before it is forwarded for settlement. + + +### Schema Validation + +The `schema` field uses JSON Schema Draft 2020-12. Facilitators should validate `info` against the provided schema. + +--- + +## Parsing + +Off-chain parsers can extract builder code attribution from settlement calldata using the ERC-8021 parsing algorithm: + +1. Extract the last 16 bytes and verify they match the ERC-8021 marker (`80218021...`) +2. Extract the preceding byte as `schemaId` and verify it equals `0x02` +3. Extract the preceding 2 bytes as `cborLength` (big-endian) +4. Extract the preceding `cborLength` bytes as `cborData` +5. Decode `cborData` as a CBOR map +6. Read `a` (app code), `w` (wallet code), and `s` (service codes array) from the map + +--- + +## Responsibilities + +| Role | Responsibility | +| --------------- | ----------------------------------------------------------------------------------------------------------- | +| **Application** | Declares `a` (app code) per-route in the payment middleware configuration | +| **Client** | Echoes `a` from `PaymentRequired`; attaches its service code array as `info.s` in `PaymentPayload` | +| **Facilitator** | Adds `w` (wallet code) at settlement, encodes the full CBOR suffix (`a`, `s`, `w`), appends to calldata | diff --git a/specs/extensions/eip2612_gas_sponsoring.md b/specs/extensions/eip2612_gas_sponsoring.md new file mode 100644 index 00000000..e96254c7 --- /dev/null +++ b/specs/extensions/eip2612_gas_sponsoring.md @@ -0,0 +1,114 @@ +# Extension: `eip2612GasSponsoring` + +## Summary + +`eip2612GasSponsoring` lets a client authorize the canonical Permit2 contract with an off-chain +EIP-2612 permit. The facilitator submits the token permit and the Permit2 payment together, so the +client does not need a prior on-chain approval transaction. + +The current EVM implementation supports Permit2 payments for: + +- [`exact`](../schemes/exact/scheme_exact_evm.md); +- [`upto`](../schemes/upto/scheme_upto_evm.md); and +- the Permit2 deposit path of + [`batch-settlement`](../schemes/batch-settlement/scheme_batch_settlement_evm.md). + +This extension is not used for EIP-3009 transfers or TRON schemes. + +## Declaration + +A resource server advertises the extension in `PaymentRequired.extensions`: + +```json +{ + "eip2612GasSponsoring": { + "info": { + "description": "The facilitator accepts EIP-2612 gasless Permit to `Permit2` canonical contract.", + "version": "1" + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "from": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$" }, + "asset": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$" }, + "spender": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$" }, + "amount": { "type": "string", "pattern": "^[0-9]+$" }, + "nonce": { "type": "string", "pattern": "^[0-9]+$" }, + "deadline": { "type": "string", "pattern": "^[0-9]+$" }, + "signature": { "type": "string", "pattern": "^0x[a-fA-F0-9]+$" }, + "version": { "type": "string", "pattern": "^[0-9]+(\\.[0-9]+)*$" } + }, + "required": [ + "from", + "asset", + "spender", + "amount", + "nonce", + "deadline", + "signature", + "version" + ] + } + } +} +``` + +The extension schema validates the client-supplied `info` object, not the server's descriptive +`info` object. + +## Client payload + +The client adds the extension only when the server advertised it, the selected transfer method is +Permit2, and the current token allowance to Permit2 is insufficient. The signer must support token +contract reads and EIP-712 signing. + +```json +{ + "extensions": { + "eip2612GasSponsoring": { + "info": { + "from": "0x2222222222222222222222222222222222222222", + "asset": "0x55d398326f99059fF775485246999027B3197955", + "spender": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "amount": "1000000", + "nonce": "3", + "deadline": "1786464300", + "signature": "0x...", + "version": "1" + } + } + } +} +``` + +The signature uses the token's EIP-712 domain (`name`, `version`, EVM chain ID, and token address) +and the standard `Permit(owner,spender,value,nonce,deadline)` message. `spender` MUST be the +canonical Permit2 contract. The payment requirements therefore need accurate token `name` and +`version` metadata when automatic client signing is expected. + +For exact and upto payments, `amount` is the Permit2 authorized amount. For a batch deposit it MUST +equal the deposit amount. + +## Facilitator verification + +Before accepting the extension, the facilitator MUST: + +1. validate every field against the advertised schema; +2. match `from` to the payment payer and `asset` to the selected token; +3. require `spender` to equal canonical Permit2; +4. require the deadline to be at least six seconds in the future; +5. enforce the scheme-specific amount invariant; and +6. verify the complete settlement path, normally by simulating the proxy call. + +The token contract validates the EIP-2612 signature and nonce when settlement executes. A malformed, +expired, replayed, or token-incompatible permit causes settlement to fail. + +## Settlement + +For exact and upto, the facilitator calls the scheme proxy's `settleWithPermit`, which invokes the +token permit before consuming the Permit2 authorization in the same transaction. For batch deposit, +the EIP-2612 permit data is included in the Permit2 deposit collector call. + +If sufficient Permit2 allowance already exists, clients normally omit this extension and use the +standard Permit2 settlement path. diff --git a/specs/extensions/erc20_gas_sponsoring.md b/specs/extensions/erc20_gas_sponsoring.md new file mode 100644 index 00000000..feaab5cc --- /dev/null +++ b/specs/extensions/erc20_gas_sponsoring.md @@ -0,0 +1,101 @@ +# Extension: `erc20ApprovalGasSponsoring` + +## Summary + +`erc20ApprovalGasSponsoring` supports Permit2 payments for tokens that cannot use EIP-2612. The +client signs, but does not broadcast, an EIP-1559 transaction calling +`token.approve(Permit2, MaxUint256)`. The facilitator submits that transaction before settlement. + +The current EVM implementation supports Permit2 payments for: + +- [`exact`](../schemes/exact/scheme_exact_evm.md); +- [`upto`](../schemes/upto/scheme_upto_evm.md); and +- the Permit2 deposit path of + [`batch-settlement`](../schemes/batch-settlement/scheme_batch_settlement_evm.md). + +This extension is not used for EIP-3009 transfers or TRON schemes. + +## Declaration + +A resource server advertises the extension in `PaymentRequired.extensions`: + +```json +{ + "erc20ApprovalGasSponsoring": { + "info": { + "description": "The facilitator broadcasts a pre-signed ERC-20 approve() transaction to grant Permit2 allowance.", + "version": "1" + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "from": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$" }, + "asset": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$" }, + "spender": { "type": "string", "pattern": "^0x[a-fA-F0-9]{40}$" }, + "amount": { "type": "string", "pattern": "^[0-9]+$" }, + "signedTransaction": { "type": "string", "pattern": "^0x[a-fA-F0-9]+$" }, + "version": { "type": "string", "pattern": "^[0-9]+(\\.[0-9]+)*$" } + }, + "required": ["from", "asset", "spender", "amount", "signedTransaction", "version"] + } + } +} +``` + +## Client payload + +The client adds the extension only when the server advertised it, the selected transfer method is +Permit2, and the current token allowance is insufficient. Its signer must be able to read the token +allowance, obtain the account nonce and fee estimate, and sign an EIP-1559 transaction. + +```json +{ + "extensions": { + "erc20ApprovalGasSponsoring": { + "info": { + "from": "0x2222222222222222222222222222222222222222", + "asset": "0x55d398326f99059fF775485246999027B3197955", + "spender": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "signedTransaction": "0x02...", + "version": "1" + } + } + } +} +``` + +The transaction target MUST be `asset`, its calldata MUST decode as +`approve(canonicalPermit2, amount)`, and it MUST be signed by `from`. The current client signs an +approval for `MaxUint256`. + +## Facilitator verification + +Before accepting the extension, the facilitator MUST: + +1. validate the client `info` fields against the advertised schema; +2. match `from` to the payment payer and `asset` to the selected token; +3. require both the declared and calldata spender to equal canonical Permit2; +4. parse the serialized transaction and recover its signer; +5. require its target and selector to be the expected token and `approve` call; and +6. verify the Permit2 payment authorization and other scheme-specific invariants. + +The signed transaction also needs a usable chain ID, nonce, gas limit, and fee settings. These values +are ultimately enforced by the network when the transaction is broadcast. + +## Settlement and signer capability + +The facilitator registers an extension signer with `sendTransactions`. Settlement passes two ordered +operations to it: + +1. the client's serialized approval transaction; and +2. an unsigned call to the exact/upto proxy or batch-deposit contract. + +The signer owns the execution strategy. It may submit sequential transactions, an account batch, or +an atomic bundle. Production deployments SHOULD use an atomic or otherwise protected strategy to +avoid approval/settlement races. If the signer implements `simulateTransactions`, verification can +simulate the combined sequence; otherwise the scheme falls back to prerequisite checks. + +The `sendTransactions` result is ordered, and the final transaction hash is treated as the settlement +transaction. A missing extension signer makes this path unavailable. diff --git a/specs/extensions/extension-offer-and-receipt.md b/specs/extensions/extension-offer-and-receipt.md new file mode 100644 index 00000000..6ef11b16 --- /dev/null +++ b/specs/extensions/extension-offer-and-receipt.md @@ -0,0 +1,213 @@ +# Offer and Receipt Extension + +## 1. Scope + +The `offer-receipt` extension lets a resource server sign two off-chain artifacts: + +- an **offer**, committing to one entry in `PaymentRequired.accepts`; and +- a **receipt**, confirming a successful settlement for a resource and payer. + +This document describes the x402 v2 wire format implemented by +`@bankofai/x402-extensions/offer-receipt`. It does not define protocol-v1 conversion rules. + +The extension is optional. A client that does not understand it can continue the normal x402 flow. + +## 2. Extension placement + +The resource server declares the extension under the key `offer-receipt` and returns signed offers +in the `PaymentRequired` object: + +```text +PaymentRequired.extensions["offer-receipt"].info.offers[] +``` + +After successful settlement, it returns one signed receipt in the `SettleResponse` object: + +```text +SettleResponse.extensions["offer-receipt"].info.receipt +``` + +The extension object also contains a JSON Schema in its `schema` field. Offers are generated for +the requirements available to the server. A signing failure for one requirement does not prevent +other offers from being returned. No receipt is emitted when settlement fails. + +## 3. Signed artifact formats + +Both artifacts use one of these envelopes: + +| Field | Type | Required | Meaning | +| --- | --- | --- | --- | +| `format` | string | Yes | `eip712` or `jws` | +| `acceptIndex` | integer | Offers only, optional | Unsigned hint into `accepts[]` | +| `payload` | object | EIP-712 only | Signed message fields | +| `signature` | string | Yes | EIP-712 hex signature or compact JWS | + +For `eip712`, `payload` MUST be present and `signature` is a `0x`-prefixed ECDSA signature. For +`jws`, `payload` MUST be omitted because it is already the middle component of the compact JWS. + +`acceptIndex` is not signed. Clients MAY use it as a lookup hint, but MUST confirm that the signed +offer fields match the selected `PaymentRequirements` entry. + +## 4. Offer + +### 4.1 Payload + +| Field | Type | Required | Meaning | +| --- | --- | --- | --- | +| `version` | number | Yes | Offer payload version; currently `1` | +| `resourceUrl` | string | Yes | URL of the protected resource | +| `scheme` | string | Yes | Payment scheme, such as `exact` | +| `network` | string | Yes | CAIP-2 network identifier | +| `asset` | string | Yes | Asset identifier from `PaymentRequirements` | +| `payTo` | string | Yes | Payment receiver | +| `amount` | string | Yes | Atomic-unit amount from `PaymentRequirements` | +| `validUntil` | number | Yes | Unix timestamp in seconds | + +The current server derives `scheme`, `network`, `asset`, `payTo`, and `amount` without modification +from the corresponding v2 `PaymentRequirements`. By default, `validUntil` is the current time plus +`maxTimeoutSeconds`; route configuration can override it with `offerValiditySeconds`. + +### 4.2 EIP-712 offer schema + +Domain: + +```json +{ + "name": "x402 offer", + "version": "1", + "chainId": 1 +} +``` + +Primary type `Offer`: + +```json +[ + { "name": "version", "type": "uint256" }, + { "name": "resourceUrl", "type": "string" }, + { "name": "scheme", "type": "string" }, + { "name": "network", "type": "string" }, + { "name": "asset", "type": "string" }, + { "name": "payTo", "type": "string" }, + { "name": "amount", "type": "string" }, + { "name": "validUntil", "type": "uint256" } +] +``` + +`chainId` is intentionally constant because the signature is an off-chain attestation. The actual +payment network remains part of the signed payload. + +### 4.3 Example + +```json +{ + "format": "eip712", + "acceptIndex": 0, + "payload": { + "version": 1, + "resourceUrl": "https://api.example.com/report", + "scheme": "exact", + "network": "eip155:56", + "asset": "0x55d398326f99059fF775485246999027B3197955", + "payTo": "0x1111111111111111111111111111111111111111", + "amount": "1000000", + "validUntil": 1786464300 + }, + "signature": "0x..." +} +``` + +## 5. Receipt + +### 5.1 Payload + +| Field | Type | Required | Meaning | +| --- | --- | --- | --- | +| `version` | number | Yes | Receipt payload version; currently `1` | +| `network` | string | Yes | Settled CAIP-2 network | +| `resourceUrl` | string | Yes | URL of the delivered resource | +| `payer` | string | Yes | Settled payer identifier | +| `issuedAt` | number | Yes | Unix timestamp in seconds | +| `transaction` | string | No | Settlement transaction hash | + +Transaction disclosure is disabled by default. A route enables it with `includeTxHash: true`. A JWS +receipt omits `transaction` when it is not disclosed; an EIP-712 receipt uses an empty string because +the typed-data schema has a fixed field set. + +### 5.2 EIP-712 receipt schema + +Domain: + +```json +{ + "name": "x402 receipt", + "version": "1", + "chainId": 1 +} +``` + +Primary type `Receipt`: + +```json +[ + { "name": "version", "type": "uint256" }, + { "name": "network", "type": "string" }, + { "name": "resourceUrl", "type": "string" }, + { "name": "payer", "type": "string" }, + { "name": "issuedAt", "type": "uint256" }, + { "name": "transaction", "type": "string" } +] +``` + +### 5.3 Example + +```json +{ + "format": "jws", + "signature": "eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6d2ViOmFwaS5leGFtcGxlI2tleS0xIn0.eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZWlwMTU1OjU2IiwicmVzb3VyY2VVcmwiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbS9yZXBvcnQiLCJwYXllciI6IjB4MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMiIsImlzc3VlZEF0IjoxNzg2NDY0MDAwfQ.signature" +} +``` + +## 6. JWS requirements + +The protected header contains: + +| Field | Required | Meaning | +| --- | --- | --- | +| `alg` | Yes | Signing algorithm, for example `ES256K` or `EdDSA` | +| `kid` | Yes | DID URL used to resolve the verification key | + +The payload is serialized with JSON Canonicalization Scheme semantics and encoded as a compact JWS. +The SDK verifier accepts an explicit public key or resolves the `kid` forms supported by its DID +resolver, including `did:key`, `did:jwk`, and `did:web`. + +## 7. Verification requirements + +A client consuming an offer SHOULD: + +1. verify the JWS or recover the EIP-712 signer; +2. establish that the signer is authorized for `resourceUrl`; +3. reject an expired offer; +4. match `scheme`, `network`, `asset`, `payTo`, and `amount` against the selected requirement; and +5. treat `acceptIndex` only as an unsigned hint. + +A client consuming a receipt SHOULD verify the signature and signer authorization, then compare +`resourceUrl`, `network`, and `payer` with the accepted offer and local wallet. It SHOULD also enforce +an application-defined maximum receipt age. + +Cryptographic signature verification alone does not prove that a key controls a resource. The DID, +TLS origin, or another trust mechanism must bind the recovered key to the resource server. + +## 8. Server configuration + +The TypeScript server extension is registered with an issuer created by either +`createJWSOfferReceiptIssuer` or `createEIP712OfferReceiptIssuer`. A route declares: + +```ts +declareOfferReceiptExtension({ + includeTxHash: false, + offerValiditySeconds: 300, +}); +``` + +The default is privacy-minimal: receipt transaction hashes are excluded unless explicitly enabled. diff --git a/specs/extensions/payment_identifier.md b/specs/extensions/payment_identifier.md new file mode 100644 index 00000000..4b7e379c --- /dev/null +++ b/specs/extensions/payment_identifier.md @@ -0,0 +1,125 @@ +# Extension: `payment-identifier` + +## Summary + +The `payment-identifier` extension enables clients to provide an `id` that serves as an idempotency key. Both resource servers and facilitators consume `PaymentPayload`, so this can be leveraged at either or both points in the stack to deduplicate requests and return cached responses for repeated submissions. + +--- + +## `PaymentRequired` + +Server advertises support: + +```json +{ + "extensions": { + "payment-identifier": { + "info": { + "required": false + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "required": { "type": "boolean" }, + "id": { "type": "string", "minLength": 16, "maxLength": 128 } + }, + "required": ["required"] + } + } + } +} +``` + +--- + +## `PaymentPayload` + +Client echoes the extension and appends an `id`: + +```json +{ + "extensions": { + "payment-identifier": { + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "required": { "type": "boolean" }, + "id": { "type": "string", "minLength": 16, "maxLength": 128 } + }, + "required": ["required"] + }, + "info": { + "required": false, + "id": "pay_7d5d747be160e280504c099d984bcfe0" + } + } + } +} +``` + +--- + +## `required` Field + +- **Type**: boolean +- **Purpose**: Indicates whether the server requires clients to include a payment identifier +- **Default**: `false` (payment identifier is optional) + +--- + +## `id` Format + +- **Length**: 16-128 characters +- **Characters**: alphanumeric, hyphens, underscores +- **Recommendation**: UUID v4 with prefix (e.g., `pay_`) + +--- + +## Application idempotency policy + +The extension package does not provide a persistence layer, response cache, or HTTP status mapping. +It provides declaration, generation, format validation, and extraction helpers. Resource servers and +facilitators that use the identifier for idempotency SHOULD implement a policy such as: + +| Scenario | Recommended behavior | +|----------|-----------------| +| New `id` | Process request normally | +| Same `id`, same payload | Return cached response | +| Same `id`, different payload | Return 409 Conflict | +| `required: true`, no `id` provided | Return 400 Bad Request | + +### Request Binding + +Resource servers and facilitators should bind each `id` to a normalized request +fingerprint before returning a cached result. The fingerprint should cover the +parts of the request that make the paid operation unique, such as: + +- `scheme` +- `network` +- `asset` +- `amount` +- `payTo` +- resource path and method +- application-level operation or order identifier + +Implementations should store the first observed fingerprint with the `id`. +Later requests with the same `id` and the same fingerprint can return the +cached response. Later requests with the same `id` and a different fingerprint +should fail with `409 Conflict` instead of reusing the cached response or +executing a second operation. + +Servers should avoid using `id` alone as the storage key for authorization +decisions when the same backend handles multiple paid resources. Scope the key +by tenant, merchant, route, or facilitator account when those boundaries exist. + +--- + +## Responsibilities + +Both resource servers and facilitators consume `PaymentPayload`, so this extension can be leveraged at either or both points. The application supplies the stateful behavior: + +- **Resource server**: May implement request deduplication and response caching +- **Facilitator**: May implement verify/settle idempotency +- **Client**: Generates unique `id`, reuses same `id` on retries; must provide `id` if server sets `required: true` diff --git a/specs/extensions/sign-in-with-x.md b/specs/extensions/sign-in-with-x.md new file mode 100644 index 00000000..0d1a057d --- /dev/null +++ b/specs/extensions/sign-in-with-x.md @@ -0,0 +1,310 @@ +# Extension: `sign-in-with-x` + +## Summary + +The `sign-in-with-x` extension enables [CAIP-122](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-122.md) compliant wallet-based authentication for x402-protected resources. Clients prove control of a wallet address by signing a challenge message, allowing servers to identify returning users and skip payment for addresses that have previously paid. + +This is a **Server ↔ Client** extension. The Facilitator is not involved in the authentication flow. + +## PaymentRequired + +A Server advertises SIWX support by including the `sign-in-with-x` key in the `extensions` object of the `402 Payment Required` response. + +```json +{ + "x402Version": 2, + "accepts": [ + { + "scheme": "exact", + "network": "eip155:8453", + "amount": "10000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 60, + "extra": { + "name": "USDC", + "version": "2" + } + } + ], + "extensions": { + "sign-in-with-x": { + "info": { + "domain": "api.example.com", + "uri": "https://api.example.com/premium-data", + "version": "1", + "nonce": "a1b2c3d4e5f67890a1b2c3d4e5f67890", + "issuedAt": "2024-01-15T10:30:00.000Z", + "expirationTime": "2024-01-15T10:35:00.000Z", + "statement": "Sign in to access premium data", + "resources": ["https://api.example.com/premium-data"] + }, + "supportedChains": [ + { + "chainId": "eip155:8453", + "type": "eip191" + } + ], + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "domain": { "type": "string" }, + "address": { "type": "string" }, + "statement": { "type": "string" }, + "uri": { "type": "string", "format": "uri" }, + "version": { "type": "string" }, + "chainId": { "type": "string" }, + "type": { "type": "string" }, + "nonce": { "type": "string" }, + "issuedAt": { "type": "string", "format": "date-time" }, + "expirationTime": { "type": "string", "format": "date-time" }, + "notBefore": { "type": "string", "format": "date-time" }, + "requestId": { "type": "string" }, + "resources": { "type": "array", "items": { "type": "string", "format": "uri" } }, + "signature": { "type": "string" } + }, + "required": [ + "domain", + "address", + "uri", + "version", + "chainId", + "type", + "nonce", + "issuedAt", + "signature" + ] + } + } + } +} +``` + +### Multi-Chain Support + +Servers supporting multiple chains (e.g., both EVM and Solana) can include multiple entries in `supportedChains`: + +```jsonc +{ + "x402Version": 2, + "accepts": [...], + "extensions": { + "sign-in-with-x": { + "info": { + "domain": "api.example.com", + "uri": "https://api.example.com/premium-data", + "version": "1", + "nonce": "a1b2c3d4e5f67890a1b2c3d4e5f67890", + "issuedAt": "2024-01-15T10:30:00.000Z", + "expirationTime": "2024-01-15T10:35:00.000Z", + "statement": "Sign in to access premium data", + "resources": ["https://api.example.com/premium-data"] + }, + "supportedChains": [ + { + "chainId": "eip155:8453", + "type": "eip191" + }, + { + "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "type": "ed25519" + } + ], + "schema": {...} + } + } +} +``` + +Clients match their wallet's `chainId` against `supportedChains` and use the first matching entry. The same `nonce` is shared across all chains, preventing replay attacks when authenticating with different wallets. + +--- + +## Client Request + +To authenticate, the Client signs the challenge message and sends the proof in the `SIGN-IN-WITH-X` HTTP header as base64-encoded JSON. + +```http +GET /premium-data HTTP/1.1 +Host: api.example.com +SIGN-IN-WITH-X: eyJkb21haW4iOiJhcGkuZXhhbXBsZS5jb20iLCJhZGRyZXNzIjoiMHg4NTdiMDY1MTlFOTFlM0E1NDUzODc5MWJEYmIwRTIyMzczZTM2YjY2IiwidXJpIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20vcHJlbWl1bS1kYXRhIiwidmVyc2lvbiI6IjEiLCJjaGFpbklkIjoiZWlwMTU1Ojg0NTMiLCJ0eXBlIjoiZWlwMTkxIiwibm9uY2UiOiJhMWIyYzNkNGU1ZjY3ODkwYTFiMmMzZDRlNWY2Nzg5MCIsImlzc3VlZEF0IjoiMjAyNC0wMS0xNVQxMDozMDowMC4wMDBaIiwiZXhwaXJhdGlvblRpbWUiOiIyMDI0LTAxLTE1VDEwOjM1OjAwLjAwMFoiLCJzdGF0ZW1lbnQiOiJTaWduIGluIHRvIGFjY2VzcyBwcmVtaXVtIGRhdGEiLCJyZXNvdXJjZXMiOlsiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20vcHJlbWl1bS1kYXRhIl0sInNpZ25hdHVyZVNjaGVtZSI6ImVpcDE5MSIsInNpZ25hdHVyZSI6IjB4MmQ2YTc1ODhkNmFjY2E1MDVjYmYwZDlhNGEyMjdlMGM1MmM2YzM0MDA4YzhlODk4NmExMjgzMjU5NzY0MTczNjA4YTJjZTY0OTY2NDJlMzc3ZDZkYThkYmJmNTgzNmU5YmQxNTA5MmY5ZWNhYjA1ZGVkM2Q2MjkzYWYxNDhiNTcxYyJ9 +``` + +The base64 header decodes to: + +```json +{ + "domain": "api.example.com", + "address": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "uri": "https://api.example.com/premium-data", + "version": "1", + "chainId": "eip155:8453", + "type": "eip191", + "nonce": "a1b2c3d4e5f67890a1b2c3d4e5f67890", + "issuedAt": "2024-01-15T10:30:00.000Z", + "expirationTime": "2024-01-15T10:35:00.000Z", + "statement": "Sign in to access premium data", + "resources": ["https://api.example.com/premium-data"], + "signatureScheme": "eip191", + "signature": "0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a1283259764173608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3d6293af148b571c" +} +``` + +--- + +## Server-Declared Fields + +### Message Metadata (`info`) + +The Server includes these fields in `extensions["sign-in-with-x"].info`: + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `domain` | `string` | Required | Server's domain (e.g., `"api.example.com"`). | +| `uri` | `string` | Required | Full resource URI being accessed. | +| `version` | `string` | Required | CAIP-122 version. Always `"1"`. | +| `nonce` | `string` | Required | Cryptographic nonce (32 hex characters). Server MUST generate this. | +| `issuedAt` | `string` | Required | ISO 8601 timestamp when challenge was created. | +| `statement` | `string` | Optional | Human-readable purpose for signing. | +| `expirationTime` | `string` | Optional | ISO 8601 timestamp when challenge expires. Emitted only when `expirationSeconds` is configured. | +| `notBefore` | `string` | Optional | ISO 8601 timestamp before which the signature is not valid. | +| `requestId` | `string` | Optional | Correlation ID for the request. | +| `resources` | `string[]` | Optional | URIs associated with the request. | + +### Authentication Methods (`supportedChains[]`) + +The Server declares supported authentication methods in `extensions["sign-in-with-x"].supportedChains`: + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `chainId` | `string` | Required | CAIP-2 chain identifier (e.g., `"eip155:8453"`, `"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"`). | +| `type` | `string` | Required | Signature algorithm: `"eip191"` for EVM, `"ed25519"` for Solana. | +| `signatureScheme` | `string` | Optional | Hint for client signing UX: `"eip191"`, `"eip1271"`, `"eip6492"`, or `"siws"`. | + +Clients select the first entry in `supportedChains` that matches their wallet's chain. + +--- + +## Client Proof Fields + +The Client echoes all server fields and adds: + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `address` | `string` | Required | Wallet address that signed the message. Checksummed for EVM, Base58 for Solana. | +| `signature` | `string` | Required | Cryptographic signature. Hex-encoded (`0x...`) for EVM, Base58 for Solana. | + +--- + +## Supported Chains + +### EVM (`eip155:*`) + +- **Type**: `eip191` +- **Signature Schemes**: `eip191` (EOA), `eip1271` (smart contract wallet), `eip6492` (counterfactual wallet) +- **Message Format**: [EIP-4361 (SIWE)](https://eips.ethereum.org/EIPS/eip-4361) +- **Chain ID Examples**: `eip155:1` (Ethereum), `eip155:8453` (Base), `eip155:137` (Polygon) + +### Solana (`solana:*`) + +- **Type**: `ed25519` +- **Signature Scheme**: `siws` +- **Message Format**: [Sign-In With Solana](https://github.com/phantom/sign-in-with-solana) +- **Chain ID Examples**: `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` (mainnet), `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` (devnet) + +--- + +## Message Format + +### EVM (SIWE/EIP-4361) + +``` +api.example.com wants you to sign in with your Ethereum account: +0x857b06519E91e3A54538791bDbb0E22373e36b66 + +Sign in to access premium data + +URI: https://api.example.com/premium-data +Version: 1 +Chain ID: 8453 +Nonce: a1b2c3d4e5f67890a1b2c3d4e5f67890 +Issued At: 2024-01-15T10:30:00.000Z +Expiration Time: 2024-01-15T10:35:00.000Z +Resources: +- https://api.example.com/premium-data +``` + +### Solana (SIWS) + +``` +api.example.com wants you to sign in with your Solana account: +BSmWDgE9ex6dZYbiTsJGcwMEgFp8q4aWh92hdErQPeVW + +Sign in to access premium data + +URI: https://api.example.com/premium-data +Version: 1 +Chain ID: 5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp +Nonce: a1b2c3d4e5f67890a1b2c3d4e5f67890 +Issued At: 2024-01-15T10:30:00.000Z +Expiration Time: 2024-01-15T10:35:00.000Z +Resources: +- https://api.example.com/premium-data +``` + +--- + +## Verification Logic + +When the Server receives a request with the `SIGN-IN-WITH-X` header: + +### 1. Parse Header + +Base64 decode the header value and JSON parse the result. + +### 2. Validate Message Fields + +- **Domain**: `domain` MUST match the hostname exposed by the HTTP adapter's current request URL. +- **URI**: `uri` MUST start with the origin exposed by the HTTP adapter's current request URL. +- **Issued At**: `issuedAt` MUST be recent (default: < 5 minutes) and MUST NOT be in the future. +- **Expiration**: If `expirationTime` is present, it MUST be in the future. +- **Not Before**: If `notBefore` is present, it MUST be in the past. +- **Nonce**: The server generates a fresh 16-byte nonce. Replay checks run when storage implements both nonce methods. + +The SDK reports human-readable validation errors through the optional `onEvent` callback. A failed +SIWX attempt does not itself fail the request; the normal payment flow continues. + +### 3. Verify Signature + +Route verification by `chainId` prefix: + +- **`eip155:*`**: Reconstruct SIWE message, verify using ECDSA recovery (EOA) or on-chain verification (EIP-1271/EIP-6492 for smart wallets). +- **`solana:*`**: Reconstruct SIWS message, verify Ed25519 signature. + +The SDK reports human-readable errors for unsupported namespaces, invalid encodings, signature +length errors, verifier failures, and invalid signatures. + +### 4. Check Payment History + +If signature is valid, the Server checks whether the recovered `address` has previously paid for the requested resource. This is application-specific logic. + +--- + +## Security Considerations + +- **Domain Binding**: The current hook validates against the hostname and origin exposed by the HTTP adapter. Behind a proxy, the adapter MUST expose the canonical browser-visible URL and must not trust unvalidated forwarded headers. +- **Nonce Uniqueness**: Challenges use fresh random nonces. Production storage SHOULD implement both `hasUsedNonce` and `recordNonce`; configuring only one is rejected. +- **Temporal Bounds**: The `issuedAt`, `expirationTime`, and `notBefore` fields constrain signature validity windows. +- **Chain-Specific Verification**: Signatures are verified using chain-appropriate algorithms, preventing cross-chain signature reuse. +- **Smart Wallet Support**: EIP-1271 and EIP-6492 verification requires an RPC call to the wallet contract. + +--- + +## References + +- [CAIP-122: Sign-In With X](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-122.md) +- [EIP-4361: Sign-In With Ethereum (SIWE)](https://eips.ethereum.org/EIPS/eip-4361) +- [Sign-In With Solana](https://github.com/phantom/sign-in-with-solana) +- [CAIP-2: Blockchain ID Specification](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) +- [Core x402 Specification](../x402-specification-v2.md) diff --git a/specs/extensions/trc20_approval_resource_sponsoring.md b/specs/extensions/trc20_approval_resource_sponsoring.md new file mode 100644 index 00000000..4503d5ba --- /dev/null +++ b/specs/extensions/trc20_approval_resource_sponsoring.md @@ -0,0 +1,220 @@ +# Extension: `trc20ApprovalResourceSponsoring` + +## Summary + +`trc20ApprovalResourceSponsoring` enables the first `exact + Permit2` payment from an activated TRON +externally owned account (EOA) without requiring the payer to hold or burn TRX. The Client signs, but +does not broadcast, a transaction calling `token.approve(canonicalPermit2, MaxUint256)`. The +Facilitator validates the signed transaction, temporarily delegates the required Energy and, when +needed, Bandwidth to the payer, broadcasts the unchanged Approval, reclaims the delegated resource +share, and settles the Permit2 payment. + +The extension uses the normal x402 flow. It does not add a `prepare` endpoint or require an additional +Resource Server interaction. + +Version `1` supports: + +- the [`exact` TRON binding](../schemes/exact/scheme_exact_tron.md) with + `extra.assetTransferMethod = "permit2"`; +- activated, single-signature EOAs using the default owner permission; +- an allowance of exactly zero; +- `approve(canonicalPermit2, MaxUint256)`; and +- temporary Stake 2.0 delegation with `lock = false`. + +A non-zero but insufficient allowance fails with `approval_reset_required`. This extension does not +define a general resource-rental API, a Sponsor Fee, collateral, GasFree-account settlement, or an +external Energy-provider protocol. + +## Declaration + +A Resource Server advertises the extension in `PaymentRequired.extensions`: + +```json +{ + "trc20ApprovalResourceSponsoring": { + "info": { + "description": "The facilitator sponsors TRON Energy and Bandwidth for a pre-signed TRC-20 approve transaction.", + "version": "1" + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "from": { "type": "string", "pattern": "^T[1-9A-HJ-NP-Za-km-z]{33}$" }, + "asset": { "type": "string", "pattern": "^T[1-9A-HJ-NP-Za-km-z]{33}$" }, + "spender": { "type": "string", "pattern": "^T[1-9A-HJ-NP-Za-km-z]{33}$" }, + "amount": { "const": "115792089237316195423570985008687907853269984665640564039457584007913129639935" }, + "signedTransaction": { "type": "string", "pattern": "^(?:[0-9a-f]{2})+$", "maxLength": 16384 }, + "version": { "const": "1" } + }, + "required": ["from", "asset", "spender", "amount", "signedTransaction", "version"] + } + } +} +``` + +`signedTransaction` is the lowercase hexadecimal encoding, without a `0x` prefix, of the complete +signed TRON `Transaction` protobuf. Its exact bytes are authoritative. The redundant display fields +`from`, `asset`, `spender`, and `amount` MUST match the independently decoded transaction. + +Approval lifetime and `fee_limit` bounds are local Client and Facilitator policy, not Extension wire +fields. + +## Client payload + +The Client adds the extension only when the Server advertised version `"1"`, the selected transfer +method is Permit2, and the current allowance is exactly zero. When using this extension, the Client +MUST return the signed Approval to the Facilitator and MUST NOT broadcast it itself. + +```json +{ + "extensions": { + "trc20ApprovalResourceSponsoring": { + "info": { + "from": "TJRyWwFs9wTFGZg3JbrVriFbNfCug5tDeC", + "asset": "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", + "spender": "TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h", + "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "signedTransaction": "0a...", + "version": "1" + } + } + } +} +``` + +The abbreviated hexadecimal value above shows the wire shape and is not a broadcastable test vector. + +Before signing, the Client MUST construct and inspect one `TriggerSmartContract` transaction with: + +- `owner_address` equal to the payer; +- `contract_address` equal to the selected TRC-20 asset; +- calldata exactly equal to `approve(canonicalPermit2, MaxUint256)` with no trailing bytes; +- zero TRX and TRC-10 value; +- no memo or custom `permission_id`; +- recent TAPOS fields; +- a short expiration; and +- a `fee_limit` within the Client's local safety policy. + +The Client computes `txID = SHA-256(raw_data protobuf bytes)`, signs that digest, and serializes the +complete transaction containing the unchanged `raw_data` and exactly one signature. The separate +Permit2 signature remains the authority for the exact payment amount, settlement proxy, recipient, +nonce, and deadline. + +## Facilitator verification + +Before accepting the extension, the Facilitator MUST: + +1. require extension version exactly `"1"` and validate the Client `info` fields against its locally + pinned version `1` schema; +2. decode the exact `signedTransaction` bytes as a TRON `Transaction` protobuf, reject malformed, + duplicate, unknown, or unsupported fields, and require exactly one `raw_data` and one 65-byte + signature; +3. compute `approvalTxID = SHA-256(raw_data)` and validate the java-tron-compatible secp256k1 + signature; +4. require the recovered signer, `owner_address`, extension `from`, and Permit2 payer to identify the + same activated EOA using the supported owner permission; +5. require exactly one `TriggerSmartContract` call whose target equals `PaymentRequirements.asset` + and an allowlisted TRC-20 contract; +6. require calldata exactly equal to `approve(canonicalPermit2, MaxUint256)` and match the decoded + asset, spender, and amount to the extension display fields; +7. reject native value, TRC-10 value, memo, additional contracts, additional signatures, and custom + permissions; +8. validate TAPOS, timestamp, expiration, and a sufficient remaining broadcast window; +9. require `fee_limit` to cover the current Energy estimate without exceeding the Facilitator's + local hard cap or the network maximum; +10. verify the Permit2 payment authorization, including network, payer, asset, amount, settlement + proxy, `payTo`, nonce, and deadline; +11. require allowance exactly zero for a new sponsorship, sufficient token balance, and successful + simulation of the exact signed Approval; and +12. estimate the required Energy and Bandwidth and reject the request when resource or sponsorship + policy limits are exceeded. + +The signed transaction is immutable. The Facilitator MUST broadcast the exact bytes supplied by the +Client and MUST require the node-returned transaction ID to equal the computed `approvalTxID`. + +`/verify` is read-only and MUST NOT reserve, delegate, purchase, or broadcast resources. `/settle` +MUST repeat all mutable checks immediately before creating a chain-side effect. Any decoding, +signature, simulation, RPC, or policy error on the sponsored path fails closed. + +## Settlement and resource sponsorship + +The Facilitator supplies resources only after it holds both the final signed Approval and a valid +Permit2 payment authorization. `(network, approvalTxID)` MUST identify at most one sponsorship +operation so concurrent or retried `/settle` calls cannot create duplicate delegations. + +Settlement proceeds in this order: + +1. estimate the Approval's Energy and Bandwidth requirements and apply bounded safety margins; +2. reserve sufficient sponsorship capacity under the Facilitator's local policy; +3. submit one unlocked `DelegateResource` transaction for each required resource type; +4. confirm the delegated Energy and Bandwidth are visible and sufficient on the payer; +5. repeat mutable Approval and Payment checks; +6. broadcast the exact payer-signed Approval; +7. confirm successful execution and independently observe the expected allowance; +8. immediately submit a matching `UnDelegateResource` transaction for every successful delegation; + and +9. submit and confirm the Permit2 payment settlement. + +Energy and Bandwidth are separate TRON resource types and therefore require separate delegation and +reclamation transactions when both are needed. The Facilitator MUST NOT broadcast the Approval until +the required resources are visible and sufficient, and MUST NOT deliberately rely on burning the +payer's TRX as a fallback. + +If allowance becomes sufficient before the Approval is broadcast, the Facilitator skips the +Approval. Any resource already delegated MUST still be reclaimed before normal Permit2 settlement +continues. + +After any successful delegation, Approval failure, expiration, timeout, caller disconnection, or +settlement failure MUST NOT strand resources on the payer. An unknown chain result MUST be reconciled +using the original transaction ID before the Facilitator retries or reclaims; it MUST NOT create a +replacement transaction while the original may still be included. + +Delegate, Approval, Undelegate, and settlement are separate TRON transactions. Atomic or same-block +execution is not guaranteed. The `transaction` in a successful core `SettleResponse` is the Permit2 +payment settlement transaction ID, not a delegation, Approval, or reclamation transaction ID. + +Undelegation returns the delegated stake share but does not immediately restore consumed resources. +Unrecovered usage remains subject to TRON's recovery window, so recovering capacity MUST NOT be +treated as clean available capacity until the Resource Owner's usable resources are observed again. + +## Security considerations + +### Account-level resource diversion + +Stake 2.0 delegates resources to an account, not to a transaction, token, method, or transaction ID. +After resources become visible and before the Approval consumes them, the payer can use them in +another transaction. This risk cannot be removed by stricter Approval validation. + +Implementations SHOULD obtain the final signed Approval before delegation, delegate only the minimum +required amount, allow only one active sponsorship per payer, broadcast immediately after resource +visibility, use short transaction expiration, and enforce payer and global loss limits. + +### Economic exhaustion + +A valid Approval does not guarantee that the later payment will succeed or reimburse sponsorship +cost. Before delegation, each request MUST be covered by a bounded sponsorship policy, such as funded +tenant credit or a capped platform subsidy. This extension does not charge a user fee inside +`approve`; standard TRC-20 `approve` changes allowance and cannot simultaneously transfer a fee. + +### Replay and recovery + +`(network, approvalTxID)` idempotency protects only Facilitators sharing the same sponsorship state. +Independent Facilitators can still sponsor the same signed Approval. Deployments SHOULD route one +sponsorship through one Facilitator domain and enforce bounded exposure. + +Implementations MUST persist enough operation and transaction identity to resume reconciliation and +resource reclamation after RPC errors, process restarts, or request disconnection. The persistence, +worker, locking, monitoring, and storage design is implementation-defined. + +## References + +- [Core x402 v2 specification](../x402-specification-v2.md) +- [`exact` on TRON](../schemes/exact/scheme_exact_tron.md) +- [TRON transaction model](https://developers.tron.network/docs/tron-protocol-transaction) +- [TRON confirmation semantics](https://developers.tron.network/docs/confirmation-semantics) +- [TRON signed transaction broadcast](https://developers.tron.network/reference/broadcasthex) +- [TRON `fee_limit`](https://developers.tron.network/docs/set-feelimit) +- [TRON resource model](https://developers.tron.network/docs/resource-model) +- [TRON resource delegation](https://developers.tron.network/docs/delegation) +- [TRON resource reclamation](https://developers.tron.network/docs/resource-reclamation-upon-undelegation) diff --git a/specs/scheme_impl_template.md b/specs/scheme_impl_template.md new file mode 100644 index 00000000..c15cf970 --- /dev/null +++ b/specs/scheme_impl_template.md @@ -0,0 +1,37 @@ +# Scheme: `` on `` + +## Summary + +Describe how the generic scheme is implemented on this network family. + +## Network and Contracts + +Document CAIP-2 identifiers, address representation, required contracts, and deployment discovery. + +## Payment Requirements + +Document scheme-specific fields in `PaymentRequirements.extra`. + +## Payment Payload + +Document the object carried in `PaymentPayload.payload`, including a complete v2 JSON example. + +## Typed Data or Authorization + +Document signature domain, primary type, field order, and address normalization. + +## Verification + +List all required validation steps in order. + +## Settlement + +Document the on-chain or relayer action and the resulting `SettleResponse`. + +## Error Codes + +List stable scheme-specific `invalidReason` and `errorReason` values. + +## Security Considerations + +Cover replay protection, amount and recipient binding, expiry, simulation, and external trust. diff --git a/specs/scheme_template.md b/specs/scheme_template.md new file mode 100644 index 00000000..e4496269 --- /dev/null +++ b/specs/scheme_template.md @@ -0,0 +1,22 @@ +# Scheme: `` + +## Summary + +Describe the transport-independent payment semantics and intended use cases. + +## Invariants + +List the amount, recipient, time, replay, and settlement properties every network binding MUST +enforce. + +## Lifecycle + +Describe payment construction, verification, settlement, retries, and any deferred operations. + +## Network Bindings + +Link every implemented `scheme__.md` document. + +## Security Considerations + +Describe the trust model and failure modes that apply across network bindings. diff --git a/specs/schemes/auth-capture/scheme_auth_capture.md b/specs/schemes/auth-capture/scheme_auth_capture.md new file mode 100644 index 00000000..c17662c4 --- /dev/null +++ b/specs/schemes/auth-capture/scheme_auth_capture.md @@ -0,0 +1,85 @@ +# Scheme: `auth-capture` + +## Summary + +`auth-capture` is a payment scheme where funds can be held and settled later. The client authorizes a maximum amount, and the facilitator submits it — either locking funds in escrow for later settlement (two-phase) or sending them directly to the receiver with refund capability (single-shot). + +The **captureAuthorizer** is the entity authorized to authorize, capture, void, refund, or charge a payment. In a facilitator-submits flow, that's either the facilitator itself or any smart contract that ends up calling the underlying escrow. + +Unlike `exact`, which has no built-in mechanism for returning funds, `auth-capture` supports returning funds to the client through void, refund, and reclaim. + +## Example Use Cases + +- Refundable payments with buyer protection +- Delayed delivery where the client needs recourse if the service is unsatisfactory +- Subscription or session billing with periodic captures against a single authorization + +## Settlement Paths + +The scheme supports two settlement paths, selected via `extra.autoCapture`: + +| `autoCapture` | Behavior | +| :---------------- | :--------------------------------------------------------------------------------------------------------------------------- | +| `false` (default) | Two-phase. Funds held in escrow. CaptureAuthorizer can capture, void, refund. Client can reclaim if capture deadline passes. | +| `true` | Single-shot. Funds sent directly to receiver. CaptureAuthorizer can refund post-settlement. | + +### Two-phase (`autoCapture: false`, default) + +``` +AUTHORIZE → RESOURCE DELIVERED → CAPTURE / VOID → (REFUND) +``` + +1. **Authorize**: Client authorization is submitted — funds locked in escrow. +2. **Resource delivered**: Server returns the resource (HTTP 200). +3. **Capture or void**: The captureAuthorizer can capture (finalize funds to the receiver) or void (release escrowed funds back to client). +4. **Reclaim**: If the capture deadline passes without action, the client can reclaim directly. +5. **Refund**: After capture, the captureAuthorizer can refund within the refund window. + +### Single-shot (`autoCapture: true`) + +``` +CHARGE → RESOURCE DELIVERED → (REFUND) +``` + +1. **Charge**: Client authorization is submitted — funds sent directly to receiver. +2. **Resource delivered**: Server returns the resource (HTTP 200). +3. **Refund**: The captureAuthorizer can refund within the refund window. + +No capture, void, or reclaim — funds are never held in escrow. + +## Core Properties + +### Fund Safety + +- Cannot overcharge — settlement amount is capped by the client-signed maximum. +- Two-phase path: client can reclaim escrowed funds after the capture deadline if no action is taken. +- Fee bounds are client-signed and enforced at settlement. + +### Replay Prevention + +- Each payment has a unique nonce derived from the payment parameters and a fresh client-generated salt. +- Nonce is consumed on-chain at settlement, preventing double-spend. + +### Expiry Enforcement + +Two absolute-timestamp deadlines govern the payment lifecycle (network-specific implementations may add a derived pre-approval expiry from `maxTimeoutSeconds`): + +- **Capture deadline** (`captureDeadline`): Last moment to capture escrowed funds (two-phase); after this, the client can reclaim. +- **Refund deadline** (`refundDeadline`): Last moment to issue a refund on captured or charged payments. + +## Relationship to `exact` + +| Aspect | `exact` | `auth-capture` | +| :--------- | :----------------- | :-------------------------------------------------------------------- | +| Settlement | Immediate transfer | Via escrow (two-phase) or direct with refund capability (single-shot) | +| Refundable | No | Yes (both paths) | +| Fee system | None | Configurable (min/max bounds, client-signed) | + +## Appendix + +Network-specific implementation details (contracts, signature formats, verification logic) are in per-network documents: `scheme_auth_capture_evm.md` (EVM). + +### References + +- [Escrow Scheme Proposal — Agentokratia (Issue #834)](https://github.com/coinbase/x402/issues/834) +- [Escrow Scheme Proposal — x402r (Issue #1011)](https://github.com/coinbase/x402/issues/1011) diff --git a/specs/schemes/auth-capture/scheme_auth_capture_evm.md b/specs/schemes/auth-capture/scheme_auth_capture_evm.md new file mode 100644 index 00000000..e92c036d --- /dev/null +++ b/specs/schemes/auth-capture/scheme_auth_capture_evm.md @@ -0,0 +1,297 @@ +# Scheme: `auth-capture` on `EVM` + +## Summary + +The `auth-capture` scheme on EVM uses the [base/commerce-payments](https://github.com/base/commerce-payments) contract stack: + +- **AuthCaptureEscrow**: Singleton — locks funds, enforces expiries, distributes on capture/refund. Universal canonical address (same address on every supported chain). +- **Token Collectors**: Universal canonical addresses, one per `assetTransferMethod`: + - `EIP3009_TOKEN_COLLECTOR_ADDRESS` — collects funds via `receiveWithAuthorization` signatures (USDC, EURC, etc.) + - `PERMIT2_TOKEN_COLLECTOR_ADDRESS` — collects funds via Uniswap Permit2 `permitTransferFrom` (any ERC-20) +- **`captureAuthorizer`**: Address authorized to authorize, capture, void, refund, or charge a payment. The escrow contract gates those operations on `msg.sender` matching this address. In x402's facilitator-submits flow that means either **the facilitator's EOA**, or **any smart contract** that ends up calling the escrow (e.g., an arbiter contract with dispute logic, a multisig, etc.). + +The client signs a single signature (ERC-3009 or Permit2). The facilitator calls `AuthCaptureEscrow.authorize()` (two-phase) or `AuthCaptureEscrow.charge()` (single-shot via `autoCapture: true`), either directly or through a smart contract set as the captureAuthorizer. + +## PaymentRequirements + +Servers accepting auth-capture payments advertise with scheme `auth-capture`: + +```json +{ + "x402Version": 2, + "accepts": [ + { + "scheme": "auth-capture", + "network": "eip155:8453", + "amount": "1000000", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "payTo": "0xReceiverAddress", + "maxTimeoutSeconds": 60, + "extra": { + "name": "USDC", + "version": "2", + "captureAuthorizer": "0xCaptureAuthorizerAddress", + "captureDeadline": 1740758554, + "refundDeadline": 1741276954, + "minFeeBps": 0, + "maxFeeBps": 1000, + "feeRecipient": "0xFeeRecipientAddress", + "autoCapture": false, + "assetTransferMethod": "eip3009" + } + } + ] +} +``` + +### `extra` Fields + +| Field | Required | Type | Description | +| :-------------------- | :------- | :----------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `name` | Yes | `string` | EIP-712 token-domain name (e.g., `"USDC"`). Used for ERC-3009 signing only. | +| `version` | Yes | `string` | EIP-712 token-domain version (e.g., `"2"`). | +| `captureAuthorizer` | Yes | `address` | Address authorized to authorize/capture/void/refund/charge. Committed on-chain as `PaymentInfo.operator`. | +| `captureDeadline` | Yes | `uint48` | Absolute Unix seconds — capture must occur before this. Encoded as `authorizationExpiry`. | +| `refundDeadline` | Yes | `uint48` | Absolute Unix seconds — refunds allowed until this. Encoded as `refundExpiry`. | +| `feeRecipient` | Yes | `address` | Fee recipient (committed on-chain as `PaymentInfo.feeReceiver`). Set to `address(0)` to let the captureAuthorizer specify any non-zero recipient at capture/charge time. | +| `minFeeBps` | Yes | `uint16` | Minimum fee in basis points (the fee floor the captureAuthorizer must take). `0` = no minimum. | +| `maxFeeBps` | Yes | `uint16` | Maximum fee in basis points (the cap on the captureAuthorizer's fee). | +| `autoCapture` | No | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). Default: `false`. | +| `assetTransferMethod` | No | `"eip3009" \| "permit2"` | Which token collector to use. Default: `"eip3009"`. A server MAY list multiple `accepts[]` entries with different `assetTransferMethod` values so clients can pick the method matching their token approvals. | + +### Spec → on-chain field name mapping + +The wire-format extra uses spec-level field names. The on-chain `PaymentInfo` struct keeps canonical Solidity names so the EIP-712 typehash matches the AuthCaptureEscrow contract byte-for-byte. + +| Wire (`extra`) | On-chain (`PaymentInfo`) | +| :----------------------------------- | :----------------------- | +| `captureAuthorizer` | `operator` | +| `captureDeadline` | `authorizationExpiry` | +| `refundDeadline` | `refundExpiry` | +| `feeRecipient` | `feeReceiver` | +| (derived: `now + maxTimeoutSeconds`) | `preApprovalExpiry` | + +## PaymentPayload + +The payload carries the signature and the client-generated `salt`. The facilitator reconstructs the full `PaymentInfo` from `extra` + `salt` + payer + top-level requirements (`payTo`, `asset`, `amount`). + +### EIP-3009 (default) + +```json +{ + "x402Version": 2, + "resource": { "url": "https://api.example.com/resource", "method": "GET" }, + "accepted": { "scheme": "auth-capture", "...": "..." }, + "payload": { + "authorization": { + "from": "0xPayerAddress", + "to": "0xEIP3009TokenCollectorAddress", + "value": "1000000", + "validAfter": "0", + "validBefore": "1740675754", + "nonce": "0xf374...3480" + }, + "signature": "0x2d6a...571c", + "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc" + } +} +``` + +**Field derivation (EIP-3009):** + +| Payload field | Derived from | +| :-------------------------- | :---------------------------------------------------------------------------------------------------------- | +| `authorization.from` | Client's own address | +| `authorization.to` | `EIP3009_TOKEN_COLLECTOR_ADDRESS` (universal constant) | +| `authorization.value` | `requirements.amount` | +| `authorization.validAfter` | `0` (the token collector hardcodes the lower bound) | +| `authorization.validBefore` | `now + requirements.maxTimeoutSeconds` (also used as `preApprovalExpiry` when reconstructing `PaymentInfo`) | +| `authorization.nonce` | Payer-agnostic `PaymentInfo` hash — see [Nonce Derivation](#nonce-derivation-both-methods) | +| `salt` | Fresh `bytes32` generated client-side per signing call | +| EIP-712 domain | `{ name, version }` from `extra`; `chainId` from `network`; `verifyingContract = requirements.asset` | + +### Permit2 + +```json +{ + "x402Version": 2, + "resource": { "url": "https://api.example.com/resource", "method": "GET" }, + "accepted": { "scheme": "auth-capture", "...": "..." }, + "payload": { + "permit2Authorization": { + "from": "0xPayerAddress", + "permitted": { + "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "1000000" + }, + "spender": "0xPermit2TokenCollectorAddress", + "nonce": "110210486920734568342928534950928740912034856789012345678901234567890123456789", + "deadline": "1740675754" + }, + "signature": "0x2d6a...571c", + "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc" + } +} +``` + +**Field derivation (Permit2):** + +| Payload field | Derived from | +| :-------------------------------------- | :---------------------------------------------------------------------------------------------------------- | +| `permit2Authorization.from` | Client's own address | +| `permit2Authorization.permitted.token` | `requirements.asset` | +| `permit2Authorization.permitted.amount` | `requirements.amount` | +| `permit2Authorization.spender` | `PERMIT2_TOKEN_COLLECTOR_ADDRESS` (universal constant) | +| `permit2Authorization.nonce` | `uint256(payerAgnosticPaymentInfoHash)` — see [Nonce Derivation](#nonce-derivation-both-methods) | +| `permit2Authorization.deadline` | `now + requirements.maxTimeoutSeconds` (also used as `preApprovalExpiry` when reconstructing `PaymentInfo`) | +| `salt` | Fresh `bytes32` generated client-side per signing call | +| EIP-712 domain | Canonical Permit2 contract; `chainId` from `network` | + +**No witness** — the merchant address is bound through the deterministic nonce, not a separate witness struct. + +### Nonce Derivation (both methods) + +The signature nonce is the payer-agnostic `PaymentInfo` hash. Payer is zeroed; everything else is the values that will appear on-chain. + +``` +paymentInfoHash = keccak256(abi.encode(PAYMENT_INFO_TYPEHASH, paymentInfoWithZeroPayer)) +nonce = keccak256(abi.encode(chainId, AUTH_CAPTURE_ESCROW_ADDRESS, paymentInfoHash)) +``` + +Freshness is enforced by `salt`: each signing call generates a fresh `bytes32` salt, so two payers signing concurrently produce distinct nonces with no collision risk. + +## Verification Logic + +The facilitator performs these checks in order: + +1. **Type guard**: Verify payload matches one of `Eip3009Payload` or `Permit2Payload` (must include `signature` and `salt`). +2. **Scheme match**: `requirements.scheme === "auth-capture"` and `payload.accepted.scheme === "auth-capture"`. +3. **Network match**: `payload.accepted.network === requirements.network` and format is `eip155:`. +4. **Extra validation**: `requirements.extra` contains all required fields (`captureAuthorizer`, `captureDeadline`, `refundDeadline`, `feeRecipient`, `minFeeBps`, `maxFeeBps`, `name`, `version`). +5. **Method routing**: `extra.assetTransferMethod` (default `"eip3009"`) matches the payload shape. +6. **Deadline ordering**: `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and `payload.validBefore` (EIP-3009) / `payload.deadline` (Permit2) `<= captureDeadline`. +7. **Time window**: `payload.deadline / validBefore > now + 6s` (not expired) and `validAfter <= now` (active, EIP-3009 only). +8. **Spender / collector match**: `payload.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `payload.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). +9. **Token match**: `payload.permitted.token === requirements.asset` (Permit2 only — EIP-3009 binds via signing domain). +10. **Signature verify**: Recover signer from EIP-712 (`ReceiveWithAuthorization` or `PermitTransferFrom`); must match `payer`. +11. **Amount**: `authorization.value` (EIP-3009) or `permit2Authorization.permitted.amount` (Permit2) matches `requirements.amount`. +12. **Nonce match**: Reconstruct `PaymentInfo` from extra + payload.salt + payer + requirements; recompute payer-agnostic hash; assert it matches the wire nonce. This transitively enforces equality on every field encoded in `PaymentInfo` (receiver, token, deadlines, fee bounds, feeRecipient), so individual field-by-field checks for those values are unnecessary. +13. **Simulate** `AUTH_CAPTURE_ESCROW.authorize(...)` or `.charge(...)` to ensure success. + +### EIP-6492 Support + +For smart wallet clients, the signature may be EIP-6492 wrapped (containing deployment bytecode). The facilitator extracts the inner ECDSA signature for verification. The on-chain `ERC6492SignatureHandler` in the token collector handles wallet deployment during settlement. + +## Settlement Logic + +1. **Re-verify** the payload (catches expired/invalid payloads before spending gas). +2. **Determine function**: `extra.autoCapture === true ? "charge" : "authorize"`. +3. **Resolve collector**: `EIP3009_TOKEN_COLLECTOR_ADDRESS` or `PERMIT2_TOKEN_COLLECTOR_ADDRESS` (per `assetTransferMethod`). +4. **Encode `collectorData`**: raw ERC-3009 signature, or ABI-encoded Permit2 signature. +5. **Call escrow**: `AUTH_CAPTURE_ESCROW.(paymentInfo, amount, tokenCollector, collectorData)`. +6. **Wait for receipt**: 60s timeout. +7. **Return result**: tx hash, network, payer. + +## Error Codes + +The auth-capture scheme uses the standard x402 error codes plus these scheme-specific codes: + +### Verification Errors + +| Error Code | Description | +| :---------------------------------- | :-------------------------------------------------------------------------------- | +| `invalid_payload_format` | Payload doesn't match `Eip3009Payload` or `Permit2Payload`. | +| `unsupported_scheme` | Scheme is not `auth-capture`. | +| `network_mismatch` | Payload network doesn't match requirements. | +| `invalid_network` | Network format is not `eip155:`. | +| `invalid_auth_capture_extra` | Extra is missing required fields. | +| `unsupported_asset_transfer_method` | `assetTransferMethod` is not `"eip3009"` or `"permit2"`. | +| `payload_method_mismatch` | Payload shape doesn't match `assetTransferMethod`. | +| `capture_deadline_expired` | `captureDeadline <= now + 6s`. | +| `invalid_deadline_ordering` | Deadlines violate `now + maxTimeoutSeconds <= captureDeadline <= refundDeadline`. | +| `authorization_expired` | EIP-3009 `validBefore` (or Permit2 `deadline`) `<= now + 6s`. | +| `authorization_not_yet_valid` | EIP-3009 `validAfter > now`. | +| `invalid_auth_capture_signature` | Signature verification failed. | +| `amount_mismatch` | Authorization value doesn't match `requirements.amount`. | +| `token_collector_mismatch` | `to` / `spender` doesn't match the canonical collector for the method. | +| `token_mismatch` | Permit2 `permitted.token` doesn't match `requirements.asset`. | +| `nonce_mismatch` | Wire nonce doesn't match the recomputed payer-agnostic PaymentInfo hash. | +| `insufficient_balance` | Payer balance is less than required amount. | +| `simulation_failed` | Settlement simulation reverted with an unmapped error. | + +### Typed simulation reverts + +If the simulate call reverts with an `AuthCaptureEscrow` custom error declared in the call's ABI, the facilitator decodes it via `BaseError.walk()` + `ContractFunctionRevertedError` and surfaces a stable reason instead of the opaque `simulation_failed` fallback: + +| Custom error | `invalidReason` | +| :------------------------------ | :------------------------------------ | +| `AfterPreApprovalExpiry` | `authorization_expired` | +| `InvalidExpiries` | `invalid_deadline_ordering` | +| `ExceedsMaxAmount` | `amount_mismatch` | +| `PaymentAlreadyCollected` | `payment_already_collected` | +| `TokenCollectionFailed` | `token_collection_failed` | +| `InvalidCollectorForOperation` | `invalid_collector` | +| `InvalidSender` | `invalid_capture_authorizer` | +| `ZeroAmount` / `AmountOverflow` | `amount_mismatch` / `amount_overflow` | +| `FeeBpsOverflow` | `invalid_fee_bps` | +| `InvalidFeeBpsRange` | `invalid_fee_bps_range` | +| `FeeBpsOutOfRange` | `fee_bps_out_of_range` | +| `ZeroFeeReceiver` | `zero_fee_receiver` | +| `InvalidFeeReceiver` | `invalid_fee_receiver` | +| `AfterAuthorizationExpiry` | `capture_deadline_expired` | +| `InsufficientAuthorization` | `insufficient_authorization` | +| `ZeroAuthorization` | `zero_authorization` | + +### Settlement Errors + +| Error Code | Description | +| :--------------------- | :------------------------------------------------ | +| `verification_failed` | Re-verification before settlement failed. | +| `transaction_reverted` | On-chain transaction reverted after confirmation. | + +## Appendix + +### PaymentInfo Struct (canonical Solidity — wire-level field names map per the table above) + +```solidity +struct PaymentInfo { + address operator; // = extra.captureAuthorizer + address payer; // payload-derived + address receiver; // = requirements.payTo + address token; // = requirements.asset + uint120 maxAmount; // = requirements.amount + uint48 preApprovalExpiry; // = now + maxTimeoutSeconds (client-derived) + uint48 authorizationExpiry; // = extra.captureDeadline + uint48 refundExpiry; // = extra.refundDeadline + uint16 minFeeBps; + uint16 maxFeeBps; + address feeReceiver; // = extra.feeRecipient + uint256 salt; // = payload.salt (client-generated, fresh per request) +} +``` + +### Expiry Ordering + +The contract enforces: `preApprovalExpiry <= authorizationExpiry <= refundExpiry`. + +| Expiry | Wire field | Enforced at | Effect | +| :-------------------- | :---------------- | :------------------------- | :---------------------------------- | +| `preApprovalExpiry` | derived | `authorize()` / `charge()` | Blocks settlement after this time | +| `authorizationExpiry` | `captureDeadline` | `capture()` | Blocks capture; enables `reclaim()` | +| `refundExpiry` | `refundDeadline` | `refund()` | Blocks refund requests | + +### Fee System + +Fees are enforced on-chain by the escrow contract: + +- `minFeeBps` and `maxFeeBps` set by the client in `PaymentInfo` (0–10,000 bps) +- `feeBps` at capture/charge must fall within `[minFeeBps, maxFeeBps]` +- If `feeReceiver` (`extra.feeRecipient`) is set in `PaymentInfo`, actual `feeReceiver` at capture/charge must match +- If `feeReceiver` is `address(0)`, the caller can specify any non-zero address +- Fee distribution: `feeAmount = amount * feeBps / 10000`, remainder goes to receiver + +### Canonical Addresses + +The `AUTH_CAPTURE_ESCROW_ADDRESS`, `EIP3009_TOKEN_COLLECTOR_ADDRESS`, and `PERMIT2_TOKEN_COLLECTOR_ADDRESS` constants resolve to the canonical [Base Commerce-Payments contracts](https://github.com/base/commerce-payments/releases/tag/v1.0.0). + +The `PERMIT2_ADDRESS` constant resolves to the canonical [Uniswap Permit2 contract](https://docs.uniswap.org/contracts/v4/deployments). diff --git a/specs/schemes/batch-settlement/scheme_batch_settlement.md b/specs/schemes/batch-settlement/scheme_batch_settlement.md new file mode 100644 index 00000000..e49608c1 --- /dev/null +++ b/specs/schemes/batch-settlement/scheme_batch_settlement.md @@ -0,0 +1,82 @@ +# Scheme: `batch-settlement` + +## Summary + +`batch-settlement` is a payment scheme in which the client provides a cryptographic payment commitment at request time, but the transfer of value is not executed synchronously during that request. The commitment is accepted, access is granted immediately, and financial settlement occurs later through a process defined by the network binding. + +Per-request onchain settlement may not be ideal for some real-world use cases. `batch-settlement` exists to serve situations where gas fees exceed the value of individual requests, block confirmation time is incompatible with HTTP response latency, request volume requires batched settlement, or settlement happens through infrastructure that operates asynchronously from HTTP (payment channels, fiat billing systems, stablecoin invoices). + +The model of how a commitment is formed, what backs it, and how it is eventually redeemed,is defined entirely by the network binding. + +The `batch-settlement` scheme supports **dynamic pricing**: the client commits up to the maximum per-request price (`PaymentRequirements.amount`), but the server may charge a lower actual price after executing the request. The actual charge is communicated via the `PAYMENT-RESPONSE`. + +## Protocol behavior + +For `exact` and `upto`, verification and settlement happen in a single pass: the commitment is validated, a transaction is broadcast, and value has moved. The settlement result contains an onchain transaction hash. + +For `batch-settlement`, verification confirms the commitment is valid, but settlement stores it rather than executing a transfer. The settlement result contains a commitment identifier, but value moves later, through the network binding's redemption process. + +### Commitment identifier + +The settlement result MUST include a non-empty commitment identifier on success. This identifier is meaningful to the network binding; a token, a voucher ID, channel receipt hash, account ledger reference, or equivalent. + +## Commitment models + +Network bindings may choose one of two trust models for backing the client's commitment. + +### Capital-backed + +The client's commitment is backed by onchain capital committed before or during the session such as pre-funded escrow, a payment channel, or a delegated authorization against a wallet balance. The trust anchor is the client's own funds. No network intermediary is required to underwrite access. + +### Credit-backed + +The client's commitment is backed by a verified identity associated with a billing account managed by a trusted network intermediary. No onchain capital is required from the client. The network authenticates the identity, underwrites the access obligation, and settles with the resource server through off-chain infrastructure on a defined schedule. + +## Use cases + +**Escrow-backed micropayments.** An AI agent pre-funds an onchain escrow at session start. Each sub-cent API call produces a signed voucher drawn against that balance. The provider accumulates vouchers and redeems them in a single onchain transaction at session end, keeping per-request gas cost to zero. + +**Payment channel streaming.** A client and provider open a payment channel once. Each request increments a signed running total (a receipt). The provider closes the channel periodically, collecting accumulated value in one settlement regardless of how many individual requests were made. + +**Delegated authorization.** A client delegates spending authority to an operator against their wallet balance. The operator signs commitments per request on the client's behalf. The provider collects authorizations and settles them through the delegation contract. + +**Credit-backed content licensing.** A content publisher monetizes AI crawler access. Crawlers authenticate via a network-registered identity backed by a billing account. The network verifies each request, accumulates usage, and invoices the crawler operator on a billing cycle with no wallet or onchain interaction required from the client. + +## Settlement lifecycle + +All `batch-settlement` network bindings share this abstract lifecycle. The network binding defines the specifics of each phase. + +1. **Commit.** The client produces a cryptographic payment commitment and attaches it to the request. The commitment is validated and stored. The resource is served immediately. + +2. **Accumulate.** The network retains the commitment in a voucher store, channel state, account ledger, or billing system. The network binding defines who stores commitments, where, and for how long. + +3. **Redeem.** Value is transferred out of band through an onchain contract call, a channel close, a fiat batch invoice, or any rail the network defines. The trigger, timing, and mechanism are network-defined. + +## Appendix + +### Network requirements + +Every `batch-settlement` network binding MUST specify: + +1. **Commitment format** — the structure and encoding of the payment payload, including all fields required for verification and redemption. +2. **Verification rules** — how the commitment is validated: signature scheme, balance or credit check, replay prevention, expiry. +3. **Storage behavior** — what constitutes a stored commitment for this network, and what the commitment identifier contains on success. +4. **Double-spend prevention** — how the network ensures the same commitment cannot be accepted or redeemed more than once. +5. **Commitment expiry** — when commitments become invalid and what happens to unaccepted commitments after expiry. +6. **Redemption** — who triggers redemption, when, and through what rail. +7. **Trust model** — whether the trust anchor is the client's onchain capital (capital-backed) or a network intermediary (credit-backed), and what guarantee the seller has of eventual settlement. + +### Extensions + +Network bindings may use optional extensions to communicate additional requirements in the `PaymentRequired` response. See the [extensions directory](../../extensions/) for available specifications. + +### Related schemes + +[`exact`](../exact/scheme_exact.md) — value is transferred immediately per request. + +[`upto`](../upto/scheme_upto.md) — value is transferred immediately, variable amount up to a client-authorized maximum. + +### Network bindings + +- EVM: [`scheme_batch_settlement_evm.md`](scheme_batch_settlement_evm.md) +- TRON: [`scheme_batch_settlement_tron.md`](scheme_batch_settlement_tron.md) diff --git a/specs/schemes/batch-settlement/scheme_batch_settlement_evm.md b/specs/schemes/batch-settlement/scheme_batch_settlement_evm.md new file mode 100644 index 00000000..95120807 --- /dev/null +++ b/specs/schemes/batch-settlement/scheme_batch_settlement_evm.md @@ -0,0 +1,684 @@ +# Scheme: `batch-settlement` on `EVM` + +## Summary + +The `batch-settlement` scheme on EVM is a **capital-backed** network binding using stateless unidirectional payment channels for high-throughput, low-cost payments. Clients deposit funds into onchain channels once and sign off-chain **cumulative vouchers** per request. Servers verify vouchers with fast signature checks and claim them onchain periodically in batches, reducing both latency and gas costs drastically. A single claim transaction can cover many channels at once and only updates onchain accounting; claimed funds are later transferred to the receiver via a separate settle operation that sweeps many claims into one token transfer. + +The scheme supports **dynamic pricing**: the client authorizes a maximum per-request, and the server charges the actual cost within that ceiling. + +--- + +## Channel Lifecycle + +### Channel creation and deposits + +A channel is created implicitly on the first deposit. The client deposits funds from the `payer` address into an onchain escrow via one of two asset transfer methods: `eip3009` for tokens that support `receiveWithAuthorization` (e.g. USDC) or `permit2` as a universal fallback for any ERC-20. Deposits are sponsored by the facilitator (gasless for the client). + +Channel identity is derived from an immutable config struct: +```solidity +struct ChannelConfig { + address payer; // Client wallet (EOA or smart wallet) + address payerAuthorizer; // EOA for voucher signing, or address(0) for EIP-1271 via payer + address receiver; // Server's payment destination (EOA or routing contract) + address receiverAuthorizer; // Authorizes claims and refunds via EIP-712 signatures + address token; // ERC-20 payment token + uint40 withdrawDelay; // Seconds before timed withdrawal completes (15 min – 30 days) + bytes32 salt; // Differentiates channels with identical parameters +} +``` +with `channelId = EIP712Hash(ChannelConfig)` under the `x402 Batch Settlement` domain. The hash binds the immutable config to the EVM `chainId` and deployed `x402BatchSettlement` address, so the same config produces different IDs across chains or deployments. + +### Requests and vouchers + +The channel tracks two values: `balance` (total deposited minus withdrawals and refunds) and `totalClaimed` (cumulative amount claimed by the server). Each voucher the client signs carries a cumulative ceiling (`maxClaimableAmount`). The server can claim up to that ceiling. Because vouchers are monotonically increasing, old vouchers with lower ceilings are naturally superseded. + +The server tracks a running total of actual charges per channel (`chargedCumulativeAmount`). For each subsequent request, the client sets the voucher's `maxClaimableAmount` to `chargedCumulativeAmount + amount`, where `amount` is the per-request maximum. + +### Claim and settle + +The server claims the latest voucher per channel onchain at its discretion. `claimWithSignature(claims, signature)` allows aggregating claims from multiple channels in one call. Claiming updates `totalClaimed` per channel; no token transfer occurs. + +`settle` sweeps all claimed-but-unsettled funds to the `receiver` in one transfer. + +### Refund and withdrawal + +**Cooperative refund**: the receiver side can return up to `balance - totalClaimed` to the payer via two paths: +- `refund(config, amount)`: direct call by `receiver` or `receiverAuthorizer`, no signature required. +- `refundWithSignature(config, amount, nonce, sig)`: relay-friendly; anyone submits an EIP-712 `Refund` signature from `receiverAuthorizer`. + +Both paths share the same internal execution: `refundNonce` is incremented **first** (before the amount cap is applied and before any token transfer), so a no-op refund (`amount > 0` but no unclaimed escrow available) still advances the nonce without emitting `Refunded` or moving tokens. A direct `refund` call therefore invalidates any pre-signed `refundWithSignature` digest for the previous nonce. If a timed withdrawal is pending, a cooperative refund **reduces** its recorded amount proportionally; it is only cancelled entirely when the refund amount meets or exceeds the pending withdrawal amount. + +**Timed withdrawal** (escape hatch): the `payer` or `payerAuthorizer` calls `initiateWithdraw(config, amount)` to start a grace period. The requested `amount` must not exceed `balance - totalClaimed` at initiation time; the call reverts otherwise. During the grace period the server can claim outstanding vouchers. After the withdrawal delay elapses, `finalizeWithdraw` (also callable by `payerAuthorizer`) completes the withdrawal, capping the transferred amount to whatever unclaimed escrow remains at that point. + +### Authorizer roles + +**Payer authorizer** (`payerAuthorizer`): if set to a non-zero address (an EOA), vouchers are verified via ECDSA recovery against that committed key ( fast, no RPC required). If set to zero, vouchers are verified against the payer address, supporting EIP-1271 smart wallets at the cost of an RPC call. + +**Receiver authorizer** (`receiverAuthorizer`): authorizes claim and refund operations via EIP-712 signatures. The server chooses this address: a server-owned EOA or smart contract (eg for key rotation), or a facilitator-provided address when the server delegates authorization. Must not be zero. Anyone can relay a `claimWithSignature` or `refundWithSignature` transaction with a valid authorization signature from the `receiverAuthorizer`. + +### Channel lifecycle events + +The contract emits `ChannelCreated(channelId, config)` on the first deposit into a channel (when `balance` transitions from zero with `totalClaimed == 0`). It emits `ChannelClosed(channelId, config)` when unclaimed escrow returns to zero with `totalClaimed == 0` — triggered by either a full cooperative refund or a timed withdrawal that drains all escrow. Indexers must handle `ChannelCreated` firing more than once on the same `channelId` if the channel is re-funded after being fully drained. + +### Channel reuse and parameter changes + +Channels are long-lived. After a refund, the client can top up and reuse the same channel. However, the channel config is immutable. If any parameter needs to change, a new channel is required. If delegating `receiverAuthorizer` to a facilitator, the server should claim all outstanding vouchers and refund remaining balances on old channels before switching to another facilitator. + +--- + +## 402 Response (PaymentRequirements) + +The 402 response contains pricing terms and the server's channel parameters. The client maps `payTo` → `ChannelConfig.receiver`, `extra.receiverAuthorizer` → `ChannelConfig.receiverAuthorizer`, `asset` → `ChannelConfig.token`, and `extra.withdrawDelay` → `ChannelConfig.withdrawDelay`, then fills in its own `payer`, `payerAuthorizer`, and `salt` to construct the full config. + +```json +{ + "scheme": "batch-settlement", + "network": "eip155:8453", + "amount": "100000", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "payTo": "0xServerReceiverAddress", + "maxTimeoutSeconds": 3600, + "extra": { + "receiverAuthorizer": "0xReceiverAuthorizerAddress", + "withdrawDelay": 900, + "name": "USDC", + "version": "2" + } +} +``` + +| Field | Type | Required | Description | +| --------------------------- | -------- | -------- | ------------------------------------------------------ | +| `extra.receiverAuthorizer` | `string` | yes | Address that will authorize claims/refunds | +| `extra.withdrawDelay` | `number` | yes | Withdrawal delay in seconds (15 min – 30 days) | +| `extra.assetTransferMethod` | `string` | optional | `"eip3009"` (default) or `"permit2"` | +| `extra.name` | `string` | yes | EIP-712 domain name of the token contract | +| `extra.version` | `string` | yes | EIP-712 domain version of the token contract | +| `extra.channelState` | `object` | optional | Corrective-only server channel snapshot for cumulative amount resynchronization | +| `extra.voucherState` | `object` | optional | Corrective-only signed voucher proof for cumulative amount resynchronization | + +--- + +## Client: Payment Construction + +The client constructs a payment payload whose type depends on channel state: + +- `deposit`: No channel exists or balance is exhausted — client signs a token authorization and voucher +- `voucher`: Channel has sufficient balance — client signs a new cumulative voucher +- `refund`: Client requests a cooperative refund — client signs a zero-charge voucher and optionally includes a refund amount + +### Deposit Payload + +The `deposit.authorization` field contains the token transfer authorization — exactly one of `erc3009Authorization` or `permit2Authorization` must be present. + +```json +{ + "x402Version": 2, + "accepted": { + "scheme": "batch-settlement", + "network": "eip155:8453", + "amount": "1000", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "payTo": "0xServerReceiverAddress", + "maxTimeoutSeconds": 3600, + "extra": { + "receiverAuthorizer": "0xReceiverAuthorizerAddress", + "withdrawDelay": 900, + "name": "USDC", + "version": "2" + } + }, + "payload": { + "type": "deposit", + "channelConfig": { + "payer": "0xClientAddress", + "payerAuthorizer": "0xClientPayerAuthorizerEOA", + "receiver": "0xServerReceiverAddress", + "receiverAuthorizer": "0xReceiverAuthorizerAddress", + "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "withdrawDelay": 900, + "salt": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "voucher": { + "channelId": "0xabc123...channelId", + "maxClaimableAmount": "1000", + "signature": "0x...EIP-712 voucher signature" + }, + "deposit": { + "amount": "100000", + "authorization": { + "erc3009Authorization": { + "validAfter": "0", + "validBefore": "1770000000", + "salt": "0x...authorization salt", + "signature": "0x...ERC-3009 signature" + } + } + } + } +} +``` + +### Voucher Payload + +```json +{ + "x402Version": 2, + "accepted": { "..." : "..." }, + "payload": { + "type": "voucher", + "channelConfig": { + "payer": "0xClientAddress", + "payerAuthorizer": "0xClientPayerAuthorizerEOA", + "receiver": "0xServerReceiverAddress", + "receiverAuthorizer": "0xReceiverAuthorizerAddress", + "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "withdrawDelay": 900, + "salt": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "voucher": { + "channelId": "0xabc123...channelId", + "maxClaimableAmount": "5000", + "signature": "0x...EIP-712 voucher signature" + } + } +} +``` + +### Refund Payload + +The optional `amount` requests a partial refund; omit it for a full refund. The voucher is zero-charge: `voucher.maxClaimableAmount` MUST equal the channel's current `chargedCumulativeAmount`. Before settlement, the server completes the payload with the refund nonce, claim data, and any receiver-authorizer signatures it is responsible for. + +```json +{ + "x402Version": 2, + "accepted": { "..." : "..." }, + "payload": { + "type": "refund", + "channelConfig": { + "payer": "0xClientAddress", + "payerAuthorizer": "0xClientPayerAuthorizerEOA", + "receiver": "0xServerReceiverAddress", + "receiverAuthorizer": "0xReceiverAuthorizerAddress", + "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "withdrawDelay": 900, + "salt": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "voucher": { + "channelId": "0xabc123...channelId", + "maxClaimableAmount": "3200", + "signature": "0x...EIP-712 zero-charge voucher signature" + }, + "amount": "1500" + } +} +``` + +--- + +## Server: State & Forwarding + +The server is the sole owner of per-channel state. + +### Per-Channel State + +The server must maintain per-channel state, keyed by channel ID: + +| State Field | Type | Description | +| ------------------------- | --------------- | ------------------------------------------------------------------------------------------ | +| `channelConfig` | `ChannelConfig` | Full channel configuration object | +| `chargedCumulativeAmount` | `uint128` | Actual accumulated cost for this channel | +| `signedMaxClaimable` | `uint128` | `maxClaimableAmount` from the latest client-signed voucher | +| `signature` | `bytes` | Client's voucher signature for the latest `signedMaxClaimable` | +| `balance` | `uint128` | Current channel balance (mirrored from onchain) | +| `totalClaimed` | `uint128` | Total claimed onchain (mirrored from onchain) | +| `withdrawRequestedAt` | `uint64` | Unix timestamp when timed withdrawal was initiated, or 0 if none (mirrored from onchain) | +| `refundNonce` | `uint256` | Next nonce required for `refundWithSignature` (mirrored from onchain) | +| `onchainSyncedAt` | `uint64` | Local timestamp when mirrored onchain fields were refreshed | +| `lastRequestTimestamp` | `uint64` | Timestamp of the last paid request | + +### Request Processing + +The server must serialize request processing per channel and must not update voucher state until the resource handler has succeeded. + +1. **Verify**: + - For `voucher` and `deposit` payloads, check that `payload.voucher.maxClaimableAmount == chargedCumulativeAmount + paymentRequirements.amount`. If this fails, reject with `invalid_batch_settlement_evm_cumulative_amount_mismatch` and return a corrective 402. + - For refund payloads, check that `payload.voucher.maxClaimableAmount == chargedCumulativeAmount` and skip the resource handler after facilitator verification. + - Always call facilitator `/verify` for `deposit` and `refund` payloads, as well as `voucher` payloads with EIP-1271 vouchers. + - A plain EOA-authorized `voucher` may be verified locally when the server's mirrored onchain state is fresh. +2. **Execute**: Run the resource handler +3. **On success** — commit state: + - `chargedCumulativeAmount += actualPrice` (where `actualPrice <= PaymentRequirements.amount`) + - Mirror `balance`, `totalClaimed`, `withdrawRequestedAt`, and `refundNonce` from the facilitator response +4. **On failure**: State unchanged, client can retry the same voucher. + +### Payment Response Contract + +Successful paid responses distinguish onchain transfers from offchain charges: + +- Voucher-only response: `transaction` is `""`, top-level `amount` is `""`, `extra.chargedAmount` is the request charge, and `extra.channelState` carries the channel snapshot. +- Deposit response: `transaction` is the deposit transaction hash, top-level `amount` is the deposited amount, `extra.chargedAmount` is the request charge, and `extra.channelState` carries the channel snapshot. +- Refund response: `transaction` is the refund transaction hash, top-level `amount` is the refunded amount, `extra.channelState` carries the post-refund channel snapshot and `extra.chargedAmount` is omitted. + +```json +{ + "success": true, + "transaction": "", + "network": "eip155:8453", + "payer": "0xClientAddress", + "amount": "", + "extra": { + "chargedAmount": "700", + "channelState": { + "channelId": "0xabc123...channelId", + "balance": "100000", + "totalClaimed": "3200", + "withdrawRequestedAt": 0, + "refundNonce": "1", + "chargedCumulativeAmount": "3900" + } + } +} +``` + +### Cooperative refund flow + +When the server receives a `type: "refund"` payload: + +1. **Verify (zero-charge)**: enforce `payload.voucher.maxClaimableAmount == chargedCumulativeAmount` (no increment from `paymentRequirements.amount`). If local state is stale, emit a corrective 402 so the client can recover and retry. +2. **Bypass the protected resource.** Refund payloads are payment operations, not paid requests; the application route is not invoked. +3. **Complete the settlement payload**: resolve omitted `amount` to a full refund, validate any partial `amount`, add `refundNonce`, build `claims`, and add receiver-authorizer signatures when the server owns that key. +4. **Submit onchain**: `claimWithSignature(claims, claimSig)` (no-op when `maxClaimableAmount == totalClaimed`) followed by `refundWithSignature(config, amount, nonce, refundSig)`. The contract increments `refundNonce` before applying the amount cap; even if no tokens move (zero available escrow), the nonce advances. +5. **Update channel state**: + - **Full refund** (refunded amount equals the remainder): delete the channel record. + - **Partial refund**: keep the channel record, mirror the returned `balance`, `totalClaimed`, `withdrawRequestedAt`, and `refundNonce`. If a timed withdrawal was pending, its recorded amount is reduced proportionally (or cancelled if the refund covers it entirely). +6. Return the settle response in the standard `PAYMENT-RESPONSE` header. + +After the server completes the refund payload, the facilitator receives: + +```json +{ + "type": "refund", + "channelConfig": { "..." : "..." }, + "voucher": { + "channelId": "0xabc123...channelId", + "maxClaimableAmount": "3200", + "signature": "0x...EIP-712 zero-charge voucher signature" + }, + "amount": "1500", + "refundNonce": "1", + "claims": [ + { + "voucher": { + "channel": { "..." : "..." }, + "maxClaimableAmount": "3200" + }, + "signature": "0x...EIP-712 zero-charge voucher signature", + "totalClaimed": "3200" + } + ], + "refundAuthorizerSignature": "0x...refund authorization", + "claimAuthorizerSignature": "0x...claim authorization" +} +``` + +`refundAuthorizerSignature` and `claimAuthorizerSignature` are included when the server owns the receiver-authorizer key. If the channel delegates receiver authorization to the facilitator, the server omits them and the facilitator signs before submitting the transaction. + +--- + +## Facilitator Interface + +Uses the standard x402 facilitator interface (`/verify`, `/settle`, `/supported`). + +### POST /verify + +Verifies a deposit, voucher, or refund payment payload. Returns the onchain channel snapshot: + +```json +{ + "isValid": true, + "payer": "0xPayerAddress", + "extra": { + "channelId": "0xabc123...", + "balance": "1000000", + "totalClaimed": "500000", + "withdrawRequestedAt": 0, + "refundNonce": "0" + } +} +``` + +### POST /settle + +| `payload.type` | When Used | Onchain Effect | +| -------------- | ----------------------------- | ----------------------------------------------------- | +| `"deposit"` | First request or top-up | Deposit via the canonical ERC-3009 or Permit2 collector | +| `"claim"` | Server batches voucher claims | Validate vouchers, update accounting (no transfer) | +| `"settle"` | Server transfers earned funds | Transfer unsettled amount to receiver | +| `"refund"` | Cooperative refund | Return specified amount to payer, increment refund nonce | + +Server-authored claim and settle payloads use the same `type` discriminator: + +```json +{ + "type": "claim", + "claims": [ + { + "voucher": { + "channel": { "..." : "..." }, + "maxClaimableAmount": "5000" + }, + "signature": "0x...voucher signature", + "totalClaimed": "5000" + } + ], + "claimAuthorizerSignature": "0x...claim authorization" +} +``` + +`claimAuthorizerSignature` is included when the server owns the receiver-authorizer key. If receiver authorization is delegated to the facilitator, the server omits it and the facilitator signs before submitting the transaction. + +```json +{ + "type": "settle", + "receiver": "0xServerReceiverAddress", + "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" +} +``` + +Example facilitator response for a claim: + +```json +{ + "success": true, + "transaction": "0x...transactionHash", + "network": "eip155:8453", + "amount": "" +} +``` + +`amount` is empty because claim only updates accounting; no funds move. + +Example facilitator response for a settle: + +```json +{ + "success": true, + "transaction": "0x...transactionHash", + "network": "eip155:8453", + "amount": "5000" +} +``` + +`amount` is the amount transferred to the receiver; if settlement is a no-op, it is `"0"`. + +Example facilitator response for a deposit: + +```json +{ + "success": true, + "transaction": "0x...transactionHash", + "network": "eip155:8453", + "payer": "0xPayerAddress", + "amount": "100000", + "asset": "0xAssetAddress", + "extra": { + "channelState": { + "channelId": "0xabc123...", + "balance": "100000", + "totalClaimed": "3200", + "withdrawRequestedAt": 0, + "refundNonce": "1" + } + } +} +``` + +Example facilitator response for a refund: + +```json +{ + "success": true, + "transaction": "0x...transactionHash", + "network": "eip155:8453", + "payer": "0xPayerAddress", + "amount": "1500", + "extra": { + "channelState": { + "channelId": "0xabc123...", + "balance": "98500", + "totalClaimed": "3200", + "withdrawRequestedAt": 0, + "refundNonce": "2" + } + } +} +``` + +`amount` is the amount returned to the payer. + +### GET /supported + +The facilitator MAY declare a receiver authorizer whose role is to produce EIP-712 signatures for claims and refunds. The server may delegate to this address as its channel's `receiverAuthorizer`, or supply its own. Any address in `signers` may relay the resulting transactions. + +```json +{ + "kinds": [ + { + "x402Version": 2, + "scheme": "batch-settlement", + "network": "eip155:8453", + "extra": { + "receiverAuthorizer": "0xReceiverAuthorizerAddress" + } + } + ], + "extensions": [], + "signers": { + "eip155:*": [ + "0xSignerAddress1", + "0xSignerAddress2" + ] + } +} +``` + +### Verification Rules + +A facilitator must enforce: + +1. **Channel config consistency** (deposit, voucher, and refund): the config's chain-bound EIP-712 hash must equal the claimed channel ID. +2. **Token match**: the channel token must match the payment requirements asset. +3. **Receiver match**: the channel receiver must equal the payment requirements `payTo`. +4. **Receiver authorizer match**: the channel receiver authorizer must equal `extra.receiverAuthorizer`. +5. **Withdraw delay match**: the channel withdraw delay must equal `extra.withdrawDelay`. +6. **Signature validity**: recover the signer from the EIP-712 voucher digest. If the payer authorizer is set, the signer must match it (ECDSA only). If the payer authorizer is zero, validate via `SignatureChecker` against the payer. +7. **Channel existence**: the channel must have a positive balance. +8. **Balance check** (deposit only): the client must have sufficient token balance. +9. **Deposit sufficiency**: `maxClaimableAmount` must be at most `balance` (or `balance + depositAmount` for deposit payloads). +10. **Not below claimed**: `maxClaimableAmount` must exceed onchain `totalClaimed`. For refund payloads (`payload.type == "refund"`), this rule is relaxed to `maxClaimableAmount >= totalClaimed`, since refund vouchers are zero-charge and may match the already-claimed total exactly. +11. **Signed refunds**: the refund nonce must equal the onchain `refundNonce` at the time of submission; the EIP-712 `Refund` digest (`Refund(bytes32 channelId,uint256 nonce,uint128 amount)`) must bind the same `amount` submitted in the transaction. The contract increments the nonce before computing the capped transfer amount, so the nonce advances even when no tokens move. + +The facilitator must return the channel snapshot (`balance`, `totalClaimed`, `withdrawRequestedAt`, `refundNonce`) in every `/verify` response `extra` field and in every `/settle` response under `extra.channelState`. If `withdrawRequestedAt` is non-zero, the server should claim outstanding vouchers promptly before the withdraw delay elapses. + +--- + +## Claim & Settlement Strategy + +`claim(voucherClaims)` validates payer voucher signatures and updates accounting for multiple channels; `msg.sender` must be `receiver` or `receiverAuthorizer` for every row. `claimWithSignature(claims, signature)` is the relay-friendly variant: anyone can submit it with a valid EIP-712 `ClaimBatch` signature from `receiverAuthorizer` covering all rows (all rows must share the same `receiverAuthorizer`). No token transfer occurs in either path. + +`settle(receiver, token)` transfers all claimed-but-unsettled funds for a receiver+token pair to the receiver in one transfer. Permissionless. + +| Strategy | Description | Trade-off | +| ----------------- | ---------------------------------------------- | -------------------------------- | +| **Periodic** | Claim + settle every N minutes | Predictable gas costs | +| **Threshold** | Claim + settle when unclaimed amount exceeds T | Bounds server's risk exposure | +| **On withdrawal** | Claim + settle when withdrawal is initiated | Minimum gas, maximum risk window | + +The server must claim all outstanding vouchers before the withdraw delay elapses. Unclaimed vouchers become unclaimable after `finalizeWithdraw()` reduces the channel balance. + +--- + +## Client Verification Rules + +### Steady State + +Before signing the next voucher, the client must verify from the payment response: + +1. `extra.chargedAmount <= PaymentRequirements.amount` +2. `extra.channelState.chargedCumulativeAmount == previous + extra.chargedAmount` +3. `extra.channelState.balance` is consistent with the client's expectation +4. `extra.channelState.channelId` matches + +If any check fails, the client must not sign further vouchers and should initiate withdrawal. + +### Recovery After State Loss + +Channel identity is deterministic. The client can recompute `channelId` from the 402 response plus its own channel parameters (`payer`, `payerAuthorizer`, `salt`), then read `channels(channelId)` to recover the onchain `balance` and `totalClaimed`. + +The recovery baseline is: + +- Use onchain `totalClaimed` when no trusted offchain state is available. +- Use server-provided `chargedCumulativeAmount` only when the server also returns the last signed voucher (`signedMaxClaimable` and `signature`) and the client verifies that signature against its own voucher signer. + +**Client cold start.** When the client has no local channel record, it reads onchain state and sets `chargedCumulativeAmount = totalClaimed`. If the next request would exceed the recovered `balance`, the client sends a deposit/top-up payload. Otherwise it signs a voucher for `totalClaimed + amount`. + +**Server state loss.** If the server has no local channel record, it sets `chargedCumulativeAmount = totalClaimed` as the baseline. If the server lost unclaimed vouchers, those unclaimed charges are forfeited by the server. + +**Corrective 402.** If the server has local channel state and rejects a paid payload (`deposit` or `voucher`) because the client's cumulative amount does not match the server's channel state, it returns `invalid_batch_settlement_evm_cumulative_amount_mismatch` with `accepts[].extra.channelState` containing the channel snapshot and `accepts[].extra.voucherState` containing `signedMaxClaimable` and `signature`. The client verifies the voucher signature before adopting `chargedCumulativeAmount` and retrying. + +```json +{ + "x402Version": 2, + "error": "invalid_batch_settlement_evm_cumulative_amount_mismatch", + "accepts": [ + { + "scheme": "batch-settlement", + "extra": { + "receiverAuthorizer": "0xReceiverAuthorizerAddress", + "withdrawDelay": 900, + "name": "USDC", + "version": "2", + "channelState": { + "channelId": "0xabc123...channelId", + "balance": "100000", + "totalClaimed": "500", + "withdrawRequestedAt": 0, + "refundNonce": "1", + "chargedCumulativeAmount": "3200" + }, + "voucherState": { + "signedMaxClaimable": "3200", + "signature": "0x...last voucher signature" + } + } + } + ] +} +``` + +--- + +## Error Codes + +| Error Code | Description | +| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------- | +| `invalid_batch_settlement_evm_authorizer_address_mismatch` | Authorizer address does not match the expected receiver authorizer | +| `invalid_batch_settlement_evm_channel_busy` | Another request holds the per-channel lock; client should retry shortly | +| `invalid_batch_settlement_evm_channel_id_mismatch` | Channel config does not hash to the claimed channel ID | +| `invalid_batch_settlement_evm_channel_not_found` | No channel with positive balance for the given channel ID | +| `invalid_batch_settlement_evm_channel_state_read_failed` | Facilitator failed to read onchain channel state | +| `invalid_batch_settlement_evm_charge_exceeds_signed_cumulative` | Committing the charge would exceed the voucher's signed `maxClaimableAmount` | +| `invalid_batch_settlement_evm_claim_payload` | Claim payload is malformed | +| `invalid_batch_settlement_evm_claim_simulation_failed` | Claim simulation failed | +| `invalid_batch_settlement_evm_claim_transaction_failed` | Onchain claim transaction failed | +| `invalid_batch_settlement_evm_cumulative_amount_mismatch` | Corrective 402: client's cumulative voucher ceiling does not match the server's tracked `chargedCumulativeAmount` | +| `invalid_batch_settlement_evm_cumulative_below_claimed` | Voucher `maxClaimableAmount` violates monotonicity vs onchain `totalClaimed` (non-refund: must be greater than `totalClaimed`; refund: must not be strictly below `totalClaimed`; deposit verify: must not be strictly below `totalClaimed`) | +| `invalid_batch_settlement_evm_cumulative_exceeds_balance` | Voucher `maxClaimableAmount` exceeds effective onchain balance | +| `invalid_batch_settlement_evm_deposit_payload` | Deposit payload is malformed | +| `invalid_batch_settlement_evm_deposit_simulation_failed` | Deposit simulation failed | +| `invalid_batch_settlement_evm_deposit_transaction_failed` | Onchain deposit transaction failed | +| `invalid_batch_settlement_evm_eip2612_amount_mismatch` | EIP-2612 permit amount does not match the requested authorization | +| `invalid_batch_settlement_evm_eip2612_asset_mismatch` | EIP-2612 permit asset does not match the payment asset | +| `invalid_batch_settlement_evm_eip2612_deadline_expired` | EIP-2612 permit deadline has expired | +| `invalid_batch_settlement_evm_eip2612_invalid_format` | EIP-2612 permit segment is malformed | +| `invalid_batch_settlement_evm_eip2612_invalid_signature` | EIP-2612 permit signature is invalid | +| `invalid_batch_settlement_evm_eip2612_owner_mismatch` | EIP-2612 permit owner does not match the payer | +| `invalid_batch_settlement_evm_eip2612_spender_mismatch` | EIP-2612 permit spender does not match the expected spender | +| `invalid_batch_settlement_evm_erc20_approval_asset_mismatch` | ERC-20 approval asset does not match the payment asset | +| `invalid_batch_settlement_evm_erc20_approval_broadcast_failed` | Facilitator failed to broadcast the pre-signed ERC-20 approval transaction | +| `invalid_batch_settlement_evm_erc20_approval_from_mismatch` | ERC-20 approval signer does not match the payer | +| `invalid_batch_settlement_evm_erc20_approval_invalid_format` | ERC-20 approval segment is malformed | +| `invalid_batch_settlement_evm_erc20_approval_unavailable` | ERC-20 approval gas sponsorship is unavailable | +| `invalid_batch_settlement_evm_erc20_approval_wrong_spender` | ERC-20 approval spender is not Permit2 | +| `invalid_batch_settlement_evm_erc3009_authorization_required` | Deposit payload is missing the required `erc3009Authorization` | +| `invalid_batch_settlement_evm_insufficient_balance` | Client token balance is insufficient for the deposit | +| `invalid_batch_settlement_evm_missing_channel` | Resource server has no channel session for the payload's channel ID | +| `invalid_batch_settlement_evm_missing_eip712_domain` | Token EIP-712 domain (`name`, `version`) is missing from payment requirements | +| `invalid_batch_settlement_evm_network_mismatch` | Payment payload `accepted.network` does not match `paymentRequirements.network` on the verify request | +| `invalid_batch_settlement_evm_payload_authorization_valid_after` | ERC-3009 authorization `validAfter` is still in the future | +| `invalid_batch_settlement_evm_payload_authorization_valid_before` | ERC-3009 authorization `validBefore` has already passed | +| `invalid_batch_settlement_evm_payload_type` | Payload `type` is not valid for the current verify/settle operation | +| `invalid_batch_settlement_evm_permit2_allowance_required` | Permit2 allowance is required before deposit | +| `invalid_batch_settlement_evm_permit2_amount_mismatch` | Permit2 authorization amount does not match the requested deposit amount | +| `invalid_batch_settlement_evm_permit2_authorization_required` | Deposit payload is missing the required Permit2 authorization | +| `invalid_batch_settlement_evm_permit2_deadline_expired` | Permit2 authorization deadline has expired | +| `invalid_batch_settlement_evm_permit2_invalid_signature` | Permit2 authorization signature is invalid | +| `invalid_batch_settlement_evm_permit2_invalid_spender` | Permit2 authorization spender is not the expected spender | +| `invalid_batch_settlement_evm_receive_authorization_signature` | ERC-3009 `receiveWithAuthorization` signature is invalid | +| `invalid_batch_settlement_evm_receiver_authorizer_mismatch` | Channel receiver authorizer does not match `extra.receiverAuthorizer` | +| `invalid_batch_settlement_evm_receiver_mismatch` | Channel receiver does not match `payTo` | | +| `invalid_batch_settlement_evm_refund_amount_invalid` | Refund `amount` is non-numeric or non-positive | +| `invalid_batch_settlement_evm_refund_no_balance` | Cooperative refund requested but no refundable balance remains | +| `invalid_batch_settlement_evm_refund_payload` | Refund payload is malformed | +| `invalid_batch_settlement_evm_refund_simulation_failed` | Refund simulation failed | +| `invalid_batch_settlement_evm_refund_transaction_failed` | Onchain refund transaction failed | +| `invalid_batch_settlement_evm_rpc_read_failed` | Facilitator failed to read required onchain data | +| `invalid_batch_settlement_evm_scheme` | `scheme` is not `batch-settlement` | +| `invalid_batch_settlement_evm_settle_payload` | Settle payload is malformed | +| `invalid_batch_settlement_evm_nothing_to_settle` | Receiver/token pair has no claimed-but-unsettled funds | +| `invalid_batch_settlement_evm_settle_simulation_failed` | Settle simulation failed | +| `invalid_batch_settlement_evm_settle_transaction_failed` | Onchain settle transaction failed | +| `invalid_batch_settlement_evm_token_mismatch` | Channel token does not match the payment requirements asset | +| `invalid_batch_settlement_evm_transaction_reverted` | Submitted transaction reverted | +| `invalid_batch_settlement_evm_unknown_settle_action` | Settle payload requested an unknown action | +| `invalid_batch_settlement_evm_voucher_payload` | Voucher payload is malformed | +| `invalid_batch_settlement_evm_voucher_signature` | EIP-712 voucher signature does not recover to the expected signer | +| `invalid_batch_settlement_evm_wait_for_receipt_failed` | Facilitator failed while waiting for the transaction receipt | +| `invalid_batch_settlement_evm_withdraw_delay_mismatch` | Channel withdraw delay does not match `extra.withdrawDelay` | +| `invalid_batch_settlement_evm_withdraw_delay_out_of_range` | Withdraw delay is outside the 15 min - 30 day bounds | + +--- + +## Security and Trust + +1. **Capital risk and cumulative replay protection**: Clients bear risk up to the signed `maxClaimableAmount`; the receiver authorizer determines actual `totalClaimed` onchain within that bound. Over-claiming is a trust violation, not a protocol violation. The cumulative model makes nonces unnecessary. As `totalClaimed` only increases, and old vouchers are naturally superseded. + +2. **Withdrawal delay as escape hatch**: The 15 min – 30 day bounds prevent a server from indefinitely trapping client funds while giving the server a fair window to claim outstanding vouchers. Cooperative refund returns unclaimed balance immediately when the server cooperates; timed withdrawal is the unilateral fallback. Servers bear the risk of vouchers left unclaimed when `finalizeWithdraw` completes. + +3. **Cross-function replay prevention**: `Voucher`, `Refund`, and `ClaimBatch` use distinct EIP-712 type hashes so a signature for one cannot be replayed as another. Refunds additionally carry a per-channel nonce. + +4. **Voucher expiry via escrow depletion**: Vouchers carry no expiry field. A voucher remains claimable as long as `balance - totalClaimed > 0`; `finalizeWithdraw` and `refundWithSignature` close the claim window by draining available escrow. The ERC-3009 `validBefore`/`validAfter` fields bound only the deposit authorization, not the voucher. + +5. **Refund authorization when the facilitator is `receiverAuthorizer`**: A cooperative refund bypasses the timed-withdrawal delay, so it must carry receiver-side consent. When a server supplies its own `refundAuthorizerSignature` that signature is the consent. When the server delegates `receiverAuthorizer` to the facilitator, the faciltator MUST authenticate that each `refund` request originates from the service that created the channel (e.g. SIWX, JWT, or API credential bound at channel-creation time) and reject all others. A facilitator with no such authentication mechanism MUST NOT advertise a `receiverAuthorizer` in `/supported`. + +--- + +## Reference Implementation: `x402BatchSettlement` + +The `batch-settlement` scheme is implemented by the `x402BatchSettlement` contract alongside the `ERC3009DepositCollector` and `Permit2DepositCollector` deposit collector contracts. Each contract is deployed to a deterministic address across all supported EVM chains via CREATE2. + +| Contract | Canonical Address | +| -------- | ------- | +| `x402BatchSettlement` | `0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003` | +| `ERC3009DepositCollector` | `0x4020806089470a89826cB9fB1f4059150b550004` | +| `Permit2DepositCollector` | `0x4020425FAf3B746C082C2f942b4E5159887B0005` | + +The `x402BatchSettlement` contract uses `ReentrancyGuardTransient` (EIP-1153 transient storage) and must only be deployed on chains where that opcode is supported. +--- + +## Version History + +| Version | Date | Changes | Authors | +| ------- | ---------- | ------------- | ----------------------- | +| v1.0 | 2025-04-28 | Initial draft | @phdargen @CarsonRoscoen @ilikesymmetry | diff --git a/specs/schemes/batch-settlement/scheme_batch_settlement_tron.md b/specs/schemes/batch-settlement/scheme_batch_settlement_tron.md new file mode 100644 index 00000000..13f8526f --- /dev/null +++ b/specs/schemes/batch-settlement/scheme_batch_settlement_tron.md @@ -0,0 +1,153 @@ +# Scheme: `batch-settlement` on `TRON` + +## Summary + +The TRON binding implements capital-backed, unidirectional payment channels. The payer deposits +TRC-20 funds once, signs cumulative TIP-712 vouchers for requests, and the receiver later claims and +settles many vouchers on-chain. A cooperative refund returns unused funds. + +The wire structures match the EVM binding where possible. TRON Base58Check addresses are accepted at +the protocol boundary and normalized to 20-byte hex for hashing, typed data, and contract calls. + +## Networks and Contracts + +| Network | Channel contract | ERC-3009 collector | Permit2 collector | +| --- | --- | --- | --- | +| `tron:0x2b6653dc` | `TW9yNhTySkEHYfjnGQU2u4NAsdb1tW4fbm` | `TTWA7aWMdx4jfcbp8XRAS2JAd2sUhyF9qj` | `TAg5qqp1K9x5KeSTWnRa8LT79B5HUjzSHY` | +| `tron:0xcd8690dc` | `TWBwWHZWwH8TzrZnbxit1J645VGYY1K2fA` | `TJUQ3BQt4YFg8EeevjiUa5LbfSGz5BxzRW` | `TEp6bCqSEKAr99sCiqANC84RtRwx7xGbA4` | +| `tron:0x94a9059e` | `TA3MZHMLsgi8JMU1DL8H4gKp1YjJKATibf` | `TRd1KBfy1iUs6R45oZrtbLUjtcSKzXAvPG` | `TNmfrxbKCHqPUTj9zHVfg4Dq8WNZXPyf1x` | + +The channel TIP-712 domain is `{ name: "x402 Batch Settlement", version: "1", chainId, +verifyingContract: channelContract }`. + +## Payment Requirements + +`PaymentRequirements.extra` contains: + +| Field | Required | Meaning | +| --- | --- | --- | +| `receiverAuthorizer` | Yes | Key authorized to approve claims and cooperative refunds | +| `withdrawDelay` | Yes | Withdrawal delay in seconds, from 900 through 2,592,000 | +| `name`, `version` | For ERC-3009 deposits | Token TIP-712 domain | +| `assetTransferMethod` | No | `eip3009` (default) or `permit2` | +| `channelState`, `voucherState` | Corrective 402 only | Server state used to resynchronize a client | + +The receiver authorizer may be held by the resource server or advertised by the facilitator. It MUST +be non-zero and must match `channelConfig.receiverAuthorizer`. + +## Channel Configuration and ID + +```text +ChannelConfig( + address payer, + address payerAuthorizer, + address receiver, + address receiverAuthorizer, + address token, + uint40 withdrawDelay, + bytes32 salt +) +``` + +The channel ID is the hash of the typed channel configuration under the network's channel-contract +domain. `payerAuthorizer` signs vouchers; it may equal the payer or be a delegated key. + +## Request Payloads + +The first request or a top-up uses `type: "deposit"`: + +```json +{ + "type": "deposit", + "channelConfig": { + "payer": "TPayerAddress", + "payerAuthorizer": "TPayerAddress", + "receiver": "TReceiverAddress", + "receiverAuthorizer": "TAuthorizerAddress", + "token": "TTokenAddress", + "withdrawDelay": 900, + "salt": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "voucher": { + "channelId": "0x...", + "maxClaimableAmount": "1000", + "signature": "0x..." + }, + "deposit": { + "amount": "5000", + "authorization": { + "permit2Authorization": { + "from": "0x...", + "permitted": { "token": "0x...", "amount": "5000" }, + "spender": "0x...", + "nonce": "1", + "deadline": "1786500000", + "witness": { "channelId": "0x..." }, + "signature": "0x..." + } + } + } +} +``` + +An EIP-3009 deposit instead carries `erc3009Authorization` with `validAfter`, `validBefore`, `salt`, +and `signature`. Its signed `ReceiveWithAuthorization` sends the deposit to the configured collector; +the nonce is `keccak256(abi.encode(channelId, salt))`. + +Subsequent requests use `type: "voucher"` with the same `channelConfig` and a voucher whose +`maxClaimableAmount` is cumulative. The voucher primary type is +`Voucher(bytes32 channelId,uint128 maxClaimableAmount)`. + +The server MAY accept a `type: "refund"` payload carrying the latest voucher and an optional amount. +It enriches that request with current claim data and authorizer signatures before facilitator +settlement. + +## Verification + +For deposits, the facilitator MUST validate: + +1. Scheme, network, channel ID, receiver, receiver authorizer, token, and withdrawal delay. +2. The deposit authorization selected by `assetTransferMethod`. +3. For Permit2: configured collector spender, token, deposit amount, channel witness, deadline, + signature, and sufficient allowance. +4. For ERC-3009: token domain, collector recipient, deposit amount, derived nonce, time window, and + signature. +5. The cumulative voucher signature and that `maxClaimableAmount` fits within post-deposit balance + and is greater than already claimed value. + +For vouchers and refunds, the facilitator verifies channel configuration, voucher signature, +on-chain channel existence, monotonic cumulative value, and balance bounds. + +The resource server serializes same-channel requests with a bounded pending reservation. It rejects a +voucher whose cumulative value does not equal `chargedCumulativeAmount + requestAmount`, and returns +corrective `channelState`/`voucherState` when the client must resynchronize. + +## Settlement and Deferred Actions + +| Payload type | Action | +| --- | --- | +| `deposit` | Call channel `deposit` through the selected collector and return the updated channel state | +| `voucher` | No immediate transfer; persist the verified cumulative commitment | +| `claim` | Call `claimWithSignature` for a batch of vouchers and update accounting | +| `settle` | Transfer claimed-but-unsettled tokens for a receiver/token pair | +| enriched `refund` | Claim as needed, then call `refundWithSignature` cooperatively | + +Successful deposit settlement returns the deposited amount and channel state. Voucher request +responses carry `chargedAmount`, channel state, and the latest signed voucher in `SettleResponse.extra`. + +## Error Codes + +TRON errors use the prefix `invalid_batch_settlement_tron_`. Stable categories cover channel lookup +and ID mismatch, token/receiver/authorizer mismatch, invalid voucher signatures, cumulative amount +bounds, deposit authorization or allowance failures, channel busy/resynchronization, invalid refund +amounts, RPC reads, and failed deposit/claim/settle/refund transactions. The exported names in +`typescript/packages/mechanisms/tron/src/batch-settlement/errors.ts` are the authoritative list. + +## Security Considerations + +- Vouchers are cumulative and MUST be monotonic relative to on-chain claimed state. +- A server MUST durably store its charged cumulative amount before accepting concurrent work. +- The receiver authorizer can approve claims and refunds; operators SHOULD isolate and rotate this key. +- `withdrawDelay` bounds unilateral exit risk and MUST stay within the contract limits. +- Deposit collectors and Permit2 addresses are network constants, never payload-selected contracts. +- A corrective 402 is part of state recovery, not authorization to reduce an already claimed amount. diff --git a/specs/schemes/exact-gasfree/scheme_exact_gasfree.md b/specs/schemes/exact-gasfree/scheme_exact_gasfree.md new file mode 100644 index 00000000..0fc02a94 --- /dev/null +++ b/specs/schemes/exact-gasfree/scheme_exact_gasfree.md @@ -0,0 +1,33 @@ +# Scheme: `exact_gasfree` + +## Summary + +`exact_gasfree` is a TRON-only exact-payment scheme in which the payer signs a GasFreeController +permit and a GasFree service provider relays the transaction. Payment value and the provider's fee +are deducted from the payer's GasFree smart-account token balance, so the payer does not need TRX. + +This is distinct from `exact` with Permit2: + +- funds reside in the payer's GasFree address; +- no Permit2 allowance is required; +- the signed permit includes the service provider and `maxFee`; +- settlement depends on an external GasFree relayer API. + +## Invariants + +- Both scheme fields MUST equal `exact_gasfree` and networks MUST match. +- The signed token and receiver MUST match the payment requirements. +- The signed value MUST be at least the required amount. +- The service provider MUST be returned by the configured relayer. +- The permit MUST be unexpired, signed by `user`, and use the network's GasFreeController domain. +- The GasFree address SHOULD have at least `value + maxFee` of the token before submission. + +## Network Binding + +- [TRON](scheme_exact_gasfree_tron.md) + +## Trust Model + +The payer trusts the selected provider and GasFree contracts to enforce `maxFee`. The facilitator +trusts the configured relayer API to report providers, accept permits, and return a final transaction +identifier. Implementations MUST fail closed if the provider list cannot be validated. diff --git a/specs/schemes/exact-gasfree/scheme_exact_gasfree_tron.md b/specs/schemes/exact-gasfree/scheme_exact_gasfree_tron.md new file mode 100644 index 00000000..54d12eec --- /dev/null +++ b/specs/schemes/exact-gasfree/scheme_exact_gasfree_tron.md @@ -0,0 +1,136 @@ +# Scheme: `exact_gasfree` on `TRON` + +## Summary + +The client signs a TIP-712 `PermitTransfer` for the network's GasFreeController. The facilitator +verifies the signed terms, submits them to a GasFree relayer, polls until the transaction succeeds or +fails, and returns the resulting TRON transaction ID. + +## Networks and Contracts + +| Network | GasFreeController | Beacon | Default relayer base URL | +| --- | --- | --- | --- | +| `tron:0x2b6653dc` | `TFFAMQLZybALaLb4uxHA9RBE7pxhUAjF3U` | `TSP9UW6FQhT76XD2jWA6ipGMx3yGbjDffP` | `https://facilitator.bankofai.io/mainnet` | +| `tron:0xcd8690dc` | `THQGuFzL87ZqhxkgqYEryRAd7gqFqL5rdc` | `TLtCGmaxH3PbuaF6kbybwteZcHptEdgQGC` | `https://facilitator.bankofai.io/nile` | +| `tron:0x94a9059e` | `TQghdCeVDA6CnuNVTUhfaAyPfTetqZWNpm` | `TQ1jvA3nLDMDNbJoMPLzTPoqAg8NvZ5CCW` | `https://facilitator.bankofai.io/shasta` | + +Deployments MAY override the relayer URL, but MUST retain the controller associated with the selected +network unless using a separately specified GasFree deployment. + +## Payment Requirements + +`scheme` is `exact_gasfree`; `network` is a supported `tron:*` identifier; `asset`, `amount`, and +`payTo` identify the TRC-20 payment. GasFree provider fees are not included in requirements and MUST +NOT be advertised as `extra.fee`. + +## Payment Payload + +```json +{ + "x402Version": 2, + "accepted": { + "scheme": "exact_gasfree", + "network": "tron:0xcd8690dc", + "amount": "1000", + "asset": "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", + "payTo": "TReceiverAddress", + "maxTimeoutSeconds": 60, + "extra": {} + }, + "payload": { + "signature": "0x...", + "gasfreeAddress": "TGasFreeAddress", + "gasfree": { + "token": "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", + "serviceProvider": "TProviderAddress", + "user": "TPayerAddress", + "receiver": "TReceiverAddress", + "value": "1000", + "maxFee": "1000000", + "deadline": "1786500000", + "version": "1", + "nonce": "7" + } + }, + "extensions": { + "scheme": "exact_gasfree", + "gasfreeAddress": "TGasFreeAddress" + } +} +``` + +The `extensions` fields above are emitted by the current client as compatibility metadata. The +facilitator validates the `payload` fields. + +## TIP-712 Authorization + +The domain is: + +```json +{ + "name": "GasFreeController", + "version": "V1.0.0", + "chainId": 3448148188, + "verifyingContract": "0x..." +} +``` + +`verifyingContract` is the selected controller normalized to 20-byte hex. The primary type is: + +```text +PermitTransfer( + address token, + address serviceProvider, + address user, + address receiver, + uint256 value, + uint256 maxFee, + uint256 deadline, + uint256 version, + uint256 nonce +) +``` + +The client obtains its GasFree address, activation state, nonce, supported assets, fees, and available +providers from the relayer. `maxFee` is the transfer fee plus activation fee when required. If fee +metadata is unavailable, the current SDK uses one whole token as a conservative fallback. + +Mainnet deadlines are clamped to roughly 55–595 seconds from creation; testnet deadlines are clamped +to roughly 55–3595 seconds. A caller-requested deadline below the minimum is rejected. + +## Verification + +The facilitator MUST: + +1. Require a complete `gasfree` object and signature. +2. Match scheme and network. +3. Match token and receiver after TRON address normalization. +4. Require `gasfree.value >= requirements.amount`. +5. Fetch providers and require an exact provider address match. Provider lookup failure or an empty + list is invalid. +6. Reject an expired deadline. +7. Reconstruct the TIP-712 message and verify the signature against `gasfree.user`. + +## Settlement + +The facilitator re-verifies, performs a best-effort balance check for `amount + maxFee` at +`gasfreeAddress`, submits the message and signature to `POST /api/v1/gasfree/submit`, and polls the +returned trace ID. Success requires a terminal success/on-chain state and a non-empty transaction +hash. + +## Error Codes + +Stable reasons include `invalid_exact_gasfree_scheme`, `invalid_exact_gasfree_network_mismatch`, +`missing_gasfree_payload`, `gasfree_token_mismatch`, `gasfree_amount_mismatch`, +`gasfree_payto_mismatch`, `gasfree_fee_to_mismatch`, `gasfree_expired`, +`invalid_gasfree_signature`, `insufficient_funds`, `gasfree_api_no_response`, +`gasfree_missing_transaction_hash`, and `gasfree_provider_list_unavailable`. + +Relayer transport errors may be returned as `errorReason` text by the current implementation. + +## Security Considerations + +The provider and `maxFee` are signed, preventing facilitator substitution. The account nonce and +deadline limit replay. The facilitator MUST validate the provider list at verification time so that a +payment accepted by `/verify` is likely to be accepted by `/settle`. Operators SHOULD use authenticated +or trusted relayer endpoints and avoid logging full signed payloads. diff --git a/specs/schemes/exact/scheme_exact.md b/specs/schemes/exact/scheme_exact.md new file mode 100644 index 00000000..94b80fe2 --- /dev/null +++ b/specs/schemes/exact/scheme_exact.md @@ -0,0 +1,45 @@ +# Scheme: `exact` + +## Summary + +`exact` transfers the amount quoted in `PaymentRequirements.amount` to `PaymentRequirements.payTo` +for one protected request. The client signs the complete payment terms and the facilitator pays the +network fee required to submit settlement. + +## Invariants + +Every `exact` network binding MUST enforce: + +- `paymentPayload.accepted.scheme` and `paymentRequirements.scheme` are `exact`. +- The accepted and verified network identifiers match. +- The signed asset matches `paymentRequirements.asset`. +- The signed recipient matches `paymentRequirements.payTo`. +- The signed value equals `paymentRequirements.amount`; overpayment and underpayment are invalid. +- The authorization is active, has not expired, and cannot be replayed after settlement. +- The signature is valid for the payer under the selected network binding. +- Settlement re-verifies the authorization before changing network state. + +## Lifecycle + +1. The resource server advertises one or more `exact` requirements. +2. The client selects a supported network and transfer method and signs an authorization. +3. The facilitator verifies the authorization without transferring value. +4. The resource server performs the protected work. +5. The facilitator re-verifies and settles the exact amount. +6. The resource server returns the result and a `SettleResponse` through the selected transport. + +## Network Bindings + +- [EVM](scheme_exact_evm.md): EIP-3009 or Permit2 with EIP-712 signatures. +- [TRON](scheme_exact_tron.md): TIP-712 TransferWithAuthorization or Permit2. + +The TRON-only `exact_gasfree` flow is a separate scheme because it signs a GasFreeController permit, +charges a relayer fee from a GasFree wallet, and has a different trust boundary. See +[`exact_gasfree`](../exact-gasfree/scheme_exact_gasfree.md). + +## Security Considerations + +The facilitator MUST NOT take a token, amount, recipient, deadline, spender, or verification contract +from untrusted payload fields without matching it to the requirements and binding-specific constants. +Best-effort balance or allowance reads do not replace signature and settlement validation; on-chain +execution remains authoritative. diff --git a/specs/schemes/exact/scheme_exact_evm.md b/specs/schemes/exact/scheme_exact_evm.md new file mode 100644 index 00000000..4931c52e --- /dev/null +++ b/specs/schemes/exact/scheme_exact_evm.md @@ -0,0 +1,355 @@ +# Scheme: `exact` on `EVM` + +## Summary + +The `exact` scheme on EVM executes a transfer where the Facilitator (server) pays the gas, but the Client (user) controls the exact flow of funds via cryptographic signatures. + +This repository selects one of two asset transfer methods, depending on the token's capabilities: + +| AssetTransferMethod | Use Case | Recommendation | Usage Semantics | +| :------------------ | :----------------------------------------------------------- | :--------------------------------------------- | :---------------------------------- | +| **1. EIP-3009** | Tokens with native `transferWithAuthorization` (e.g., USDC). | **Recommended** (Simplest, truly gasless). | One-time use | +| **2. Permit2** | Tokens without EIP-3009. Uses a Proxy + Permit2. | **Universal Fallback** (Works for any ERC-20). | One-time use | + +If no `assetTransferMethod` is specified in `PaymentRequired.extra`, clients should default to `"eip3009"`. Payment payloads that use a non-default transfer method should echo the selected `assetTransferMethod` in `accepted.extra`. + +In all cases, the Facilitator cannot modify the amount or destination. They serve only as the transaction broadcaster. + +--- + +## 1. AssetTransferMethod: `EIP-3009` + +The `eip3009` asset transfer method uses the `transferWithAuthorization` function directly on token contracts that support it. + +### Phase 1: `PAYMENT-SIGNATURE` Header Payload + +The `payload` field must contain: + +- `signature`: The 65-byte signature of the `transferWithAuthorization` operation. +- `authorization`: The parameters required to reconstruct the signed message. + +**Example PaymentPayload:** + +```json +{ + "x402Version": 2, + "resource": { + "url": "https://api.example.com/premium-data", + "description": "Access to premium market data", + "mimeType": "application/json" + }, + "accepted": { + "scheme": "exact", + "network": "eip155:84532", + "amount": "10000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 60, + "extra": { + "assetTransferMethod": "eip3009", + "name": "USDC", + "version": "2" + } + }, + "payload": { + "signature": "0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a1283259764173608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3d6293af148b571c", + "authorization": { + "from": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "value": "10000", + "validAfter": "1740672089", + "validBefore": "1740672154", + "nonce": "0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480" + } + } +} +``` + +**`extra` field definitions specific to `eip3009`:** + +- `extra.assetTransferMethod` (optional in `PaymentRequired`, default `"eip3009"`): if present, MUST be `"eip3009"`. +- `extra.name` (required): The EIP-712 domain name of the token contract. Used for `transferWithAuthorization` signature construction. +- `extra.version` (required): The EIP-712 domain version of the token contract. Used for `transferWithAuthorization` signature construction. + +### Phase 2: Verification Logic + +1. **Verify** the signature is valid and recovers to the `authorization.from` address. +2. **Verify** the `client` has sufficient balance of the `asset`. +3. **Verify** the authorization parameters (Amount, Validity Window) meet the `PaymentRequirements`. +4. **Verify** the Token and Network match the requirement. +5. **Simulate** `token.transferWithAuthorization(...)` to ensure success. + +### Phase 3: Settlement Logic + +Settlement is performed via the facilitator calling the `transferWithAuthorization` function on the `EIP-3009` compliant contract with the `payload.signature` and `payload.authorization` parameters from the `PAYMENT-SIGNATURE` header. + +--- + +## 2. AssetTransferMethod: `Permit2` + +This asset transfer method uses the `permitWitnessTransferFrom` from the [canonical **Permit2** contract](#canonical-permit2) combined with a [`x402ExactPermit2Proxy`](#reference-implementation-x402exactpermit2proxy) to enforce receiver address security via the "Witness" pattern. + +### Phase 1: One-Time Gas Approval + +Permit2 requires the user to approve the [**Permit2 Contract** (Canonical Address)](#canonical-permit2) to spend their tokens. This is a one-time setup. The specification supports three ways to handle this: + +#### Option A: Direct User Approval (Standard) + +The user submits a standard on-chain `approve(Permit2)` transaction paying their own gas. + +- _Prerequisite:_ User must have Native Gas currency. + +#### Option B: Sponsored ERC20 Approval (Extension: [`erc20ApprovalGasSponsoring`](../../extensions/erc20_gas_sponsoring.md)) + +The client provides a signed approval transaction and the extension signer submits it before +settlement. + +- _Prerequisite:_ Server supports this extension. +- _Flow:_ The extension signer receives the ordered operations `ERC20.approve(Permit2)` -> `settle`. + The signer decides whether to execute sequentially, as an account batch, or through an atomic + bundle, and is responsible for any gas-funding strategy it requires. + +#### Option C: EIP2612 Permit (Extension: [`eip2612GasSponsoring`](../../extensions/eip2612_gas_sponsoring.md)) + +If the token supports EIP-2612, the user signs a permit authorizing Permit2. + +- _Prerequisite:_ Token supports EIP-2612. +- _Flow:_ Facilitator calls `x402ExactPermit2Proxy.settleWithPermit()` + +### Phase 2: `PAYMENT-SIGNATURE` Header Payload + +The `payload` field must contain: + +- `signature`: The signature for `permitWitnessTransferFrom`. +- `permit2Authorization`: Parameters to reconstruct the message. + +**Important Logic:** The `spender` in the signature is the [**x402ExactPermit2Proxy**](#reference-implementation-x402exactpermit2proxy), not the Facilitator. This Proxy enforces that funds are only sent to the `witness.to` address. + +> **Requirement**: This contract will be deployed to the same address across all supported EVM chains using `CREATE2` to ensure consistent behavior and simpler integration. + +**Example PaymentPayload:** + +```json +{ + "x402Version": 2, + "accepted": { + "scheme": "exact", + "network": "eip155:84532", + "amount": "10000", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 60, + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "extra": { + "assetTransferMethod": "permit2", + "name": "USDC", + "version": "2" + } + }, + "payload": { + "signature": "0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a1283259764173608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3d6293af148b571c", + "permit2Authorization": { + "permitted": { + "token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "amount": "10000" + }, + "from": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "spender": "0x402085c248EeA27D92E8b30b2C58ed07f9E20001", + "nonce": "33247007178036348590600198031289925668252061821958005840077069883511451257277", + "deadline": "1740672154", + "witness": { + "to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "validAfter": "1740672089" + } + } + } +} +``` + +**`extra` field definitions specific to `permit2`:** + +- `extra.assetTransferMethod` (required): MUST be `"permit2"`. +- `extra.name` (conditional): The EIP-712 domain name of the token contract. Required when the token supports EIP-2612 for gasless Permit2 approval. +- `extra.version` (conditional): The EIP-712 domain version of the token contract. Required when the token supports EIP-2612 for gasless Permit2 approval. + +### Phase 3: Verification Logic + +The verifier must execute these checks in order: + +1. **Verify** `payload.signature` is valid and recovers to the `permit2Authorization.from`. + +2. **Verify** that the `client` has enabled the Permit2 approval. + + - if ERC20.allowance(from, Permit2_Address) < amount: + - Check for **Sponsored ERC20 Approval** (Extension): Refers to [`erc20ApprovalGasSponsoring`](../../extensions/erc20_gas_sponsoring.md). + - Check for **EIP2612 Permit** (Extension): Refers to [`eip2612GasSponsoring`](../../extensions/eip2612_gas_sponsoring.md). + - **If neither exists:** Return `permit2_allowance_required`. The HTTP resource server maps + this challenge to `412 Precondition Failed`, signaling that a one-time direct approval is + required before retrying. + +3. **Verify** the `client` has sufficient balance of the `asset`. + +4. **Verify** the `permit2Authorization.amount` covers the payment. + +5. **Verify** the `deadline` (not expired) and `witness.validAfter` (active). + +6. **Verify** the Token and Network match the requirement. + +7. **Simulation (Recommended):** + + Simulation is recommended but implementations may defer to re-verify-before-settle. + + - _Standard:_ Simulate `x402ExactPermit2Proxy.settle`. + - _With "Sponsored ERC20 Approval" (Extension):_ Ask the extension signer to simulate the + ordered `approve` -> `settle` operations when it provides `simulateTransactions`; otherwise + fall back to prerequisite checks. + - _With "EIP2612 Permit" (Extension):_ Simulate `x402ExactPermit2Proxy.settleWithPermit`. + +### Phase 4: Settlement Logic + +Settlement is performed by calling the `x402ExactPermit2Proxy`. + +1. **Standard Settlement:** + If the user has a sufficient direct allowance, call `x402ExactPermit2Proxy.settle`. + +2. **With Sponsored ERC20 Approval (Extension):** + If `erc20ApprovalGasSponsoring` is used, the facilitator passes the signed approval and unsigned + settlement calls to the registered extension signer in that order. Atomicity depends on the + signer's execution strategy. + +3. **With EIP-2612 Permit (Extension):** + If `eip2612GasSponsoring` is used, call `x402ExactPermit2Proxy.settleWithPermit`. + +--- + +## Implementer Notes + +- **Permit2 Dependency:** Integrators inherit the security properties and deployment assumptions of + both the canonical Permit2 contract and `x402ExactPermit2Proxy`. +- **Smart-account signatures:** EIP-1271, ERC-6492 counterfactual wallets, and ERC-7702 delegated + EOAs are supported as signature-validation forms for EIP-3009 and Permit2 payloads. + +--- + +## Annex + +### Canonical Permit2 + +The canonical Permit2 address used by this implementation is +`0x000000000022D473030F116dDEE9F6B43aC78BA3`. + +### Reference Implementation: `x402ExactPermit2Proxy` + +This contract acts as the authorized Spender. It validates the Witness data to ensure the destination cannot be altered by the Facilitator. + +> **Requirement**: This contract will be deployed to the same address across all supported EVM chains using `CREATE2` to ensure consistent behavior and simpler integration. + +**Canonical Address:** `0x402085c248EeA27D92E8b30b2C58ed07f9E20001` + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {ISignatureTransfer} from "permit2/src/interfaces/ISignatureTransfer.sol"; + +// Interface for EIP-2612 Support +interface IERC20Permit { + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; +} + +contract x402ExactPermit2Proxy { + ISignatureTransfer public immutable PERMIT2; + + event x402PermitTransfer(address from, address to, uint256 amount, address asset); + + // EIP-712 Type Definition (post-audit: extra removed from Witness) + string public constant WITNESS_TYPE_STRING = + "Witness witness)TokenPermissions(address token,uint256 amount)Witness(address to,uint256 validAfter)"; + + bytes32 public constant WITNESS_TYPEHASH = + keccak256("Witness(address to,uint256 validAfter)"); + + struct Witness { + address to; + uint256 validAfter; + } + + struct EIP2612Permit { + uint256 value; + uint256 deadline; + bytes32 r; + bytes32 s; + uint8 v; + } + + constructor(address _permit2) { + PERMIT2 = ISignatureTransfer(_permit2); + } + + /** + * @notice Settles a transfer using a standard Permit2 signature + */ + function settle( + ISignatureTransfer.PermitTransferFrom calldata permit, + address owner, + Witness calldata witness, + bytes calldata signature + ) external { + _settleInternal(permit, owner, witness, signature); + } + + /** + * @notice Extension: Settles a transfer using an EIP-2612 Permit for the allowance + */ + function settleWithPermit( + EIP2612Permit calldata permit2612, + ISignatureTransfer.PermitTransferFrom calldata permit, + address owner, + Witness calldata witness, + bytes calldata signature + ) external { + // 1. Submit the EIP-2612 Permit to the Token + IERC20Permit(permit.permitted.token).permit( + owner, + address(PERMIT2), + permit2612.value, + permit2612.deadline, + permit2612.v, permit2612.r, permit2612.s + ); + + // 2. Execute Permit2 Settlement + _settleInternal(permit, owner, witness, signature); + } + + function _settleInternal( + ISignatureTransfer.PermitTransferFrom calldata permit, + address owner, + Witness calldata witness, + bytes calldata signature + ) internal { + require(block.timestamp >= witness.validAfter, "Too early"); + + ISignatureTransfer.SignatureTransferDetails memory transferDetails = + ISignatureTransfer.SignatureTransferDetails({ + to: witness.to, + requestedAmount: permit.permitted.amount + }); + + bytes32 witnessHash = keccak256(abi.encode( + WITNESS_TYPEHASH, + witness.to, + witness.validAfter + )); + + PERMIT2.permitWitnessTransferFrom( + permit, + transferDetails, + owner, + witnessHash, + WITNESS_TYPE_STRING, + signature + ); + + emit x402PermitTransfer(owner, transferDetails.to, transferDetails.requestedAmount, permit.permitted.token); + } +} +``` diff --git a/specs/schemes/exact/scheme_exact_tron.md b/specs/schemes/exact/scheme_exact_tron.md new file mode 100644 index 00000000..2e877de0 --- /dev/null +++ b/specs/schemes/exact/scheme_exact_tron.md @@ -0,0 +1,154 @@ +# Scheme: `exact` on `TRON` + +## Summary + +The TRON `exact` binding transfers one fixed TRC-20 amount. It supports: + +| `extra.assetTransferMethod` | Authorization | Settlement | +| --- | --- | --- | +| `eip3009` or omitted | TIP-712 `TransferWithAuthorization` | Call the token's `transferWithAuthorization` | +| `permit2` | TIP-712 `PermitWitnessTransferFrom` | Call the network's `x402ExactPermit2Proxy.settle` | + +TRON Base58Check addresses are used in requirements and deployment configuration. Addresses inside +TIP-712 typed data are normalized to 20-byte, `0x`-prefixed hex by removing the TRON `0x41` network +prefix. + +## Networks and Contracts + +| Network | CAIP-2 ID | Permit2 | Exact proxy | +| --- | --- | --- | --- | +| Mainnet | `tron:0x2b6653dc` | `TTJxU3P8rHycAyFY4kVtGNfmnMH4ezcuM9` | `TN49yaJmZMZoEdDCqjB4uPzQLHvYkGw95m` | +| Nile | `tron:0xcd8690dc` | `TYQuuhGbEMxF7nZxUHV3uHJxAVVAegNU9h` | `TFGoaq2KjizijgjtkVxT7yjffW1A5T1j6F` | +| Shasta | `tron:0x94a9059e` | `TJMkP7a3ucTMkvi17p7ChhTCw6zriFX3tg` | `TGZkC38n14f2GpBWPMQLF2BpmcpWW3QNhg` | + +The numeric TIP-712 `chainId` is the hexadecimal CAIP-2 reference interpreted as an unsigned +integer. + +## Payment Requirements + +The common fields follow the [core specification](../../x402-specification-v2.md#51-paymentrequired-schema). +`extra` contains: + +| Field | Required | Meaning | +| --- | --- | --- | +| `assetTransferMethod` | No | `eip3009` (default) or `permit2` | +| `name` | For `eip3009` | Token TIP-712 domain name | +| `version` | For `eip3009` | Token TIP-712 domain version | + +The built-in token registry selects Permit2 for mainstream USDT/USDD deployments because those +tokens do not expose TransferWithAuthorization. + +## TransferWithAuthorization Payload + +```json +{ + "x402Version": 2, + "accepted": { + "scheme": "exact", + "network": "tron:0xcd8690dc", + "amount": "1000", + "asset": "TTokenAddress", + "payTo": "TReceiverAddress", + "maxTimeoutSeconds": 60, + "extra": { + "assetTransferMethod": "eip3009", + "name": "Example Token", + "version": "1" + } + }, + "payload": { + "signature": "0x...", + "authorization": { + "from": "0x1111111111111111111111111111111111111111", + "to": "0x2222222222222222222222222222222222222222", + "value": "1000", + "validAfter": "0", + "validBefore": "1786500000", + "nonce": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + } +} +``` + +The TIP-712 domain is `{ name, version, chainId, verifyingContract = asset }`. The primary type is +`TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 +validBefore,bytes32 nonce)`. + +## Permit2 Payload + +```json +{ + "x402Version": 2, + "accepted": { + "scheme": "exact", + "network": "tron:0xcd8690dc", + "amount": "1000", + "asset": "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", + "payTo": "TReceiverAddress", + "maxTimeoutSeconds": 60, + "extra": { "assetTransferMethod": "permit2" } + }, + "payload": { + "signature": "0x...", + "permit2Authorization": { + "from": "0x1111111111111111111111111111111111111111", + "permitted": { + "token": "0x2222222222222222222222222222222222222222", + "amount": "1000" + }, + "spender": "0x3333333333333333333333333333333333333333", + "nonce": "1", + "deadline": "1786500000", + "witness": { + "to": "0x4444444444444444444444444444444444444444", + "validAfter": "0" + } + } + } +} +``` + +The TIP-712 domain is `{ name: "Permit2", chainId, verifyingContract: Permit2 }`. The spender MUST +be the configured exact proxy. The witness binds `payTo`; `permitted.token` and `permitted.amount` +bind the asset and exact amount. The payer MUST first grant the Permit2 contract sufficient TRC-20 +allowance. The SDK-created client signer automatically broadcasts a one-time unlimited approval when +needed and when its wallet can sign TRON transactions. + +## Verification + +The facilitator MUST: + +1. Match both schemes and the accepted network. +2. Reconstruct typed data using the requirement's network and configured contracts. +3. Verify the payer signature. +4. Match recipient, asset, and exact amount. +5. Require at least six seconds of remaining validity and reject a future `validAfter`. +6. For Permit2, match the exact proxy spender and check Permit2 allowance when readable. +7. Check payer token balance when readable. + +Allowance and balance read failures are treated optimistically by the current implementation; all +cryptographic and term checks remain mandatory, and settlement is authoritative. + +## Settlement + +- TransferWithAuthorization: the facilitator calls the TRC-20 token directly with `(from, to, + value, validAfter, validBefore, nonce, v, r, s)`. +- Permit2: the facilitator calls `x402ExactPermit2Proxy.settle(permit, owner, witness, signature)`. + +The facilitator waits for a successful receipt and returns the TRON transaction ID. It MUST re-run +verification immediately before broadcasting. + +## Error Codes + +Stable reasons include `invalid_exact_tron_scheme`, `invalid_exact_tron_network_mismatch`, +`invalid_exact_tron_payload_signature`, `invalid_exact_tron_payload_recipient_mismatch`, +`invalid_exact_tron_payload_authorization_value_mismatch`, `invalid_permit2_spender`, +`permit2_amount_mismatch`, `permit2_token_mismatch`, `permit2_allowance_required`, +`insufficient_funds`, `invalid_transaction_state`, and `transaction_failed`. + +## Security Considerations + +Only configured chain IDs, Permit2 deployments, and proxy deployments may be used. Payload-supplied +addresses MUST NOT replace those constants. Permit2 nonce consumption and token authorization nonces +provide replay protection. The proxy is required because a raw Permit2 authorization without a +recipient-bound witness would let the submitter redirect funds. diff --git a/specs/schemes/upto/scheme_upto.md b/specs/schemes/upto/scheme_upto.md new file mode 100644 index 00000000..8378cc72 --- /dev/null +++ b/specs/schemes/upto/scheme_upto.md @@ -0,0 +1,75 @@ +# Scheme: `upto` + +## Summary + +`upto` is a scheme that authorizes a transfer of up to a **maximum amount** of funds from a client to a resource server. The actual amount charged is determined at settlement time based on resource consumption during the request. + +This scheme is ideal for usage-based pricing models where the final cost is not known until after the resource has been consumed. + +## Example Use Cases + +- Paying for LLM token generation (charge per token generated) +- Bandwidth or data transfer metering (charge per byte transferred in a single request) +- Dynamic compute pricing (charge based on actual resources consumed) + +## Core Properties (MUST) + +The `upto` scheme MUST enforce the following properties across ALL network implementations: + +### 1. Single-Use Authorization + +Each authorization MUST be settled at most once. After settlement (regardless of amount), the authorization is consumed and cannot be reused. + +- Rationale: Provides a clear audit trail, simpler mental model, and matches x402's request-response pattern. +- Implementation: On EVM, Permit2's nonce mechanism enforces this. Other networks MUST implement equivalent replay protection. + +### 2. Time-Bound Authorization + +Each authorization MUST have explicit validity time constraints: + +- **Start time** (`validAfter`): Authorization is not valid before this timestamp +- **End time** (`deadline`): Authorization expires after this timestamp + +- Rationale: Limits exposure window for unused authorizations and ensures timely settlement. +- Implementation: On EVM, Permit2's `deadline` and witness `validAfter` enforce this. Other networks MUST implement equivalent time bounds. + +### 3. Recipient Binding + +The authorization MUST cryptographically bind the recipient address. The server/facilitator cannot redirect funds to a different address than what the client signed. + +- Rationale: Prevents malicious facilitators from stealing funds. +- Implementation: On EVM, the Permit2 witness pattern binds `witness.to`. Other networks MUST implement equivalent recipient binding. + +### 4. Maximum Amount Enforcement + +The settled amount MUST be less than or equal to the authorized maximum. + +- The settled `amount` MUST be `<=` the authorized maximum +- The settled `amount` MAY be `0` (no charge if no usage occurred) + +### 5. Phase-Dependent `amount` Semantics in `PaymentRequirements` + +In the x402 protocol, the verify and settle requests share the same `PaymentPayload` and `PaymentRequirements` types. In the `upto` scheme, the `amount` field of `PaymentRequirements` is **phase-dependent** for server-to-facilitator communication: + +- At **verification** time, `amount` represents the **maximum** amount the client authorizes. +- At **settlement** time, `amount` represents the **actual amount to settle**, which MUST be less than or equal to the previously authorized maximum. + +The actual settled amount is communicated by the resource server to the facilitator via the `amount` field in the settlement-time `PaymentRequirements`. This allows the resource server to determine the final charge based on actual resource consumption (e.g., tokens generated, bytes transferred) and communicate it to the facilitator without requiring additional fields or a separate settlement type. + +- Rationale: Reusing the existing `PaymentRequirements` type for both phases keeps the protocol simple and avoids introducing settlement-specific message types. The `amount` field naturally maps to "how much" in both contexts — "how much is authorized" at verification time and "how much to charge" at settlement time. +- Implementation: The resource server MUST set the `amount` field in the `PaymentRequirements` passed to the facilitator's settle endpoint to the desired settlement amount. The facilitator MUST verify that this amount does not exceed the authorized maximum from the client's signed authorization. **Critically, the facilitator MUST re-verify the client's signature using the authorized maximum (`permitted.amount`), not the settlement-time `requirements.amount`.** See network-specific specs (e.g., [EVM — Settle-Time Verification](./scheme_upto_evm.md#settle-time-verification)) for the exact procedure. + +## Out of Scope + +The following patterns are NOT supported by `upto` and would require different schemes: + +- **Multi-settlement / streaming**: Settling the same authorization multiple times (e.g., pay-per-chunk streaming) +- **Recurring payments**: Automatic periodic charges without new authorizations +- **Open-ended allowances**: Authorizations without time bounds or single-use constraints + +## Network-Specific Implementation + +Network-specific rules and implementation details are defined in the per-network scheme documents: + +- EVM chains: See [`scheme_upto_evm.md`](./scheme_upto_evm.md) +- TRON: See [`scheme_upto_tron.md`](./scheme_upto_tron.md) diff --git a/specs/schemes/upto/scheme_upto_evm.md b/specs/schemes/upto/scheme_upto_evm.md new file mode 100644 index 00000000..2013fc90 --- /dev/null +++ b/specs/schemes/upto/scheme_upto_evm.md @@ -0,0 +1,330 @@ +# Scheme: `upto` on `EVM` + +## Summary + +The `upto` scheme on EVM enables usage-based payments where the Client (user) authorizes a **maximum amount**, and the Facilitator (server) settles for the **actual amount used** at the end of the request. This is ideal for variable-cost resources like LLM token generation, bandwidth metering, or time-based access. + +This scheme uses the **Permit2** asset transfer method exclusively, leveraging the `permitWitnessTransferFrom` function to allow settling for any amount up to the signed maximum. + +| AssetTransferMethod | Use Case | Notes | +| :------------------ | :-------------------------------------------------------------- | :---------------------------------------------- | +| **Permit2** | All ERC-20 tokens. Client signs max, server settles actual. | Uses existing `x402Permit2Proxy` contract. | + +> **Note**: EIP-3009 (`transferWithAuthorization`) is **not supported** for the `upto` scheme because it requires exact amounts at signature time. + +--- + +## Use Cases + +- **LLM Token Generation**: Client authorizes up to $5, actual charge based on tokens generated +- **Bandwidth/Data Transfer**: Pay per byte transferred in a single request, up to a cap +- **Dynamic Compute**: Authorize max cost, charge based on actual compute resources consumed + +--- + +## 1. AssetTransferMethod: `Permit2` + +This scheme uses the `permitWitnessTransferFrom` from the [canonical **Permit2** contract](#canonical-permit2) combined with the [`x402Permit2Proxy`](#reference-implementation-x402permit2proxy) to enforce receiver address security via the "Witness" pattern. + +The `permit.permitted.amount` represents the **maximum** authorized amount, while the actual settlement amount is determined by the server at settlement time. + +### Phase 1: One-Time Gas Approval + +Permit2 requires the user to approve the [**Permit2 Contract** (Canonical Address)](#canonical-permit2) to spend their tokens. This is a one-time setup. The specification supports three approval methods: + +#### Option A: Direct User Approval (Standard) + +The user submits a standard on-chain `approve(Permit2)` transaction paying their own gas. + +- _Prerequisite:_ User must have Native Gas currency. + +#### Option B: Sponsored ERC20 Approval (Extension: [`erc20ApprovalGasSponsoring`](../../extensions/erc20_gas_sponsoring.md)) + +The client provides a signed approval transaction and the extension signer submits it before +settlement. + +- _Prerequisite:_ Server supports this extension. +- _Flow:_ The extension signer receives the ordered operations `ERC20.approve(Permit2)` -> `settle`. + It owns batching, atomicity, and any gas-funding strategy. + +#### Option C: EIP2612 Permit (Extension: [`eip2612GasSponsoring`](../../extensions/eip2612_gas_sponsoring.md)) + +If the token supports EIP-2612, the user signs a permit authorizing Permit2. + +- _Prerequisite:_ Token supports EIP-2612. +- _Flow:_ Facilitator calls `x402Permit2Proxy.settleWithPermit()` + +### Phase 2: `PAYMENT-SIGNATURE` Header Payload + +The `payload` field must contain: + +- `signature`: The signature for `permitWitnessTransferFrom`. +- `permit2Authorization`: Parameters to reconstruct the message. + +**Important Logic:** The `permit2Authorization.permitted.amount` represents the **maximum** amount the client is willing to pay. The actual amount charged will be determined at settlement and will be less than or equal to this maximum. + +> **Requirement**: The `x402Permit2Proxy` contract will be deployed to the same address across all supported EVM chains using `CREATE2` to ensure consistent behavior and simpler integration. + +**Facilitator Address Discovery:** The facilitator announces its address via the `/supported` endpoint in the `extra` field of each supported scheme. The client MUST include this `facilitatorAddress` in the `permit2Authorization.witness.facilitator` field when constructing the payment signature. This binds the authorization to a specific facilitator, preventing unauthorized settlement by other parties. + +**Example PaymentRequired (402 Response):** + +```json +{ + "x402Version": 2, + "error": "PAYMENT-SIGNATURE header is required", + "resource": { + "url": "https://api.example.com/llm/generate", + "description": "LLM text generation endpoint", + "mimeType": "application/json" + }, + "accepts": [ + { + "scheme": "upto", + "network": "eip155:84532", + "amount": "5000000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 300, + "extra": { + "name": "USDC", + "version": "2", + "facilitatorAddress": "0xFacilitatorAddress1234567890123456789012" + } + } + ] +} +``` + +**Example PaymentPayload (Client Request):** + +```json +{ + "x402Version": 2, + "resource": { + "url": "https://api.example.com/llm/generate", + "description": "LLM text generation endpoint", + "mimeType": "application/json" + }, + "accepted": { + "scheme": "upto", + "network": "eip155:84532", + "amount": "5000000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 300, + "extra": { + "name": "USDC", + "version": "2" + } + }, + "payload": { + "signature": "0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a1283259764173608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3d6293af148b571c", + "permit2Authorization": { + "permitted": { + "token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "amount": "5000000" + }, + "from": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "spender": "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002", + "nonce": "0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480", + "deadline": "1740672154", + "witness": { + "to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "facilitator": "0xFacilitatorAddress1234567890123456789012", + "validAfter": "1740672089" + } + } + } +} +``` + +### Phase 3: Verification Logic + +The verifier must execute these checks in order: + +1. **Verify** `payload.signature` is valid and recovers to the `permit2Authorization.from`. + + - Note that `extra` must be converted to its ABI encoded version. + +2. **Verify** that the `client` has enabled the Permit2 approval. + + - if ERC20.allowance(from, Permit2_Address) < amount: + - Check for **Sponsored ERC20 Approval** (Extension): Refers to [`erc20ApprovalGasSponsoring`](../../extensions/erc20_gas_sponsoring.md). + - Check for **EIP2612 Permit** (Extension): Refers to [`eip2612GasSponsoring`](../../extensions/eip2612_gas_sponsoring.md). + - **If neither exists:** Return `permit2_allowance_required`. The HTTP resource server maps + this challenge to `412 Precondition Failed`. + +3. **Verify** the `client` has sufficient balance of the `asset` to cover `amount`. + +4. **Verify** the `permit2Authorization.permitted.amount` equals the `amount` from requirements. + + > **Note**: This check applies at **verification** time, where `requirements.amount` represents the authorized maximum. At **settlement** time, `requirements.amount` carries the actual settlement amount, which may be less than `permitted.amount`. See [Phase 4 — Settle-Time Verification](#settle-time-verification) for the required handling. + +5. **Verify** the `deadline` (not expired) and `witness.validAfter` (active). + +6. **Verify** the Token and Network match the requirement. + +7. **Simulation:** + + - _Standard:_ Simulate `x402Permit2Proxy.settle` with the full `amount` (worst case). + - _With "Sponsored ERC20 Approval" (Extension):_ Use the extension signer's combined simulation + when available; otherwise fall back to prerequisite checks. + - _With "EIP2612 Permit" (Extension):_ Simulate `x402Permit2Proxy.settleWithPermit`. + +### Phase 4: Settlement Logic + +Settlement is performed by calling the `x402Permit2Proxy` with the **actual amount** to charge. + +The server determines the actual amount based on resource consumption during the request (tokens generated, bytes transferred, time elapsed, etc.). + +**Settlement Amount Rules:** + +- The settled `amount` MUST be `<=` the authorized maximum +- The settled `amount` MAY be `0` (no charge if no usage occurred) +- The settled `amount` is determined by the resource server, not the client + +#### Settle-Time Verification + +Before executing an on-chain settlement, the facilitator MUST re-verify the client's signature. Because the `upto` scheme uses phase-dependent `amount` semantics (see [§2 PaymentRequirements Schema](#2-paymentrequirements-schema)), the `/settle` request will carry `paymentRequirements.amount` set to the **actual settlement amount** (as communicated by the resource server via `setSettlementOverrides`), which may be less than `paymentPayload.payload.permit2Authorization.permitted.amount` (the **authorized maximum** the client signed). + +The facilitator MUST handle this case as follows: + +1. **Verify the signature against `permitted.amount`** — When re-verifying the client's signature at settle time, use `permit2Authorization.permitted.amount` (the ceiling from the signed payload), NOT `paymentRequirements.amount` (the actual settlement amount). The client signed for the ceiling; comparing against the metered amount would always fail for partial settlements. + +2. **Validate** `paymentRequirements.amount <= permit2Authorization.permitted.amount` — The actual settlement amount must not exceed the authorized maximum. + +3. **Execute the on-chain transfer for `paymentRequirements.amount`** — The `x402UptoPermit2Proxy.settle` call uses the actual settlement amount, not the ceiling. + +> **Conformance note**: A facilitator that enforces `paymentRequirements.amount === permit2Authorization.permitted.amount` at settle time will reject all partial settlements, breaking the core `upto` value proposition. The Phase 3 step 4 equality check (`permitted.amount === requirements.amount`) applies only to the `/verify` endpoint, where `requirements.amount` carries the ceiling. + +**Example settle request wire shape** (partial settlement): + +```jsonc +{ + "x402Version": 2, + "paymentPayload": { + "x402Version": 2, + "accepted": { "scheme": "upto", "network": "eip155:84532", "amount": "20000", ... }, + "payload": { + "signature": "0x...", + "permit2Authorization": { + "permitted": { "token": "0x036CbD53...", "amount": "20000" }, + "from": "", "spender": "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002", + "nonce": "...", "deadline": "...", + "witness": { "to": "", "facilitator": "", "validAfter": "0" } + } + } + }, + "paymentRequirements": { + "scheme": "upto", "network": "eip155:84532", + "asset": "0x036CbD53...", "payTo": "", + "amount": "1858" + } +} +``` + +In this example, the buyer signed for up to `20000` atomic units. The resource server consumed `1858` units of work. The facilitator verifies the signature against `permitted.amount` (`20000`), confirms `1858 <= 20000`, then transfers `1858` on-chain. + +**Settlement Process:** + +1. **Standard Settlement:** + Call `x402Permit2Proxy.settle(permit, actualAmount, owner, witness, signature)` where `actualAmount <= permit.permitted.amount`. + +2. **With Sponsored ERC20 Approval (Extension):** + If `erc20ApprovalGasSponsoring` is used, the facilitator must construct a batched transaction that executes the sponsored `ERC20.approve` call strictly before the `x402Permit2Proxy.settle` call. + +3. **With EIP-2612 Permit (Extension):** + If `eip2612GasSponsoring` is used, call `x402Permit2Proxy.settleWithPermit`. + +4. **Zero Settlement:** + If the settled `amount = 0`, no on-chain transaction is required. The authorization simply expires unused. + +**Example SettlementResponse:** + +```json +{ + "success": true, + "transaction": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "network": "eip155:84532", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "amount": "2350000" +} +``` + +--- + +## 2. PaymentRequirements Schema + +The `upto` scheme uses the following `PaymentRequirements` schema: + +| Field Name | Type | Required | Description | +| ------------------- | -------- | -------- | ----------------------------------------------------------------------------- | +| `scheme` | `string` | Required | Must be `"upto"` | +| `network` | `string` | Required | Blockchain network identifier in CAIP-2 format (e.g., "eip155:84532") | +| `amount` | `string` | Required | Phase-dependent: maximum amount at verification, actual amount at settlement | +| `asset` | `string` | Required | Token contract address | +| `payTo` | `string` | Required | Recipient wallet address | +| `maxTimeoutSeconds` | `number` | Required | Maximum time allowed for payment completion | +| `extra` | `object` | Optional | Scheme-specific additional information (must include `name`, `version`, and `facilitatorAddress`) | + +> **Note**: In the `upto` scheme, the `amount` field of `PaymentRequirements` is phase-dependent for server-to-facilitator communication: +> +> - At _verification_ time, `amount` represents the **maximum** amount the client authorizes. +> - At _settlement_ time, `amount` represents the **actual amount to settle**, which MUST be less than or equal to the previously authorized maximum. +> +> The actual settled amount is communicated by the resource server to the facilitator via the `amount` field in the settlement-time payment requirements. This allows the server to determine the final charge based on actual resource consumption without requiring additional fields or a separate settlement type. + +--- + +## 3. SettlementResponse Schema Extension + +The `upto` scheme extends the base [`SettlementResponse`](../../x402-specification-v2.md#53-settlementresponse-schema) with the actual settled amount: + +| Field Name | Type | Required | Description | +| --------------- | --------- | -------- | --------------------------------------------------------------------- | +| `success` | `boolean` | Required | Indicates whether the payment settlement was successful | +| `errorReason` | `string` | Optional | Error reason if settlement failed (omitted if successful) | +| `payer` | `string` | Optional | Address of the payer's wallet | +| `transaction` | `string` | Required | Blockchain transaction hash (empty string if $0 settlement) | +| `network` | `string` | Required | Blockchain network identifier in CAIP-2 format | +| `amount` | `string` | Required | Actual amount charged in atomic token units (may be 0) | + +--- + +## 4. Error Codes + +The `upto` scheme uses the standard x402 error codes defined in the [x402 specification](../../x402-specification-v2.md#9-error-handling). + +### Scheme-Specific Error Code + +The `upto` scheme defines one additional error code: + +- **`invalid_upto_evm_payload_settlement_exceeds_amount`**: Attempted to settle for more than the authorized `amount` + +--- + +## Annex + +### Canonical Permit2 + +The canonical Permit2 address used by this implementation is +`0x000000000022D473030F116dDEE9F6B43aC78BA3`. + +### Reference Implementation: `x402Permit2Proxy` + +The `upto` scheme uses its own `x402UptoPermit2Proxy` contract (deployed at `0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002`), which is structurally similar to the `x402ExactPermit2Proxy` used by the [exact scheme](../exact/scheme_exact_evm.md#reference-implementation-x402exactpermit2proxy) but includes a `facilitator` field in the witness struct for access control. The contract's `settle` function accepts an `amount` parameter that can be less than or equal to `permit.permitted.amount`, which enables the variable settlement amounts required by the `upto` scheme. + +--- + +## Security Considerations + +1. **Maximum Amount Authorization**: Clients should carefully consider the `amount` they authorize. While servers can only charge up to this amount, clients bear the risk of the full amount being charged. + +2. **Server Trust**: The `upto` scheme requires clients to trust that servers will charge fair amounts based on actual usage. Malicious servers could charge up to `amount` regardless of actual usage. + +3. **Signature Reuse Prevention**: The Permit2 nonce mechanism prevents signature reuse. Each authorization can only be settled once. + +4. **Time Constraints**: Authorizations have explicit valid time windows (`deadline`, `validAfter`) to limit their lifetime and reduce exposure. + +5. **Zero Settlement**: Allowing $0 settlements means unused authorizations naturally expire without on-chain transactions, reducing gas costs and blockchain bloat. diff --git a/specs/schemes/upto/scheme_upto_tron.md b/specs/schemes/upto/scheme_upto_tron.md new file mode 100644 index 00000000..b5241b75 --- /dev/null +++ b/specs/schemes/upto/scheme_upto_tron.md @@ -0,0 +1,106 @@ +# Scheme: `upto` on `TRON` + +## Summary + +The TRON `upto` binding uses Permit2 and `x402UptoPermit2Proxy`. The payer signs a maximum token +amount. After performing the protected work, the resource server supplies an actual settlement +amount no greater than the maximum. + +## Networks and Contracts + +The Permit2 deployments are listed in the [TRON exact binding](../exact/scheme_exact_tron.md#networks-and-contracts). +The upto proxy deployments are: + +| Network | `x402UptoPermit2Proxy` | +| --- | --- | +| `tron:0x2b6653dc` | `TBLeFPkfDiweBbYmAPqnakaFBPDt9p93sR` | +| `tron:0xcd8690dc` | `TKvcqQ7S2bYyys5ZZNpjj9xGiPhiwzHq1K` | +| `tron:0x94a9059e` | `TMxpieW75DQiA9QaoTB1ifJWeQpuppSB1g` | + +## Payment Requirements + +`extra.assetTransferMethod` MUST be `permit2`. `extra.permit2FacilitatorAddress` MUST contain one +facilitator signer address advertised by `GET /supported`; the client binds it into the witness. + +At verification time, `PaymentRequirements.amount` is the authorized maximum. At settlement time, +the resource server replaces it with the actual amount. The signed maximum remains in +`payload.permit2Authorization.permitted.amount`. + +## Payment Payload + +```json +{ + "x402Version": 2, + "accepted": { + "scheme": "upto", + "network": "tron:0xcd8690dc", + "amount": "10000", + "asset": "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", + "payTo": "TReceiverAddress", + "maxTimeoutSeconds": 60, + "extra": { + "assetTransferMethod": "permit2", + "permit2FacilitatorAddress": "TFacilitatorAddress" + } + }, + "payload": { + "signature": "0x...", + "permit2Authorization": { + "from": "0x1111111111111111111111111111111111111111", + "permitted": { + "token": "0x2222222222222222222222222222222222222222", + "amount": "10000" + }, + "spender": "0x3333333333333333333333333333333333333333", + "nonce": "1", + "deadline": "1786500000", + "witness": { + "to": "0x4444444444444444444444444444444444444444", + "facilitator": "0x5555555555555555555555555555555555555555", + "validAfter": "0" + } + } + } +} +``` + +The TIP-712 domain is `{ name: "Permit2", chainId, verifyingContract: Permit2 }`. The primary type +is `PermitWitnessTransferFrom`; its `Witness` is `(address to,address facilitator,uint256 +validAfter)`. + +## Verification + +The facilitator MUST verify: + +1. Both schemes are `upto` and networks match. +2. `spender` is the configured upto proxy. +3. The witness recipient equals `payTo`. +4. The witness facilitator equals one of its own signer addresses. +5. The authorization is active and has at least six seconds before its deadline. +6. The permitted token equals the requirement asset. +7. The permitted amount equals the verification-time requirement amount. +8. The signature is valid under the configured TRON Permit2 domain. +9. Permit2 allowance and balance cover the maximum when those reads succeed. + +## Settlement + +Before settlement, the facilitator reconstructs verification requirements using the signed maximum, +then verifies again. It rejects `actualAmount > signedMaximum`. + +- For `actualAmount > 0`, it calls `x402UptoPermit2Proxy.settle(permit, actualAmount, owner, + witness, signature)` and returns `SettleResponse.amount`. +- For `actualAmount == 0`, it returns success with an empty transaction ID and `amount: "0"`; no + nonce is consumed on-chain. + +## Error Codes + +Stable reasons include `invalid_upto_tron_scheme`, `invalid_upto_tron_network_mismatch`, +`unsupported_payload_type`, `invalid_permit2_spender`, `invalid_permit2_facilitator`, +`permit2_amount_mismatch`, `permit2_token_mismatch`, `permit2_allowance_required`, +`upto_settlement_exceeds_amount`, `insufficient_funds`, and `transaction_failed`. + +## Security Considerations + +The facilitator witness prevents a different submitter from consuming the authorization. The server +chooses the actual charge, so the payer trusts the server's metering up to the signed maximum. A zero +settlement does not consume the Permit2 nonce and MUST NOT be represented as an on-chain payment. diff --git a/specs/transport_template.md b/specs/transport_template.md new file mode 100644 index 00000000..82de4149 --- /dev/null +++ b/specs/transport_template.md @@ -0,0 +1,34 @@ +# Transport: `` + +## Summary + +Describe the request/response protocol that carries x402 v2 objects. + +## Payment Required Signaling + +Define how the server carries a `PaymentRequired` object and how clients detect it. + +## Payment Payload Transmission + +Define how a client carries a `PaymentPayload` object on the retried request. + +## Settlement Response Delivery + +Define how the server carries a `SettleResponse` after processing payment. + +## Encoding and Limits + +Define serialization, character encoding, size limits, and duplicate-field behavior. + +## Error Handling + +Map protocol failures to transport-specific status or error forms. + +## Security Considerations + +Cover integrity, confidentiality, intermediary behavior, replay, and logging of payment data. + +## References + +- [Core x402 v2 Specification](x402-specification-v2.md) +- Relevant transport protocol documentation diff --git a/specs/transports-v2/http.md b/specs/transports-v2/http.md new file mode 100644 index 00000000..d2c784b2 --- /dev/null +++ b/specs/transports-v2/http.md @@ -0,0 +1,175 @@ +# Transport: HTTP + +## 1. Scope + +The HTTP transport carries x402 v2 objects in HTTP response and request headers. It is implemented +by `@bankofai/x402-core/http` and the Fetch, Axios, Express, Hono, Fastify, and Next.js integration +packages. + +All header values described here are Base64 encodings of UTF-8 JSON. + +## 2. Header mapping + +| Header | Direction | Encoded object | +| --- | --- | --- | +| `PAYMENT-REQUIRED` | Resource server to client | `PaymentRequired` | +| `PAYMENT-SIGNATURE` | Client to resource server | `PaymentPayload` | +| `PAYMENT-RESPONSE` | Resource server to client | `SettleResponse` | + +Header names are case-insensitive as required by HTTP. This specification uses uppercase names for +clarity. + +## 3. Payment challenge + +When payment is required, an API response normally uses status `402` and includes a +`PAYMENT-REQUIRED` header: + +```http +HTTP/1.1 402 Payment Required +Content-Type: application/json +PAYMENT-REQUIRED: + +{} +``` + +Decoded header: + +```json +{ + "x402Version": 2, + "error": "PAYMENT-SIGNATURE header is required", + "resource": { + "url": "https://api.example.com/report", + "description": "Premium report", + "mimeType": "application/json" + }, + "accepts": [ + { + "scheme": "exact", + "network": "eip155:56", + "amount": "1000000", + "asset": "0x55d398326f99059fF775485246999027B3197955", + "payTo": "0x1111111111111111111111111111111111111111", + "maxTimeoutSeconds": 60, + "extra": { + "assetTransferMethod": "permit2", + "name": "USDT", + "version": "1" + } + } + ] +} +``` + +The body is application-defined. An HTML-capable browser may receive a generated paywall body while +the same canonical `PaymentRequired` remains in the header. + +The current resource-server implementation uses status `412 Precondition Failed` when the challenge +reason is `permit2_allowance_required`; the `PAYMENT-REQUIRED` header remains unchanged. + +## 4. Paid retry + +The client selects one requirement, creates a scheme-specific payload, and retries the resource +request with `PAYMENT-SIGNATURE`: + +```http +POST /report HTTP/1.1 +Host: api.example.com +Content-Type: application/json +PAYMENT-SIGNATURE: + +{"region":"apac"} +``` + +Decoded header: + +```json +{ + "x402Version": 2, + "resource": { + "url": "https://api.example.com/report", + "description": "Premium report", + "mimeType": "application/json" + }, + "accepted": { + "scheme": "exact", + "network": "eip155:56", + "amount": "1000000", + "asset": "0x55d398326f99059fF775485246999027B3197955", + "payTo": "0x1111111111111111111111111111111111111111", + "maxTimeoutSeconds": 60, + "extra": { + "assetTransferMethod": "permit2", + "name": "USDT", + "version": "1" + } + }, + "payload": { + "signature": "0x...", + "permit2Authorization": { + "permitted": { + "token": "0x55d398326f99059fF775485246999027B3197955", + "amount": "1000000" + }, + "from": "0x2222222222222222222222222222222222222222", + "spender": "0x402085c248EeA27D92E8b30b2C58ed07f9E20001", + "nonce": "1234", + "deadline": "1786464300", + "witness": { + "to": "0x1111111111111111111111111111111111111111", + "validAfter": "1786464200" + } + } + } +} +``` + +Extensions, when used, are carried in the top-level `extensions` field of both protocol objects as +defined by the core specification. + +## 5. Settlement response + +A successful paid response includes the application body and `PAYMENT-RESPONSE`: + +```http +HTTP/1.1 200 OK +Content-Type: application/json +PAYMENT-RESPONSE: + +{"report":"..."} +``` + +Decoded header: + +```json +{ + "success": true, + "transaction": "0xabc123...", + "network": "eip155:56", + "payer": "0x2222222222222222222222222222222222222222", + "amount": "1000000" +} +``` + +If verification or settlement fails, the server returns an unpaid response and exposes the reason +through a new `PaymentRequired` challenge or a failed `SettleResponse`, depending on when the failure +occurs. Framework adapters preserve the protocol headers returned by the core resource server. + +## 6. Client behavior + +An automatic HTTP client SHOULD: + +1. detect a `402` or `412` response with `PAYMENT-REQUIRED`; +2. decode and validate the header; +3. select one compatible requirement; +4. obtain user approval when required by policy; +5. retry only once with `PAYMENT-SIGNATURE`; and +6. expose `PAYMENT-RESPONSE` to the caller. + +Clients MUST prevent unbounded retry loops. Browser integrations SHOULD expose +`PAYMENT-REQUIRED` and `PAYMENT-RESPONSE` through CORS where cross-origin callers need them. + +## 7. References + +- [Core x402 v2 specification](../x402-specification-v2.md) +- [HTTP Semantics: 402 Payment Required](https://www.rfc-editor.org/rfc/rfc9110.html#name-402-payment-required) diff --git a/specs/transports-v2/mcp.md b/specs/transports-v2/mcp.md new file mode 100644 index 00000000..29a0bc4e --- /dev/null +++ b/specs/transports-v2/mcp.md @@ -0,0 +1,166 @@ +# Transport: MCP + +## 1. Scope + +The MCP transport applies the x402 v2 payment lifecycle to paid Model Context Protocol tool calls. +It is implemented by `@bankofai/x402-mcp`. + +## 2. Flow + +1. The client calls a paid tool without payment. +2. The server returns a `PaymentRequired` challenge. +3. The client selects a requirement and creates a `PaymentPayload`. +4. The client retries with the payload in `_meta["x402/payment"]`. +5. The server verifies payment, runs the tool, and settles. +6. A successful result carries `SettleResponse` in `_meta["x402/payment-response"]`. + +## 3. Payment challenge result + +The BANK OF AI server wrapper returns a normal MCP tool result with `isError: true` and the same +`PaymentRequired` in two representations: + +- `structuredContent`: the direct object; and +- `content[0].text`: `JSON.stringify(PaymentRequired)`. + +```json +{ + "isError": true, + "structuredContent": { + "x402Version": 2, + "error": "Payment required to access this tool", + "resource": { + "url": "mcp://tool/financial_analysis", + "description": "Financial analysis", + "mimeType": "application/json" + }, + "accepts": [ + { + "scheme": "exact", + "network": "tron:0x2b6653dc", + "amount": "1000000", + "asset": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + "payTo": "TReceiverAddress", + "maxTimeoutSeconds": 60, + "extra": { + "assetTransferMethod": "eip3009", + "name": "Tether USD", + "version": "1" + } + } + ] + }, + "content": [ + { + "type": "text", + "text": "{\"x402Version\":2,\"error\":\"Payment required to access this tool\",\"resource\":{...},\"accepts\":[...]}" + } + ] +} +``` + +Clients SHOULD prefer `structuredContent` and fall back to parsing the first text content item. + +For interoperability with other MCP servers, the BANK OF AI client also recognizes a thrown payment +error with code `402`, and the MCP `UrlElicitationRequired` code `-32042`. In the `-32042` form, +`PaymentRequired` may be `error.data` directly or `error.data.x402`. + +## 4. Payment payload + +On retry, the client places the direct `PaymentPayload` object in the call metadata. It is not Base64 +encoded: + +```json +{ + "method": "tools/call", + "params": { + "name": "financial_analysis", + "arguments": { + "ticker": "0700.HK" + }, + "_meta": { + "x402/payment": { + "x402Version": 2, + "resource": { + "url": "mcp://tool/financial_analysis", + "description": "Financial analysis", + "mimeType": "application/json" + }, + "accepted": { + "scheme": "exact", + "network": "tron:0x2b6653dc", + "amount": "1000000", + "asset": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + "payTo": "TReceiverAddress", + "maxTimeoutSeconds": 60, + "extra": { + "assetTransferMethod": "eip3009", + "name": "Tether USD", + "version": "1" + } + }, + "payload": { + "signature": "0x...", + "authorization": { + "from": "TPayerAddress", + "to": "TReceiverAddress", + "value": "1000000", + "validAfter": "1786464200", + "validBefore": "1786464300", + "nonce": "0x..." + } + } + } + } + } +} +``` + +The server matches `accepted` against its post-extension requirements and validates echoed extension +data before invoking the scheme verifier. + +## 5. Successful result + +After the handler succeeds and settlement completes, the server preserves the handler result and +adds settlement metadata: + +```json +{ + "content": [ + { + "type": "text", + "text": "Analysis result..." + } + ], + "_meta": { + "x402/payment-response": { + "success": true, + "transaction": "abc123...", + "network": "tron:0x2b6653dc", + "payer": "TPayerAddress", + "amount": "1000000" + } + } +} +``` + +Any pre-existing result `_meta` fields are preserved. + +## 6. Failure and cancellation behavior + +- Missing, unmatched, or invalid payment returns another payment challenge result. +- If a pre-execution hook blocks the call, the handler is not run and a challenge is returned. +- If the handler returns `isError: true`, settlement is not attempted and any cancellable payment is + cancelled. +- If the handler throws, the wrapper dispatches payment cancellation and rethrows. +- If settlement throws after handler execution, the wrapper returns `isError: true` with a fresh + `PaymentRequired` carrying the settlement error. It does not expose the tool content or attach + `x402/payment-response` to that error result. + +Clients MUST prevent repeated automatic payment attempts when a paid retry receives another +challenge. Applications SHOULD treat tool handlers as non-retriable after side effects unless their +own idempotency design makes retry safe. + +## 7. References + +- [Core x402 v2 specification](../x402-specification-v2.md) +- [Model Context Protocol](https://modelcontextprotocol.io/specification/) diff --git a/specs/x402-specification-v2.md b/specs/x402-specification-v2.md new file mode 100644 index 00000000..11757afa --- /dev/null +++ b/specs/x402-specification-v2.md @@ -0,0 +1,738 @@ +# X402 Protocol Specification + +**Protocol Version**: 2 + +**Document Scope** + +This specification defines the core x402 protocol for internet-native payments. It covers: + +- **Protocol fundamentals**: Payment requirements format, payment payload structure, and core message schemas +- **Facilitator interface**: Standard APIs for payment verification and settlement +- **Payment schemes**: `exact`, `upto`, `batch-settlement`, `auth-capture`, and `exact_gasfree` + with their implemented EVM or TRON bindings +- **Security considerations**: Replay attack prevention and trust minimization + +**Out of Scope**: This specification does not include: + +- Transport-specific implementations (covered in transport specifications) +- Specific implementation patterns (covered in application notes) +- Framework-specific integrations +- Client-side budget management +- Session handling mechanisms + +**Architecture** + +x402 is made up of three core components: + +1. **Types**: Core data structures (e.g., `PaymentRequirements`, `PaymentPayload`, `SettlementResponse`) that are independent of both transport mechanism and payment scheme +2. **Logic**: Payment formation and verification logic that depends on the payment scheme and its EVM or TRON network binding +3. **Representation**: How payment data is transmitted and signaled over HTTP or MCP + +## 1. Overview + +x402 is an open payment standard that enables clients to pay for external resources. The protocol defines standardized message formats and payment flows that can be implemented over various transport layers, providing a standardized mechanism for payments across different payment schemes, networks and transport layers. + +This specification is based on the protocol implementation and documentation in the +[x402 Foundation repository](https://github.com/x402-foundation/x402). It is the v2 implementation +profile for the BANK OF AI TypeScript SDK. Legacy revisions and capabilities not present in this +repository are outside its scope. + +## 2. Core Payment Flow + +The x402 protocol follows a standard request-response cycle with payment integration: + +1. **Client Request**: Client makes a request to a resource server +2. **Payment Required Response**: If no valid payment is attached, the server responds with a payment required signal and payment requirements +3. **Payment Authorization Request**: Client submits a signed payment authorization in the subsequent request +4. **Settlement Response**: Server verifies the payment authorization and initiates blockchain settlement + +## 3. Protocol Components + +The x402 protocol involves three primary components: + +- **Resource Server**: A service that requires payment for access to protected resources (APIs, content, data, etc.) +- **Client**: Any application or agent that requests access to protected resources +- **Facilitator**: A service that handles payment verification and blockchain settlement + +## 4. Response Types + +The x402 protocol defines standard response types with specific semantics: + +- **Success**: Request successful, payment verified and settled +- **Payment Required**: Payment required to access the resource +- **Invalid Request**: Invalid payment payload or payment requirements +- **Server Error**: Server error during payment processing + +Transport-specific implementations map these response types to appropriate transport mechanisms (e.g., HTTP status codes, JSON-RPC error codes, etc.). + +## 5. Types + +This section defines the core data structures used in the x402 protocol. These are completely independent of both transport mechanism and payment scheme. All transports and schemes use these exact data structures, differing only in how they represent them (transport layer) and what validation/settlement logic they apply (scheme layer). + +### 5.1 PaymentRequired Schema + +#### 5.1.1 JSON Payload + +When a resource server requires payment, it responds with a payment required signal containing the `PaymentRequired` object. The transport defines where this object is carried. For HTTP, the canonical wire location is the base64-encoded `PAYMENT-REQUIRED` response header, see [HTTP Payment Challenge](./transports-v2/http.md#3-payment-challenge). + +Example `PaymentRequired` object: + +```json +{ + "x402Version": 2, + "error": "PAYMENT-SIGNATURE header is required", + "resource": { + "url": "https://api.example.com/premium-data", + "description": "Access to premium market data", + "mimeType": "application/json", + "serviceName": "Example Market Data", + "tags": ["market-data", "finance"], + "iconUrl": "https://api.example.com/icon.png" + }, + "accepts": [ + { + "scheme": "exact", + "network": "eip155:84532", + "amount": "10000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 60, + "extra": { + "name": "USDC", + "version": "2" + } + } + ], + "extensions": {} +} +``` + +#### 5.1.2 Field Descriptions + +The `PaymentRequired` schema contains the following fields: + +| Field Name | Type | Required | Description | +| ------------- | -------- | -------- | ---------------------------------------------------------------------- | +| `x402Version` | `number` | Required | Protocol version identifier (must be 2) | +| `error` | `string` | Optional | Human-readable error message explaining why payment is required | +| `resource` | `object` | Required | ResourceInfo object describing the protected resource | +| `accepts` | `array` | Required | Array of payment requirement objects defining acceptable payment methods | +| `extensions` | `object` | Optional | Protocol extensions data | + +Each `PaymentRequirements` object in the `accepts` array contains: + +| Field Name | Type | Required | Description | +| ------------------- | -------- | -------- |---------------------------------------------------------------------------------------------------------------------------| +| `scheme` | `string` | Required | Payment scheme identifier (e.g., "exact") | +| `network` | `string` | Required | Blockchain network identifier in CAIP-2 format (e.g., "eip155:84532") | +| `amount` | `string` | Required | Required payment amount in atomic token units | +| `asset` | `string` | Required | Token contract address or ISO 4217 currency code for fiat | +| `payTo` | `string` | Required | Recipient wallet address or role constant (e.g., "merchant") | +| `maxTimeoutSeconds` | `number` | Required | Maximum time allowed for payment completion | +| `extra` | `object` | Optional | Scheme-specific additional information | + +The `ResourceInfo` object contains: + +| Field Name | Type | Required | Description | +| --------------- | --------------- | -------- | -------------------------------------------------------------------------------------------------------------------- | +| `url` | `string` | Required | URL of the protected resource | +| `description` | `string` | Optional | Human-readable description of the resource | +| `mimeType` | `string` | Optional | MIME type of the expected response | +| `serviceName` | `string` | Optional | Human-readable name of the service hosting the resource. Printable ASCII, max 32 characters. | +| `tags` | `array[string]` | Optional | Topical tags for the service, used for discovery filtering. Max 5 entries; each printable ASCII, max 32 characters. | +| `iconUrl` | `string` | Optional | Absolute `https`/`http` URL to an icon representing the service. Max 2048 characters. | + +The `Extensions` object is a key-value map where each key is an extension identifier. Most +server-declared extensions use this structure: + +| Field Name | Type | Required | Description | +| ---------- | -------- | -------- | -------------------------------------------------------- | +| `info` | `object` | Usually | Extension-specific data provided by the server | +| `schema` | `object` | Usually | JSON Schema defining the client-extensible `info` fields | + +Extensions enable modular optional functionality beyond core payment mechanics. Servers advertise +supported extensions in `PaymentRequired`; participating clients echo the declaration in +`PaymentPayload` and may append fields without overwriting server-advertised values. An individual +extension specification may define additional top-level fields or transport-specific placement. + +### 5.2 PaymentPayload Schema + +#### 5.2.1 JSON Structure + +The client includes payment authorization as JSON in the payment payload field: + +```json +{ + "x402Version": 2, + "resource": { + "url": "https://api.example.com/premium-data", + "description": "Access to premium market data", + "mimeType": "application/json" + }, + "accepted": { + "scheme": "exact", + "network": "eip155:84532", + "amount": "10000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 60, + "extra": { + "name": "USDC", + "version": "2" + } + }, + "payload": { + "signature": "0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a1283259764173608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3d6293af148b571c", + "authorization": { + "from": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "value": "10000", + "validAfter": "1740672089", + "validBefore": "1740672154", + "nonce": "0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480" + } + }, + "extensions": {} +} +``` + +#### 5.2.2 Field Descriptions + +The `PaymentPayload` schema contains the following fields: + +| Field Name | Type | Required | Description | +| ------------- | -------- | -------- | ------------------------------------------------------------------- | +| `x402Version` | `number` | Required | Protocol version identifier | +| `resource` | `object` | Optional | ResourceInfo object describing the resource being accessed | +| `accepted` | `object` | Required | PaymentRequirements object indicating the payment method chosen | +| `payload` | `object` | Required | Scheme-specific payment data | +| `extensions` | `object` | Optional | Protocol extensions data | + +The `accepted` field contains a `PaymentRequirements` object (see section 5.1.2). + +The `payload` field contains scheme-specific data. For example, with exact EVM scheme, this includes: + +| Field Name | Type | Required | Description | +| --------------- | -------- | -------- | ----------------------------------- | +| `signature` | `string` | Required | EIP-712 signature for authorization | +| `authorization` | `object` | Required | EIP-3009 authorization parameters | + +The `Authorization` object contains the following fields: + +| Field Name | Type | Required | Description | +| ------------- | -------- | -------- | ----------------------------------------------- | +| `from` | `string` | Required | Payer's wallet address | +| `to` | `string` | Required | Recipient's wallet address | +| `value` | `string` | Required | Payment amount in atomic units | +| `validAfter` | `string` | Required | Unix timestamp when authorization becomes valid | +| `validBefore` | `string` | Required | Unix timestamp when authorization expires | +| `nonce` | `string` | Required | 32-byte random nonce to prevent replay attacks | + +### 5.3 SettlementResponse Schema + +#### 5.3.1 JSON Structure + +After payment settlement, the server includes transaction details in the payment response field as JSON: + +```json +{ + "success": true, + "transaction": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "network": "eip155:84532", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66" +} +``` + +#### 5.3.2 Field Descriptions + +The `SettleResponse` schema contains the following fields: + +| Field Name | Type | Required | Description | +| ------------- | --------- | -------- | --------------------------------------------------------------------- | +| `success` | `boolean` | Required | Indicates whether the payment settlement was successful | +| `errorReason` | `string` | Optional | Error reason if settlement failed (omitted if successful) | +| `errorMessage`| `string` | Optional | Human-readable failure detail | +| `payer` | `string` | Optional | Address of the payer's wallet | +| `transaction` | `string` | Required | Blockchain transaction hash (empty string if settlement failed) | +| `network` | `string` | Required | Blockchain network identifier in CAIP-2 format | +| `amount` | `string` | Optional | The actual amount settled in atomic units (omitted if not applicable) | +| `extensions` | `object` | Optional | Protocol extensions data | +| `extra` | `object` | Optional | Scheme-specific settlement data | + +### 5.4 VerifyResponse Schema + + +#### 5.4.1 Field Descriptions + +The `VerifyResponse` schema contains the following fields: + +| Field Name | Type | Required | Description | +| --------------- | --------- | -------- | ------------------------------------------------------- | +| `isValid` | `boolean` | Required | Indicates whether the payment authorization is valid | +| `invalidReason` | `string` | Optional | Reason for invalidity (omitted if valid) | +| `invalidMessage`| `string` | Optional | Human-readable invalidity detail | +| `payer` | `string` | Optional | Address of the payer's wallet | +| `extensions` | `object` | Optional | Extension-specific verification results | +| `extra` | `object` | Optional | Scheme-specific additional data | + +## 6. Payment Schemes + +This section describes the payment schemes supported by the x402 protocol. Payment schemes define how payments are formed, validated, and settled on specific payment networks. Schemes are independent of the underlying transport mechanism. + +Each scheme defines: + +- How to construct the `payload` field within `PaymentPayload` +- Settlement and validation procedures +- Scheme-specific requirements in the `extra` field of `PaymentRequirements` + +### 6.1 Implemented Scheme Matrix + +| Scheme | Semantics | EVM | TRON | +| --- | --- | --- | --- | +| `exact` | Settle the advertised amount for one request | EIP-3009 or Permit2 | TIP-712 TransferWithAuthorization or Permit2 | +| `upto` | Authorize a maximum and settle actual usage | Permit2 | Permit2 | +| `batch-settlement` | Deposit once, authorize requests with cumulative vouchers, claim later | Payment channel | Payment channel | +| `auth-capture` | Authorize, capture, void, refund, or reclaim | Commerce Payments | Not implemented | +| `exact_gasfree` | Relay an exact TRC-20 payment without payer TRX | Not implemented | GasFreeController permit | + +### 6.2 Scheme Invariants + +Every network binding MUST validate the protocol version, scheme, network, asset, recipient, amount +semantics, authorization time bounds, and signature before settlement. A facilitator MUST re-verify +the authorization immediately before any state-changing action. Network bindings MUST provide +on-chain or cryptographic replay protection. + +Detailed rules are defined in: + +- [`exact`](schemes/exact/scheme_exact.md) +- [`upto`](schemes/upto/scheme_upto.md) +- [`batch-settlement`](schemes/batch-settlement/scheme_batch_settlement.md) +- [`auth-capture`](schemes/auth-capture/scheme_auth_capture.md) +- [`exact_gasfree`](schemes/exact-gasfree/scheme_exact_gasfree.md) + +## 7. Facilitator Interface + +The facilitator provides HTTP REST APIs for payment verification and settlement. This allows resource servers to delegate blockchain operations to trusted third parties or host the endpoints themselves. Note that while the core x402 protocol is transport-agnostic, facilitator APIs are currently standardized as HTTP endpoints. + +### 7.1 POST /verify + +Verifies a payment authorization without executing the transaction on the blockchain. + +**Request (Exact Scheme):** + +```jsonc +{ + "x402Version": 2, + "paymentPayload": { + /* PaymentPayload schema */ + }, + "paymentRequirements": { + /* PaymentRequirements schema */ + } +} +``` + +Example with actual data: + +```json +{ + "x402Version": 2, + "paymentPayload": { + "x402Version": 2, + "resource": { + "url": "https://api.example.com/premium-data", + "description": "Access to premium market data", + "mimeType": "application/json" + }, + "accepted": { + "scheme": "exact", + "network": "eip155:84532", + "amount": "10000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 60, + "extra": { + "name": "USDC", + "version": "2" + } + }, + "payload": { + "signature": "0x...", + "authorization": { + "from": "0x...", + "to": "0x...", + "value": "10000", + "validAfter": "1740672089", + "validBefore": "1740672154", + "nonce": "0x..." + } + } + }, + "paymentRequirements": { + "scheme": "exact", + "network": "eip155:84532", + "amount": "10000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 60, + "extra": { + "name": "USDC", + "version": "2" + } + } +} +``` + +**Successful Response:** + +```json +{ + "isValid": true, + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66" +} +``` + +**Error Response:** + +```json +{ + "isValid": false, + "invalidReason": "insufficient_funds", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66" +} +``` + +### 7.2 POST /settle + +Executes a verified payment by broadcasting the transaction to the blockchain. + +**Request:** Same structure as `/verify` endpoint (contains `paymentPayload` and `paymentRequirements`). + +> **Note**: While the request structure is identical, some payment schemes may assign different semantics to fields at settlement time versus verification time. For example, in the `upto` scheme, the `amount` field in `paymentRequirements` represents the maximum authorized amount at verification time, but the actual amount to settle at settlement time. See individual scheme specifications for details. + +**Successful Response:** + +```json +{ + "success": true, + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "transaction": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "network": "eip155:84532" +} +``` + +**Error Response:** + +```json +{ + "success": false, + "errorReason": "insufficient_funds", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "transaction": "", + "network": "eip155:84532" +} +``` + +### 7.3 GET /supported + +Returns the list of payment schemes, networks, and extensions supported by the facilitator. + +**Response:** + +```json +{ + "kinds": [ + { + "x402Version": 2, + "scheme": "exact", + "network": "eip155:97", + "extra": { + "supportedAssetTransferMethods": ["eip3009", "permit2"] + } + }, + { + "x402Version": 2, + "scheme": "upto", + "network": "tron:0xcd8690dc", + "extra": { + "assetTransferMethod": "permit2", + "permit2FacilitatorAddress": "TFacilitatorAddress" + } + }, + { + "x402Version": 2, + "scheme": "exact_gasfree", + "network": "tron:0xcd8690dc" + } + ], + "extensions": [], + "signers": { + "eip155:*": ["0x1234567890abcdef1234567890abcdef12345678"], + "tron:*": ["TFacilitatorAddress"] + } +} +``` + +#### 7.3.1 SupportedResponse Fields + +| Field Name | Type | Required | Description | +| ------------ | -------- | -------- | ------------------------------------------------------------------------ | +| `kinds` | `array` | Required | Array of supported payment kind objects | +| `extensions` | `array` | Required | Array of extension identifiers the facilitator has implemented | +| `signers` | `object` | Required | Map of CAIP-2 family patterns (for example `eip155:*` or `tron:*`) to public signer addresses | + +Each `SupportedKind` object in the `kinds` array contains: + +| Field Name | Type | Required | Description | +| ------------- | -------- | -------- | ---------------------------------------------------------- | +| `x402Version` | `number` | Required | Protocol version supported; this specification requires `2` | +| `scheme` | `string` | Required | Payment scheme identifier (e.g., "exact") | +| `network` | `string` | Required | Blockchain network identifier in CAIP-2 format | +| `extra` | `object` | Optional | Additional scheme-specific configuration | + +## 8. Discovery API + +The x402 protocol includes a discovery mechanism that allows clients to find and explore available x402-enabled resources. This enables the creation of marketplaces (known as "Bazaars") where users can discover and access monetized APIs and digital services. + +Discovery is currently implemented as HTTP REST APIs, though the discovered resources may use any x402-supported transport. + +### 8.1 GET /discovery/resources + +List discoverable x402 resources from the Bazaar. + +**Request Parameters:** + +| Parameter | Type | Required | Description | Default | +| --------- | -------- | -------- | ------------------------------------------- | ------- | +| `type` | `string` | Optional | Filter by resource type (e.g., "http") | - | +| `payTo` | `string` | Optional | Filter by payment recipient address | - | +| `scheme` | `string` | Optional | Filter by payment scheme (e.g., "exact") | - | +| `network` | `string` | Optional | Filter by payment network (e.g., "eip155:8453") | - | +| `extensions` | `string` | Optional | Filter by extension key present on each resource | - | +| `limit` | `number` | Optional | Maximum number of results to return (1-100) | 20 | +| `offset` | `number` | Optional | Number of results to skip for pagination | 0 | + +**Response:** + +```json +{ + "x402Version": 2, + "items": [ + { + "resource": "https://api.example.com/premium-data", + "type": "http", + "x402Version": 2, + "accepts": [ + { + "scheme": "exact", + "network": "eip155:84532", + "amount": "10000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 60, + "extra": { + "name": "USDC", + "version": "2" + } + } + ], + "lastUpdated": 1703123456, + "metadata": { + "category": "finance", + "provider": "Example Corp" + } + } + ], + "pagination": { + "limit": 10, + "offset": 0, + "total": 1 + } +} +``` + +### 8.2 GET /discovery/search + +Search semantics and response shape are defined in the Bazaar extension specification at +`specs/extensions/bazaar.md`, since this endpoint is extension-specific behavior. + +### 8.3 Discovered Resource Fields + +| Field Name | Type | Required | Description | +| ------------- | -------- | -------- | --------------------------------------------------------------- | +| `resource` | `string` | Required | The resource URL or identifier being monetized | +| `type` | `string` | Required | Resource type (currently "http" for HTTP endpoints) | +| `x402Version` | `number` | Required | Protocol version supported by the resource | +| `accepts` | `array` | Required | Array of PaymentRequirements objects specifying payment methods | +| `lastUpdated` | `number` | Required | Unix timestamp of when the resource was last updated | +| `extensions` | `object` | Optional | Additional extension payloads associated with this discovered resource | + +### 8.4 Bazaar Concept + +The Bazaar is a marketplace ecosystem where x402-enabled resources can be discovered and accessed. Key features: + +- **Resource Discovery**: Find APIs and services by category, provider, or payment requirements +- **Payment Transparency**: View pricing and payment methods upfront +- **Provider Information**: Learn about service providers and their offerings +- **Dynamic Updates**: Resources can be added, updated, or removed dynamically + +### 8.5 Example Usage + +```bash +# List financial data APIs +GET /discovery/resources?type=http&limit=10 + +# Search for weather APIs +GET /discovery/search?query=weather+APIs&type=http&limit=5 + +# Continue a paginated search (when server supports it) +GET /discovery/search?query=financial+data&limit=10&cursor=eyJwYWdlIjoyfQ== +``` + +## 9. Error Handling + +The x402 protocol defines standard error codes that may be returned by facilitators or resource servers. These error codes help clients understand why a payment failed and take appropriate action. + +- **`insufficient_funds`**: Client does not have enough tokens to complete the payment +- **`invalid_exact_evm_payload_authorization_valid_after`**: Payment authorization is not yet valid (before validAfter timestamp) +- **`invalid_exact_evm_payload_authorization_valid_before`**: Payment authorization has expired (after validBefore timestamp) +- **`invalid_exact_evm_payload_authorization_value_mismatch`**: Payment amount does not exactly match the required amount +- **`invalid_exact_evm_payload_signature`**: Payment authorization signature is invalid or improperly signed +- **`invalid_exact_evm_payload_recipient_mismatch`**: Recipient address does not match payment requirements +- **`invalid_network`**: Specified blockchain network is not supported +- **`invalid_payload`**: Payment payload is malformed or contains invalid data +- **`invalid_payment_requirements`**: Payment requirements object is invalid or malformed +- **`invalid_scheme`**: Specified payment scheme is not supported +- **`unsupported_scheme`**: Payment scheme is not supported by the facilitator +- **`invalid_x402_version`**: Protocol version is not supported +- **`invalid_transaction_state`**: Blockchain transaction failed or was rejected +- **`unexpected_verify_error`**: Unexpected error occurred during payment verification +- **`unexpected_settle_error`**: Unexpected error occurred during payment settlement + +## 10. Security Considerations + +### 10.1 Replay Attack Prevention + +The x402 protocol implements multiple layers of protection against replay attacks: + +- **Unique authorization values**: EIP-3009/TIP-712 flows use a 32-byte nonce; Permit2, GasFree, + payment-channel refunds, and vouchers use scheme-defined nonce or monotonic state +- **Blockchain protection**: settlement contracts consume authorizations, Permit2 nonces, channel + state, or GasFree account nonces so a successful authorization cannot be replayed +- **Time Constraints**: Authorizations have explicit valid time windows to limit their lifetime +- **Signature Verification**: All authorizations are cryptographically signed by the payer + +### 10.2 Authentication Integration + +The protocol supports wallet authentication through the optional +[`sign-in-with-x`](extensions/sign-in-with-x.md) extension. Payment authentication and resource +authentication remain separate concerns. + +## 11. Implementation Notes + +### 11.1 Network Identifiers + +Networks in x402 v2 use CAIP-2 (Chain Agnostic Improvement Proposal) format: `namespace:reference`. + +**Format:** `{namespace}:{reference}` (for example, `eip155:56` for BSC mainnet) + +Non-blockchain networks are encouraged to follow the CAIP-2 format (e.g., `ach:us`, `sepa:eu`). + +This repository's supported deployment profile uses: + +- **`eip155:56`**: BSC mainnet +- **`eip155:97`**: BSC testnet +- **`tron:0x2b6653dc`**: TRON mainnet +- **`tron:0xcd8690dc`**: TRON Nile testnet +- **`tron:0x94a9059e`**: TRON Shasta testnet + +An implementation MAY register additional EVM networks when the selected scheme's contracts and +assets are available. The TRON binding recognizes the three identifiers above unless the SDK is +extended with additional deployments. + +### 11.2 Supported Assets + +Token support varies by network: + +**EVM Networks:** +- ERC-20/BEP-20 tokens implementing EIP-3009 +- Other ERC-20/BEP-20 tokens supported through Permit2 and the scheme-specific proxy or deposit + collector + +**TRON:** +- Registered TRC-20 tokens supported through TIP-712 TransferWithAuthorization, Permit2, or GasFree +- The built-in registry includes USDT and USDD where deployed; exact availability is network-specific + +Token availability depends on facilitator service capabilities and network-specific deployments. + +## 12. Use Cases and Applications + +The x402 protocol enables diverse monetization scenarios across the internet. While the core protocol is HTTP-native and chain-agnostic, specific implementations can vary based on use case requirements. + +### 12.1 AI Agent Integration + +AI agents can use x402 to autonomously pay for resources and services. The protocol supports: + +- **Automatic payment handling** for resource access +- **Resource discovery** through facilitator services +- **Budget management** and spending controls (implementation-specific) +- **Correlation tracking** for operation grouping (implementation-specific) +- **Multi-transport support** allowing agents to work across HTTP APIs and MCP tools + +### 12.2 Human User Applications + +Applications can implement x402 for: + +- **Session-based access** (time-limited subscriptions) +- **Pay-per-use content** (articles, videos, downloads, tools) +- **Resource monetization** with per-call pricing +- **Authentication-based pricing** (discounted rates for verified users) +- **Cross-protocol payments** supporting web, desktop, and AI applications + +### 12.3 Transport Support + +x402 integrates across multiple transport layers: + +- **HTTP**: Web APIs and the TypeScript integrations for fetch, Axios, Express, Fastify, Hono, and Next.js +- **MCP (Model Context Protocol)**: AI agent tools and resources +- **Custom Protocols**: Any request-response based system can implement x402 payment flows + +### 12.4 Server Frameworks + +x402 integrates with popular frameworks: + +- **Express.js**: x402 payment middleware +- **Fastify**: x402 payment middleware +- **Hono**: Edge runtime support +- **Next.js**: server integration +- **MCP**: paid tool wrapper + +### 12.5 Client Libraries + +Clients across different transports can be enhanced with x402 payment capabilities: + +- **HTTP clients**: wrapped `fetch` and Axios +- **MCP clients**: x402-aware MCP client +- **Custom integrations**: Application-specific payment handling + +### 12.6 Advanced Patterns + +The protocol enables sophisticated monetization strategies: + +- **Dynamic pricing** based on user authentication or usage patterns +- **Session management** for time-based access control +- **Batch payments** for multiple resource access +- **Subscription models** built on micropayments + +_Note: Implementation details for specific patterns (such as budget management, correlation tracking, or session handling) are available in application notes and implementation guides. Transport-specific implementation details are covered in the transport specification documents._ + +--- + +## Version History + +| Version | Date | Changes | Author | +| ------- | ----------- | ----------------------------------------------------------------- | ------------------------- | +| v2.0-bankofai | 2026-08-12 | Aligned this profile with the TypeScript EVM/TRON implementation and HTTP/MCP transports | BANK OF AI | +| v2.0 | 2025-12-9 | Protocol v2: CAIP-2 networks, restructured PaymentPayload/Required, ResourceInfo separation, extensions support | x402 team | +| v0.2 | 2025-10-3 | Transport-agnostic redesign | Ethan Niser | +| v0.1 | 2025-8-29 | Initial draft | [derived from repository] |