Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions docs/extensions/builder-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Three parties each contribute an attribution code:
| `a` | Resource server | App code — identifies the application exposing the paid endpoint |
| `w` | Facilitator | Wallet code — identifies the facilitator settling the payment on-chain |
| `s` | Client, server, and/or facilitator | Service code(s) — attribution codes from the payment path; each party has its own dedicated reservation (`MAX_CLIENT_SERVICE_CODES`, `MAX_SERVER_SERVICE_CODES`, `MAX_FACILITATOR_SERVICE_CODES`) so declaring up to that amount never crowds out another party's codes |
| `m` | Facilitator (at settlement) | Settlement metadata — facilitator-authored key/value map supplied by the settling mechanism; keys must start with `x402` and values must be unsigned integers, strings, arrays, or maps with string keys |

These codes are CBOR-encoded as an [ERC-8021](https://eip.tools/eip/8021) Schema 2 suffix and appended to the settlement transaction calldata. Off-chain tools can parse the calldata to verify attribution.

Expand Down Expand Up @@ -337,7 +338,7 @@ const tx = await publicClient.getTransaction({ hash: txHash });
const attribution = parseBuilderCodeSuffixFromCalldata(tx.input);

console.log(attribution);
// { a: "bc_my_app", s: ["bc_my_client"], w: "bc_my_facilitator" }
// { a: "bc_my_app", s: ["bc_my_client"], w: "bc_my_facilitator", m: { x402Example: 7n } }
```

## ERC-8021 Schema 2 Calldata Format
Expand All @@ -349,7 +350,7 @@ The suffix appended to settlement calldata (reading from the end backwards):
| `ercMarker` | 16 bytes | Constant: `80218021802180218021802180218021` |
| `schemaId` | 1 byte | `0x02` for Schema 2 |
| `cborLength` | 2 bytes | Length of CBOR data (big-endian) |
| `cborData` | variable | CBOR-encoded map with `a`, `w`, and/or `s` fields |
| `cborData` | variable | CBOR-encoded map with `a`, `w`, `s`, and/or `m` fields |

Wire order: `[cborData][cborLength (2B)][schemaId (1B)][ercMarker (16B)]`

Expand Down Expand Up @@ -411,18 +412,25 @@ facilitator.registerExtension(
);
```

Reads `a` and `s` from the payment payload at settlement time, adds `w`, appends its own `serviceCode` to `s` (deduped), and encodes the ERC-8021 CBOR suffix. On v1 payloads, `a` is omitted from the suffix (the resource-server echo gate that validates `a` does not run on v1).
Reads `a` and `s` from the payment payload at settlement time, adds `w`, appends its own `serviceCode` to `s` (deduped), includes `m` when the settling mechanism supplies settlement metadata, and encodes the ERC-8021 CBOR suffix. On v1 payloads, `a` is omitted from the suffix (the resource-server echo gate that validates `a` does not run on v1).

The `m` field is written exclusively by the facilitator at settlement — it never appears in `PaymentRequired` or `PaymentPayload`, and the facilitator ignores any `m` found in a client payload. Keys inside `m` must start with `x402` and are defined by the scheme spec that emits them.

### Utilities

#### `encodeBuilderCodeSuffix(data)`

Encodes builder code fields as an ERC-8021 Schema 2 hex suffix.
Encodes builder code fields as an ERC-8021 Schema 2 hex suffix. Accepts `a`, `w`, `s`, and `m` fields. Throws when the resulting CBOR exceeds 65,535 bytes.

```typescript
import { encodeBuilderCodeSuffix } from "@x402/extensions/builder-code";

const suffix = encodeBuilderCodeSuffix({ a: "bc_my_app", w: "bc_my_fac", s: ["bc_my_client"] });
const suffix = encodeBuilderCodeSuffix({
a: "bc_my_app",
w: "bc_my_fac",
s: ["bc_my_client"],
m: { x402Example: 7n },
});
// Returns hex string to append to calldata
```

Expand All @@ -434,7 +442,8 @@ Parses ERC-8021 Schema 2 attribution from settlement transaction calldata.
import { parseBuilderCodeSuffixFromCalldata } from "@x402/extensions/builder-code";

const attribution = parseBuilderCodeSuffixFromCalldata(tx.input);
// Returns { a?, w?, s? } or null if no valid suffix found
// Returns { a?, w?, s?, m? } or null if no valid suffix found
// Unsigned integers inside `m` are returned as bigint
```

### Constants
Expand All @@ -452,6 +461,8 @@ import {
} from "@x402/extensions/builder-code";
```

Types exported include `BuilderCodeExtensionData`, `BuilderCodeSuffixData` (extends `BuilderCodeExtensionData` with `m?`), `SettlementMetadata`, `BuilderCodeFacilitatorConfig`, `BuilderCodeRequiredExtension`, and `DataSuffixContext`.

## Examples

Full working examples are available in the x402 repository:
Expand Down
7 changes: 4 additions & 3 deletions docs/schemes/overview.mdx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
title: "Payment Schemes"
description: "Choose between exact, upto, and batch-settlement payment schemes in x402."
description: "Choose between exact, upto, batch-settlement, and auth-capture payment schemes in x402."
---

Payment schemes define the payment semantics for a resource: `exact` is for fixed-price requests where the buyer authorizes the advertised amount, `upto` is for single-request usage-based billing where the buyer authorizes a maximum and the seller charges actual usage, and `batch-settlement` is for high-volume or repeated micropayments where per-request authorizations are accumulated against a reusable channel. Network implementations define how those semantics are encoded for EVM, SVM, TVM (TON), AVM, Stellar, Aptos, Hedera, Keeta, Concordium, NEAR, XRPL, and Bitcoin Lightning (`lnbtc`); see [SDK Features](/sdk-features) for current SDK support.
Payment schemes define the payment semantics for a resource: `exact` is for fixed-price requests where the buyer authorizes the advertised amount, `upto` is for single-request usage-based billing where the buyer authorizes a maximum and the seller charges actual usage, `batch-settlement` is for high-volume or repeated micropayments where per-request authorizations are accumulated against a reusable channel, and `auth-capture` is for refundable payments where funds are held in escrow and released (captured, voided, or refunded) after the resource is served. Network implementations define how those semantics are encoded for EVM, SVM, TVM (TON), AVM, Stellar, Aptos, Hedera, Keeta, Concordium, NEAR, XRPL, and Bitcoin Lightning (`lnbtc`); see [SDK Features](/sdk-features) for current SDK support.

### Schemes in a 402 Response

Expand Down Expand Up @@ -60,7 +60,7 @@ Every scheme declares which payment flows it supports per asset transfer method.

The server resolves the flow from the scheme's `paymentFlows` table using the `assetTransferMethod` in the payment requirements. When the resolved flow is not `authorization`, the server signals it to clients via `extra.paymentFlow` in the 402 response.

The `exact` scheme supports both `authorization` (default) and `upfront` flows on all networks. Servers opt in per route by setting `extra.paymentFlow: "upfront"` in the route's `accepts` config. The `upto` and `batch-settlement` schemes support `authorization` only.
The `exact` scheme supports both `authorization` (default) and `upfront` flows on all networks. Servers opt in per route by setting `extra.paymentFlow: "upfront"` in the route's `accepts` config. The `upto` and `batch-settlement` schemes support `authorization` only. The `auth-capture` scheme supports `escrow` (default) and `authorization` flows, selected per route via `extra.paymentFlow`.

### Specs

Expand All @@ -69,3 +69,4 @@ For scheme-level payload semantics, see the network-agnostic specifications. Eac
* [`exact` spec](https://github.com/x402-foundation/x402/blob/main/specs/schemes/exact/scheme_exact.md)
* [`upto` spec](https://github.com/x402-foundation/x402/blob/main/specs/schemes/upto/scheme_upto.md)
* [`batch-settlement` spec](https://github.com/x402-foundation/x402/blob/main/specs/schemes/batch-settlement/scheme_batch_settlement.md)
* [`auth-capture` spec](https://github.com/x402-foundation/x402/blob/main/specs/schemes/auth-capture/scheme_auth_capture_evm.md)
4 changes: 2 additions & 2 deletions docs/sdk-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ This page tracks which features are implemented in each SDK (TypeScript, Go, Pyt
| batch-settlement | evm | `eip3009` | ✅ | ✅ | ✅ |
| batch-settlement | evm | `permit2` | ✅ | ✅ | ✅ |
| batch-settlement | svm | - | ✅ | ✅ | ❌ |
| auth-capture | evm | `eip3009` | ✅ (client) | ✅ (client) | ❌ |
| auth-capture | evm | `permit2` | ✅ (client) | ✅ (client) | ❌ |
| auth-capture | evm | `eip3009` | ✅ | ✅ | ❌ |
| auth-capture | evm | `permit2` | ✅ | ✅ | ❌ |

## Extensions

Expand Down
4 changes: 3 additions & 1 deletion e2e/.env-local
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# Server payment addresses
SERVER_EVM_ADDRESS=
SERVER_SVM_ADDRESS=
# Optional batch-settlement / upto (see e2e/README.md)
# Optional: self-managed auth-capture sync routes (/auth-capture/evm/*). Leave unset for facilitator-delegated auth-capture routes.
SERVER_EVM_RECEIVER_AUTHORIZER_PRIVATE_KEY=
SERVER_SVM_RECEIVER_AUTHORIZER_PRIVATE_KEY=
SERVER_SVM_OPERATOR_PRIVATE_KEY=
Expand Down Expand Up @@ -46,6 +46,8 @@ CLIENT_CARDANO_MNEMONIC=

# Facilitator wallets (XRPL facilitator is keyless — no FACILITATOR_XRPL_*)
FACILITATOR_EVM_PRIVATE_KEY=
# Optional auth-capture custom operator allowlist (defaults to Base Sepolia ForwardingOperator)
# FACILITATOR_EVM_AUTH_CAPTURE_CUSTOM_OPERATORS=0x8FE415CdB559fBF5B235B81CC4F7a69684A274bb
FACILITATOR_SVM_PRIVATE_KEY=
FACILITATOR_SVM_BATCH_BINDING_STORE=
SVM_ARCHIVE_RPC_URL=
Expand Down
14 changes: 8 additions & 6 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ You do **not** need to edit `generic-server` / `generic-client` / `generic-facil

- **`env`** — map of env key → `{ required: boolean, roles: ["server"|"client"|"facilitator", ...] }`. Every key a role reads (including unprefixed ones like `TVM_PROVIDER` or `EVM_PERMIT2_ASSET`) is declared here; [`src/mechanisms.ts`](src/mechanisms.ts) has no hardcoded role override table. Prefix (`SERVER_` / `CLIENT_` / `FACILITATOR_`) is only a fallback for undeclared keys.
- **`testnet` / `mainnet`** — `name`, `caip2`, optional `rpcUrlDefault`, optional `permit2Asset`/`permit2AssetName`. RPC env is pure convention, not declared: an operator sets `${ID}_TESTNET_RPC_URL` / `${ID}_MAINNET_RPC_URL` (e.g. `EVM_TESTNET_RPC_URL`), and the harness injects it into every spawned component as `${ID}_RPC_URL`. Set `rpcUrlRequired: true` on a mode with no `rpcUrlDefault` and no free public endpoint at all (a network whose SDK has no built-in node default, unlike e.g. Hedera/Keeta) so the harness fails fast at startup — with the missing input key named in the same preflight list as other required env — instead of deep inside a scenario run. Network identity defaults (`${ID}_NETWORK`) fall back to catalog `testnet.caip2` via `resolveNetworkCaip2`.
- **`routes`** — one canonical definition per paid HTTP path: `scheme`, `sdks`, `assetTransferMethod`, `schemeOptions`, declared `extensions`, required `price`, and optional `settlementOverride`. Handlers always return `{ message: "Protected endpoint accessed successfully", timestamp }`. The loader injects `network` (the file id) — routes never declare it themselves.
- **`routes`** — one canonical definition per paid HTTP path: `scheme`, `sdks`, `assetTransferMethod`, `schemeOptions`, `schemeExtra`, declared `extensions`, required `price`, and optional `settlementOverride`. Handlers always return `{ message: "Protected endpoint accessed successfully", timestamp }`. The loader injects `network` (the file id) — routes never declare it themselves.

CI family selection ([`scripts/ci-select-families.sh`](scripts/ci-select-families.sh) → [`scripts/ci-select-families.ts`](scripts/ci-select-families.ts)) prints families whose catalog `required: true` keys are all set — no per-family hardcoding in the shell script.

Expand All @@ -28,13 +28,13 @@ Every SDK reads this same set of files. The harness ([`src/mechanisms.ts`](src/m

Resource servers resolve the same data at boot — payment middleware config **and** route handlers — through a per-language loader: [`servers/typescript/catalog.ts`](servers/typescript/catalog.ts), [`servers/python/catalog.py`](servers/python/catalog.py), [`servers/go/catalog.go`](servers/go/catalog.go). No framework entrypoint hardcodes a path, price, or extension; each loops over its resolved routes. The harness passes the catalog directory in `E2E_MECHANISMS_CATALOG`, and each loader falls back to walking up to `e2e/config/` so a server still runs standalone from its own directory.

Route support is **listed** on each route via `sdks`, never inferred from a cartesian product. Scheme **registration** stays in the language-root client/server modules and facilitator mains.
Route support is **listed** on each route via `sdks` (and `clientSdks` for SDKs that implement only the client role), never inferred from a cartesian product. Scheme **registration** stays in the language-root client/server modules and facilitator mains.

## Add a mechanism

Adding a paid route to every SDK that should serve it is a catalog edit:

1. **Define the route** — add an entry under `routes` in the relevant `config/mechanisms_<id>.json`, keyed by its path, with `scheme`, `sdks` (e.g. `["typescript", "go", "python"]`), and `price`. Add `extensions`, `schemeOptions`, or `settlementOverride` only where the route needs them.
1. **Define the route** — add an entry under `routes` in the relevant `config/mechanisms_<id>.json`, keyed by its path, with `scheme`, `sdks` (e.g. `["typescript", "go", "python"]`), and `price`. Add `extensions`, `schemeOptions`, `schemeExtra` (auth-capture deadlines/flow), or `settlementOverride` only where the route needs them. An SDK that implements only the client role goes in `clientSdks` instead; it then pairs with servers and facilitators from other SDKs but is never asked to serve the route.
2. **Register the scheme once per language**, if it is new: server module (`servers/<lang>/`), client module (`clients/<lang>/`), and the facilitator main.

Servers pick up the route, its `402` payment requirements, and its handler with no per-framework edit. A surface that serves less than its SDK’s list can declare the narrowing in a local `test.config.json` (`excludeSchemes` / `excludeNetworks`); the harness applies it to the derived endpoints and forwards it to the server process (`E2E_EXCLUDE_SCHEMES` / `E2E_EXCLUDE_NETWORKS`), so declared and mounted routes cannot diverge.
Expand Down Expand Up @@ -70,7 +70,7 @@ These keep local `test.config.json` overlays and/or special orchestration — no
| Swig smart wallet | Client overlay [`clients/typescript/http/svm-smart-wallet/test.config.json`](clients/typescript/http/svm-smart-wallet/test.config.json) (`protocolFamilies`, `facilitators`, Swig env) + [`scripts/swig-setup.ts`](scripts/swig-setup.ts); uses catalog route `/exact/svm` |
| Legacy (v1) | `legacy/` trees only — separate configs; do not extend the mechanisms catalog for v1 |

If an SDK implements a route end-to-end (client + server + facilitator), list it in that route’s `sdks`. Omit only when the mechanism package is missing (e.g. Go has no TVM; Python/Go have no AVM/NEAR/XRPL; Python has no SVM upto).
If an SDK implements a route end-to-end (client + server + facilitator), list it in that route’s `sdks`. Omit only when the mechanism package is missing (e.g. Go has no TVM; Python/Go have no AVM/NEAR/XRPL; Python has no SVM upto). Where an SDK has only a client, list it in `clientSdks`.

## Legacy

Expand Down Expand Up @@ -136,7 +136,7 @@ Launches an interactive CLI where you can select:
- **Clients** - Payment-capable HTTP clients (axios, fetch, httpx, requests, etc.)
- **Extensions** - Additional features like Bazaar discovery
- **Protocols** - EVM, SVM, AVM, Aptos, Concordium, Hedera, NEAR, Stellar, and/or TVM networks
- **Payment schemes** (when multiple apply) - `exact`, `upto`, or `batch-settlement`
- **Payment schemes** (when multiple apply) - `exact`, `upto`, `batch-settlement`, or `auth-capture`
- **Payment flows** (when multiple apply) - `authorization`, `upfront`, or `escrow`
- **Asset transfer methods** (when multiple apply) - `eip3009`, `permit2`, `sequence`, or `ticketSequence`

Expand Down Expand Up @@ -302,9 +302,11 @@ Optional environment variables (batch-settlement scheme):

```bash
# EVM
SERVER_EVM_RECEIVER_AUTHORIZER_PRIVATE_KEY=0x... # optional: self-managed receiver authorizer (omit to delegate to facilitator /supported)
SERVER_EVM_RECEIVER_AUTHORIZER_PRIVATE_KEY=0x... # self-managed auth-capture sync (/auth-capture/evm/*) and optional batch-settlement receiver authorizer; omit for facilitator-delegated auth-capture routes
CLIENT_EVM_BATCH_SETTLEMENT_VOUCHER_SIGNER_PRIVATE_KEY=0x... # EOA the client uses to sign vouchers
EVM_BATCH_SETTLEMENT_RECOVERY=true # test client state-loss recovery scenario (default: true)
FACILITATOR_EVM_AUTH_CAPTURE_CUSTOM_OPERATORS=0x8FE4... # optional comma-separated custom operator allowlist (defaults to ForwardingOperator on Base Sepolia)
# Auth-capture: SERVER_EVM_RECEIVER_AUTHORIZER_PRIVATE_KEY is required for /auth-capture/evm/* (server-signed sync capture). The facilitator-authorizer, deferred, and custom-forwarding routes always run in the same suite; deferred capture is triggered by the harness after the client GET. With the server key set you get full auth-capture coverage (5 EVM paths); without it only the three facilitator-delegated paths run (/auth-capture/evm/* are omitted via requiresEnv).

# SVM
SERVER_SVM_RECEIVER_AUTHORIZER_PRIVATE_KEY=... # required for /upto/svm and /batch-settlement/svm; signs upto vouchers and the batch receiver authorizer (no SOL required)
Expand Down
12 changes: 7 additions & 5 deletions e2e/clients/go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import (

x402 "github.com/x402-foundation/x402/go/v2"
x402http "github.com/x402-foundation/x402/go/v2/http"
authcaptureclient "github.com/x402-foundation/x402/go/v2/mechanisms/evm/auth-capture/client"
batchedclient "github.com/x402-foundation/x402/go/v2/mechanisms/evm/batch-settlement/client"
batchsvmclient "github.com/x402-foundation/x402/go/v2/mechanisms/svm/batch-settlement/client"
exactevm "github.com/x402-foundation/x402/go/v2/mechanisms/evm/exact/client"
exactevmv1 "github.com/x402-foundation/x402/go/v2/mechanisms/evm/exact/v1/client"
uptoevm "github.com/x402-foundation/x402/go/v2/mechanisms/evm/upto/client"
svmconfig "github.com/x402-foundation/x402/go/v2/mechanisms/svm"
batchsvmclient "github.com/x402-foundation/x402/go/v2/mechanisms/svm/batch-settlement/client"
svm "github.com/x402-foundation/x402/go/v2/mechanisms/svm/exact/client"
svmv1 "github.com/x402-foundation/x402/go/v2/mechanisms/svm/exact/v1/client"
uptosvm "github.com/x402-foundation/x402/go/v2/mechanisms/svm/upto/client"
Expand Down Expand Up @@ -51,9 +52,9 @@ type SettleResponseExtractor interface {

// ClientContext holds a configured payment-capable HTTP client for e2e runs.
type ClientContext struct {
URL string
HTTPClient *http.Client
Settle SettleResponseExtractor
URL string
HTTPClient *http.Client
Settle SettleResponseExtractor
BatchedEvmScheme *batchedclient.BatchSettlementEvmScheme
BatchedSvmScheme *batchsvmclient.BatchSvmScheme
BatchPhase string
Expand All @@ -64,7 +65,7 @@ type ClientContext struct {
// Transports that speak something other than plain HTTP (e.g. MCP) wrap
// Client themselves instead of going through Newx402HTTPClient.
type PaymentClientContext struct {
Client *x402.X402Client
Client *x402.X402Client
BatchedEvmScheme *batchedclient.BatchSettlementEvmScheme
BatchedSvmScheme *batchsvmclient.BatchSvmScheme
BatchPhase string
Expand Down Expand Up @@ -129,6 +130,7 @@ func BuildPaymentClient() *PaymentClientContext {
Register(evmPattern, exactevm.NewExactEvmScheme(evmSigner, evmConfig)).
Register(evmPattern, uptoevm.NewUptoEvmScheme(evmSigner, uptoConfig)).
Register(evmPattern, batchedEvmScheme).
Register(evmPattern, authcaptureclient.NewAuthCaptureEvmScheme(evmSigner)).
RegisterV1("base-sepolia", exactevmv1.NewExactEvmSchemeV1(evmSigner)).
RegisterV1("base", exactevmv1.NewExactEvmSchemeV1(evmSigner))
}
Expand Down
Loading
Loading