From b0ffef3ffac1d942bbdcf5a2514ac9076641ea3a Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:47:54 +0000 Subject: [PATCH 01/37] Update SDK docs: add Linea/SKALE Base chains, fix API to match @x402r/sdk Generated-By: mintlify-agent --- sdk/arbiter/ai-integration.mdx | 12 +++---- sdk/arbiter/batch-operations.mdx | 8 ++--- sdk/arbiter/decision-submission.mdx | 4 +-- sdk/arbiter/quickstart.mdx | 11 ++----- sdk/arbiter/registry.mdx | 17 +++------- sdk/client/escrow-management.mdx | 4 +-- sdk/client/quickstart.mdx | 51 +++++++++++++++++------------ sdk/concepts.mdx | 22 +++++++------ sdk/deploy-operator.mdx | 2 ++ sdk/helpers/refundable.mdx | 12 ++++--- sdk/installation.mdx | 26 ++++++--------- sdk/limitations.mdx | 15 +++++---- sdk/merchant/payment-operations.mdx | 8 ++--- sdk/merchant/quickstart.mdx | 19 ++++------- sdk/merchant/refund-handling.mdx | 14 ++++---- sdk/merchant/subscriptions.mdx | 6 ++-- sdk/overview.mdx | 16 ++++----- 17 files changed, 117 insertions(+), 130 deletions(-) diff --git a/sdk/arbiter/ai-integration.mdx b/sdk/arbiter/ai-integration.mdx index 333b400..2db5757 100644 --- a/sdk/arbiter/ai-integration.mdx +++ b/sdk/arbiter/ai-integration.mdx @@ -68,8 +68,8 @@ interface DecisionResult { Use `createWebhookHandler` to connect your evaluation function to the arbiter: ```typescript -import { createWebhookHandler } from '@x402r/arbiter'; -import type { CaseEvaluationContext, DecisionResult } from '@x402r/arbiter'; +import { createWebhookHandler } from '@x402r/sdk'; +import type { CaseEvaluationContext, DecisionResult } from '@x402r/sdk'; const handler = createWebhookHandler({ arbiter, @@ -96,8 +96,8 @@ Setting `autoSubmitDecision: true` calls `approveRefundRequest` or `denyRefundRe ```typescript interface WebhookHandlerConfig { - /** X402rArbiter instance */ - arbiter: X402rArbiter; + /** Arbiter client instance */ + arbiter: ArbiterClient; /** Your evaluation function */ evaluationHook: ArbiterHook; @@ -127,8 +127,8 @@ interface WebhookResult extends DecisionResult { The most common pattern combines `watchNewCases` with the webhook handler to automatically evaluate incoming refund requests: ```typescript -import { X402rArbiter, createWebhookHandler } from '@x402r/arbiter'; -import type { CaseEvaluationContext, DecisionResult, RefundRequestEventLog } from '@x402r/arbiter'; +import { createArbiterClient, createWebhookHandler } from '@x402r/sdk'; +import type { CaseEvaluationContext, DecisionResult } from '@x402r/sdk'; import { PaymentState, RequestStatus } from '@x402r/core'; import type { PaymentInfo } from '@x402r/core'; diff --git a/sdk/arbiter/batch-operations.mdx b/sdk/arbiter/batch-operations.mdx index 7f32132..7b86119 100644 --- a/sdk/arbiter/batch-operations.mdx +++ b/sdk/arbiter/batch-operations.mdx @@ -76,12 +76,12 @@ The `nonce` identifies which specific charge record the refund request targets. Fetch all pending cases, evaluate each one, then batch approve and deny: ```typescript -import { X402rArbiter } from '@x402r/arbiter'; +import { createArbiterClient } from '@x402r/sdk'; import { RequestStatus } from '@x402r/core'; import type { PaymentInfo, RefundRequestData } from '@x402r/core'; async function triageAndProcess( - arbiter: X402rArbiter, + arbiter: ReturnType, receiverAddress: `0x${string}`, lookupPaymentInfo: (hash: `0x${string}`) => Promise ) { @@ -130,11 +130,11 @@ function shouldApprove(request: RefundRequestData): boolean { After batch approving, execute refunds individually for each approved payment: ```typescript -import { X402rArbiter } from '@x402r/arbiter'; +import { createArbiterClient } from '@x402r/sdk'; import type { PaymentInfo } from '@x402r/core'; async function batchApproveAndExecute( - arbiter: X402rArbiter, + arbiter: ReturnType, items: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> ) { // Step 1: Batch approve all items diff --git a/sdk/arbiter/decision-submission.mdx b/sdk/arbiter/decision-submission.mdx index 696479e..07a98c1 100644 --- a/sdk/arbiter/decision-submission.mdx +++ b/sdk/arbiter/decision-submission.mdx @@ -176,11 +176,11 @@ if (frozen) { This example shows the full arbiter workflow: fetching pending cases, reviewing them, making a decision, and executing the refund. ```typescript -import { X402rArbiter } from '@x402r/arbiter'; +import { createArbiterClient } from '@x402r/sdk'; import { RequestStatus } from '@x402r/core'; import type { PaymentInfo } from '@x402r/core'; -async function processAllPendingCases(arbiter: X402rArbiter, receiverAddress: `0x${string}`) { +async function processAllPendingCases(arbiter: ReturnType, receiverAddress: `0x${string}`) { // Step 1: Get all pending refund requests const { keys, total } = await arbiter.getPendingRefundRequests(0n, 50n, receiverAddress); console.log(`Found ${total} pending cases`); diff --git a/sdk/arbiter/quickstart.mdx b/sdk/arbiter/quickstart.mdx index dc24b32..fb920ba 100644 --- a/sdk/arbiter/quickstart.mdx +++ b/sdk/arbiter/quickstart.mdx @@ -8,22 +8,17 @@ icon: "rocket" The Arbiter SDK is experimental. The dispute resolution system design is actively evolving. -The `@x402r/arbiter` package provides methods for arbiters to resolve disputes: reviewing refund requests, approving or denying them, and executing refunds. +The `@x402r/sdk` package provides methods for arbiters to resolve disputes: reviewing refund requests, approving or denying them, and executing refunds. ## Setup ```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { getNetworkConfig } from '@x402r/core'; +import { createArbiterClient } from '@x402r/sdk'; -const config = getNetworkConfig('eip155:84532')!; - -const arbiter = new X402rArbiter({ +const arbiter = createArbiterClient({ publicClient, walletClient, operatorAddress: '0x...', - refundRequestAddress: config.refundRequest, - arbiterRegistryAddress: config.arbiterRegistry, }); ``` diff --git a/sdk/arbiter/registry.mdx b/sdk/arbiter/registry.mdx index 1855c8d..06233e0 100644 --- a/sdk/arbiter/registry.mdx +++ b/sdk/arbiter/registry.mdx @@ -11,17 +11,12 @@ The ArbiterRegistry is an on-chain contract that allows arbiters to register the To use registry methods, provide the `arbiterRegistryAddress` when creating the arbiter instance: ```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { getNetworkConfig } from '@x402r/core'; +import { createArbiterClient } from '@x402r/sdk'; -const config = getNetworkConfig('eip155:84532')!; - -const arbiter = new X402rArbiter({ +const arbiter = createArbiterClient({ publicClient, walletClient, operatorAddress: '0x...', - refundRequestAddress: config.refundRequest, - arbiterRegistryAddress: config.arbiterRegistry, }); ``` @@ -120,17 +115,13 @@ interface ArbiterList { ## Complete Example ```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { getNetworkConfig } from '@x402r/core'; +import { createArbiterClient } from '@x402r/sdk'; async function main() { - const config = getNetworkConfig('eip155:84532')!; - - const arbiter = new X402rArbiter({ + const arbiter = createArbiterClient({ publicClient, walletClient, operatorAddress: '0x...', - arbiterRegistryAddress: config.arbiterRegistry, }); // Register diff --git a/sdk/client/escrow-management.mdx b/sdk/client/escrow-management.mdx index 5f86fd3..ad499a9 100644 --- a/sdk/client/escrow-management.mdx +++ b/sdk/client/escrow-management.mdx @@ -151,11 +151,11 @@ The escrow period length is configured at the contract level when the `EscrowPer A common pattern is to freeze a payment before submitting a refund request, ensuring the merchant cannot release funds while the request is pending. ```typescript -import { X402rClient } from '@x402r/client'; +import { createPayerClient } from '@x402r/sdk'; import type { PaymentInfo } from '@x402r/core'; async function freezeAndRequestRefund( - client: X402rClient, + client: ReturnType, paymentInfo: PaymentInfo, freezeAddress: `0x${string}`, refundAmount: bigint diff --git a/sdk/client/quickstart.mdx b/sdk/client/quickstart.mdx index f451a5f..bd41021 100644 --- a/sdk/client/quickstart.mdx +++ b/sdk/client/quickstart.mdx @@ -8,22 +8,20 @@ icon: "rocket" The Client SDK is experimental. APIs will change as the refund and dispute system design evolves. -The `@x402r/client` package provides payer-side methods for interacting with X402r payments: requesting refunds, freezing payments, and querying escrow state. +The `@x402r/sdk` package provides payer-side methods for interacting with X402r payments: requesting refunds, freezing payments, and querying escrow state. ## Setup ```typescript -import { X402rClient } from '@x402r/client'; -import { getNetworkConfig } from '@x402r/core'; +import { createPayerClient } from '@x402r/sdk'; +import { getChainConfig } from '@x402r/core'; -const networkConfig = getNetworkConfig('eip155:84532')!; +const chainConfig = getChainConfig(84532); // Base Sepolia -const client = new X402rClient({ +const client = createPayerClient({ publicClient, walletClient, operatorAddress: '0x...', // Your PaymentOperator address - refundRequestAddress: networkConfig.refundRequest, - escrowAddress: networkConfig.authCaptureEscrow, }); ``` @@ -31,20 +29,31 @@ const client = new X402rClient({ The Client SDK currently supports: -- **`requestRefund()`** — Submit a refund request for a payment in escrow -- **`getRefundStatus()`** — Check the status of a refund request -- **`freezePayment()`** — Freeze a payment to prevent release during a dispute -- **`isFrozen()`** — Check if a payment is frozen -- **`isDuringEscrowPeriod()`** — Check if a payment is still in its escrow window -- **`getAuthorizationTime()`** — Get when a payment was authorized -- **`getPaymentState()`** — Derive the lifecycle state of a payment -- **`paymentExists()`** — Check if a payment has been authorized -- **`isInEscrow()`** — Check if a payment has capturable funds -- **`getPaymentDetails()`** — Retrieve full PaymentInfo from event logs -- **`getPayerPayments()`** — List all payments for the connected wallet -- **`submitEvidence()`** — Attach evidence (IPFS CID) to a refund request -- **`getEvidence()`** — Retrieve a single evidence entry by index -- **`getAllEvidence()`** — Retrieve all evidence for a refund request +**Payment actions:** +- **`authorize()`** — Authorize a payment with escrow +- **`charge()`** — Charge a payment directly +- **`release()`** — Release escrowed funds +- **`refundInEscrow()`** — Refund funds while in escrow +- **`getState()`** — Derive the lifecycle state of a payment +- **`getAmounts()`** — Get capturable and refundable amounts + +**Refund actions** (requires `refundRequestAddress` in config): +- **`refund.request()`** — Submit a refund request +- **`refund.cancel()`** — Cancel a pending refund request +- **`refund.get()`** — Get full refund request data +- **`refund.getStatus()`** — Check refund request status +- **`refund.has()`** — Check if a refund request exists +- **`refund.getPayerRequests()`** — List refund requests by payer + +**Escrow actions** (requires `escrowPeriodAddress` via extend or config): +- **`escrow.isDuringEscrow()`** — Check if payment is in escrow window +- **`escrow.getAuthorizationTime()`** — Get when a payment was authorized +- **`escrow.getDuration()`** — Get the escrow period duration + +**Watch actions:** +- **`watch.onPayment()`** — Watch for payment events +- **`watch.onRefundRequest()`** — Watch for refund request events +- **`watch.onRefundExecuted()`** — Watch for refund execution events ## Try It Now diff --git a/sdk/concepts.mdx b/sdk/concepts.mdx index 3b66c61..75af554 100644 --- a/sdk/concepts.mdx +++ b/sdk/concepts.mdx @@ -52,7 +52,7 @@ interface PaymentInfo { ``` -Use `computePaymentInfoHash()` from `@x402r/core` to compute the unique hash of a payment. Use `parsePaymentInfo()` to deserialize a JSON PaymentInfo back into the typed struct. +Use `computePaymentInfoHash()` from `@x402r/core` to compute the unique hash of a payment. Use `toPaymentInfo()` to deserialize a JSON PaymentInfo back into the typed struct. ## Escrow Period @@ -64,13 +64,15 @@ The **EscrowPeriod** contract tracks when a payment was authorized and enforces - **After the period** — merchants can release funds to themselves ```typescript -import { X402rClient } from '@x402r/client'; +import { createPayerClient } from '@x402r/sdk'; -// Check when payment was authorized -const authTime = await client.getAuthorizationTime(paymentInfo, escrowPeriodAddress); +const client = createPayerClient({ publicClient, walletClient, operatorAddress }); + +// Check when payment was authorized (requires escrow plugin) +const authTime = await client.escrow.getAuthorizationTime(paymentInfo); // Check if still within escrow period -const inEscrow = await client.isDuringEscrowPeriod(paymentInfo, escrowPeriodAddress); +const inEscrow = await client.escrow.isDuringEscrow(paymentInfo); if (!inEscrow) { console.log('Escrow period has passed - funds can be released'); } @@ -130,14 +132,14 @@ const status = await client.getRefundStatus(paymentInfo, 0n); The **Freeze** contract allows payers to freeze a payment during the escrow period, preventing release until the freeze expires or is lifted: ```typescript -// Payer freezes payment (requires payer authorization) -await client.freezePayment(paymentInfo, freezeAddress); +// Payer freezes payment (requires freeze plugin) +await client.freeze.freeze(paymentInfo); -// Merchant unfreezes payment (requires receiver authorization) -await merchant.unfreezePayment(paymentInfo, freezeAddress); +// Merchant unfreezes payment (requires freeze plugin) +await merchant.freeze.unfreeze(paymentInfo); // Check frozen status -const frozen = await client.isFrozen(paymentInfo, freezeAddress); +const frozen = await client.freeze.isFrozen(paymentInfo); ``` diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index bafe608..b144085 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -210,6 +210,8 @@ Deployment is supported on all configured networks: | Avalanche | 43114 | `eip155:43114` | | Celo | 42220 | `eip155:42220` | | Monad | 143 | `eip155:143` | +| Linea | 59144 | `eip155:59144` | +| SKALE Base | 1187947933 | `eip155:1187947933` | Deployment requires gas fees. Ensure your wallet has ETH on the target network. On Base Sepolia, you can get testnet ETH from [Base network faucets](https://docs.base.org/base-chain/tools/network-faucets). diff --git a/sdk/helpers/refundable.mdx b/sdk/helpers/refundable.mdx index 333841c..039399a 100644 --- a/sdk/helpers/refundable.mdx +++ b/sdk/helpers/refundable.mdx @@ -7,7 +7,7 @@ icon: "wrench" The `@x402r/helpers` package provides the `refundable()` function — a framework-agnostic helper that adds escrow configuration to x402 payment options. -This function lives in `@x402r/helpers`, not `@x402r/merchant`. Install it separately: +This function lives in `@x402r/helpers`, not `@x402r/sdk`. Install it separately: ```bash npm install @x402r/helpers @x402r/core ``` @@ -121,12 +121,14 @@ const option = refundable({ ## Supported Networks -The function resolves addresses from the network config for all supported networks. See `getNetworkConfig()` for the full list (Base Sepolia, Base, Ethereum, Ethereum Sepolia, Polygon, Arbitrum, Optimism, Avalanche, Celo, Monad). +The function resolves addresses from the chain config for all supported networks. See `getChainConfig()` for the full list (Base Sepolia, Base, Ethereum, Ethereum Sepolia, Polygon, Arbitrum, Optimism, Avalanche, Celo, Monad, Linea, SKALE Base). ```typescript -refundable({ network: 'eip155:84532', ... }, '0x...'); // Base Sepolia -refundable({ network: 'eip155:8453', ... }, '0x...'); // Base Mainnet -refundable({ network: 'eip155:1', ... }, '0x...'); // Ethereum +refundable({ network: 'eip155:84532', ... }, '0x...'); // Base Sepolia +refundable({ network: 'eip155:8453', ... }, '0x...'); // Base Mainnet +refundable({ network: 'eip155:1', ... }, '0x...'); // Ethereum +refundable({ network: 'eip155:59144', ... }, '0x...'); // Linea +refundable({ network: 'eip155:1187947933', ... }, '0x...'); // SKALE Base ``` ## Integration with x402 diff --git a/sdk/installation.mdx b/sdk/installation.mdx index 7485080..8ee701d 100644 --- a/sdk/installation.mdx +++ b/sdk/installation.mdx @@ -9,22 +9,17 @@ icon: "download" Install only the packages you need for your use case: - + ```bash - npm install @x402r/client @x402r/core viem + npm install @x402r/sdk @x402r/core viem ``` - + ```bash - npm install @x402r/merchant @x402r/helpers @x402r/core viem + npm install @x402r/sdk @x402r/helpers @x402r/core viem ``` - - ```bash - npm install @x402r/arbiter @x402r/core viem - ``` - - + ```bash npm install @x402r/helpers @x402r/core viem ``` @@ -35,23 +30,22 @@ Install only the packages you need for your use case: Create `publicClient` and `walletClient` using [viem](https://viem.sh/docs/clients/public). All SDK classes require these as constructor arguments. -## Contract Addresses +## Contract addresses -Get the deployed contract addresses from the network config: +Get the deployed contract addresses from the chain config: ```typescript -import { getNetworkConfig } from '@x402r/core'; +import { getChainConfig } from '@x402r/core'; -const config = getNetworkConfig('eip155:84532'); // Base Sepolia +const config = getChainConfig(84532); // Base Sepolia console.log(config.authCaptureEscrow); // Escrow contract -console.log(config.refundRequest); // RefundRequest contract console.log(config.arbiterRegistry); // ArbiterRegistry contract console.log(config.usdc); // USDC token address ``` -Network identifiers use the [EIP-155](https://eips.ethereum.org/EIPS/eip-155) format: `eip155:`. For Base Sepolia, use `'eip155:84532'`. For Base Mainnet, use `'eip155:8453'`. +`getChainConfig()` takes a numeric chain ID. Use `fromNetworkId('eip155:84532')` to convert a CAIP-2 network identifier to a chain ID, or `toNetworkId(84532)` for the reverse. diff --git a/sdk/limitations.mdx b/sdk/limitations.mdx index 7e3a23c..a75150b 100644 --- a/sdk/limitations.mdx +++ b/sdk/limitations.mdx @@ -8,16 +8,19 @@ The SDK provides full coverage of core payment flows including authorization, re ## API Constraints -### EIP-155 Network Identifiers +### Chain ID lookups -Network configuration requires EIP-155 format strings, not chain ID numbers: +Chain configuration uses numeric chain IDs. Use the CAIP-2 helpers for conversions: ```typescript -// Correct -const config = getNetworkConfig('eip155:84532'); +import { getChainConfig, fromNetworkId, toNetworkId } from '@x402r/core'; -// Incorrect - will return undefined -const config = getNetworkConfig(84532); +// Correct - numeric chain ID +const config = getChainConfig(84532); + +// Convert from EIP-155 string +const chainId = fromNetworkId('eip155:84532'); // 84532 +const config2 = getChainConfig(chainId); ``` ### PaymentInfo Must Be Complete diff --git a/sdk/merchant/payment-operations.mdx b/sdk/merchant/payment-operations.mdx index e5bdd20..beaebfd 100644 --- a/sdk/merchant/payment-operations.mdx +++ b/sdk/merchant/payment-operations.mdx @@ -4,7 +4,7 @@ description: "Release funds, charge payments, process refunds, and query escrow icon: "coins" --- -The `X402rMerchant` class provides methods for managing the full payment lifecycle: releasing escrowed funds, charging directly for subscriptions, processing refunds, and querying operator configuration. +The `createMerchantClient` factory provides methods for managing the full payment lifecycle: releasing escrowed funds, charging directly for subscriptions, processing refunds, and querying operator configuration. ## Payment Operations @@ -13,10 +13,10 @@ The `X402rMerchant` class provides methods for managing the full payment lifecyc Use `release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required** and specifies the exact amount to release in token units. ```typescript -import { X402rMerchant } from '@x402r/merchant'; +import { createMerchantClient } from '@x402r/sdk'; // Release 10 USDC (6 decimals) from escrow -const { txHash } = await merchant.release(paymentInfo, BigInt('10000000')); +const txHash = await merchant.release(paymentInfo, BigInt('10000000')); console.log('Released:', txHash); ``` @@ -114,7 +114,7 @@ if (capturableAmount > 0n) { ``` -`getPaymentAmounts()` requires the `escrowAddress` to be configured when creating the `X402rMerchant` instance. +`getPaymentAmounts()` reads amounts directly from the PaymentOperator contract. ### Get Operator Configuration diff --git a/sdk/merchant/quickstart.mdx b/sdk/merchant/quickstart.mdx index 4929d3f..25d3add 100644 --- a/sdk/merchant/quickstart.mdx +++ b/sdk/merchant/quickstart.mdx @@ -1,19 +1,19 @@ --- title: "Merchant SDK" -description: "Release funds, charge payments, process refunds, and query escrow state with @x402r/merchant" +description: "Release funds, charge payments, process refunds, and query escrow state with @x402r/sdk" icon: "rocket" --- -The `@x402r/merchant` package provides everything merchants need for the post-payment lifecycle: releasing escrowed funds, charging directly, processing refunds, and querying operator state. +The `@x402r/sdk` package provides everything merchants need for the post-payment lifecycle: releasing escrowed funds, charging directly, processing refunds, and querying operator state. -**Looking for server setup?** The [Merchant Server Quickstart](/sdk/merchant/getting-started) shows how to accept escrow payments via Express middleware. This page covers the `X402rMerchant` class for managing payments after they arrive. +**Looking for server setup?** The [Merchant Server Quickstart](/sdk/merchant/getting-started) shows how to accept escrow payments via Express middleware. This page covers `createMerchantClient` for managing payments after they arrive. ## Installation ```bash -npm install @x402r/merchant @x402r/helpers @x402r/core viem +npm install @x402r/sdk @x402r/helpers @x402r/core viem ``` ## Setup @@ -21,17 +21,12 @@ npm install @x402r/merchant @x402r/helpers @x402r/core viem Create viem clients as described in [Installation](/sdk/installation), then: ```typescript -import { X402rMerchant } from '@x402r/merchant'; -import { getNetworkConfig } from '@x402r/core'; +import { createMerchantClient } from '@x402r/sdk'; -const config = getNetworkConfig('eip155:84532')!; - -const merchant = new X402rMerchant({ +const merchant = createMerchantClient({ publicClient, walletClient, operatorAddress: '0x...', // Your PaymentOperator address - escrowAddress: config.authCaptureEscrow, - refundRequestAddress: config.refundRequest, }); ``` @@ -138,7 +133,7 @@ if (capturableAmount > 0n) { ``` -`getPaymentAmounts()` requires the `escrowAddress` to be configured when creating the `X402rMerchant` instance. +`getPaymentAmounts()` reads amounts directly from the PaymentOperator contract. ### Get Payment State diff --git a/sdk/merchant/refund-handling.mdx b/sdk/merchant/refund-handling.mdx index 02e8b31..4d6d89a 100644 --- a/sdk/merchant/refund-handling.mdx +++ b/sdk/merchant/refund-handling.mdx @@ -4,7 +4,7 @@ description: "Process, approve, deny, and manage refund requests with the Mercha icon: "rotate-left" --- -The `X402rMerchant` class provides a complete set of methods for handling refund requests from payers. Every refund-related method requires a `nonce: bigint` parameter that identifies which specific charge the refund targets. +The `createMerchantClient` factory provides a complete set of methods for handling refund requests from payers. Every refund-related method requires a `nonce: bigint` parameter that identifies which specific charge the refund targets. The `nonce` parameter corresponds to the record index from the `PaymentIndexRecorder`. For the first charge against a payment, the nonce is `0n`. Each subsequent charge increments the nonce. @@ -90,7 +90,7 @@ console.log('Status:', request.status); ### Get Pending Refund Requests -Use `getPendingRefundRequests()` to retrieve paginated refund request keys for the current receiver address. This method uses the wallet address associated with your `X402rMerchant` instance. +Use `getPendingRefundRequests()` to retrieve paginated refund request keys for the current receiver address. This method uses the wallet address associated with your merchant client. ```typescript // Get the first 10 refund request keys @@ -204,14 +204,12 @@ console.log('Payment unfrozen:', txHash); Here is a full workflow showing how to detect a refund request, review it, make a decision, and execute the refund if approved. ```typescript -import { createPublicClient, createWalletClient, http } from 'viem'; -import { baseSepolia } from 'viem/chains'; -import { privateKeyToAccount } from 'viem/accounts'; -import { X402rMerchant } from '@x402r/merchant'; -import { getNetworkConfig, RequestStatus } from '@x402r/core'; +import { createMerchantClient } from '@x402r/sdk'; +import { RequestStatus } from '@x402r/core'; +import type { PaymentInfo } from '@x402r/core'; async function handleRefundWorkflow( - merchant: X402rMerchant, + merchant: ReturnType, paymentInfo: PaymentInfo, nonce: bigint ) { diff --git a/sdk/merchant/subscriptions.mdx b/sdk/merchant/subscriptions.mdx index 813b31d..44b9a53 100644 --- a/sdk/merchant/subscriptions.mdx +++ b/sdk/merchant/subscriptions.mdx @@ -4,7 +4,7 @@ description: "Subscribe to real-time refund, release, and freeze events with the icon: "bell" --- -The `X402rMerchant` class provides three subscription methods for watching blockchain events in real-time. Each returns an object with an `unsubscribe` function for cleanup. +The `createMerchantClient` factory provides subscription methods for watching blockchain events in real-time. Each returns an unsubscribe function for cleanup. ## Watch Refund Requests @@ -47,13 +47,13 @@ interface RefundRequestEventLog { ``` -`watchRefundRequests()` requires the `refundRequestAddress` to be configured when creating the `X402rMerchant` instance. +`watchRefundRequests()` requires the `refundRequestAddress` to be configured when creating the merchant client. ### Example: Auto-respond to Small Refund Requests ```typescript -import { X402rMerchant } from '@x402r/merchant'; +import { createMerchantClient } from '@x402r/sdk'; import { RequestStatus } from '@x402r/core'; const AUTO_APPROVE_THRESHOLD = BigInt('5000000'); // 5 USDC diff --git a/sdk/overview.mdx b/sdk/overview.mdx index 8e9f02b..74da5ef 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -16,23 +16,17 @@ The SDK is organized into packages designed for specific roles in the payment ec - Shared types, ABIs, network config, deploy utilities, and condition builders. + Shared types, ABIs, chain config, deploy utilities, and condition builders. - - SDK for payers to request refunds, freeze payments, and manage escrow. - - - SDK for merchants to release payments, charge, and handle refunds. - - - SDK for arbiters to resolve disputes and manage refund decisions. + + High-level SDK with role-scoped clients (`createPayerClient`, `createMerchantClient`, `createArbiterClient`) and the generic `createX402r` factory. Framework-agnostic helper to mark x402 payment options as refundable with escrow configuration. -## Network Support +## Network support | Network | Chain ID | Status | |---------|----------|--------| @@ -46,3 +40,5 @@ The SDK is organized into packages designed for specific roles in the payment ec | Avalanche | 43114 | Deployed, not yet tested | | Celo | 42220 | Deployed, not yet tested | | Monad | 143 | Deployed, not yet tested | +| Linea | 59144 | Deployed, not yet tested | +| SKALE Base | 1187947933 | Deployed, not yet tested | From 56bd6848baf4d4893b709011efab739403a6d6b3 Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 05:09:21 +0000 Subject: [PATCH 02/37] docs: sync SDK pages with contracts v3 API changes Generated-By: mintlify-agent --- sdk/arbiter/ai-integration.mdx | 38 ++--- sdk/arbiter/batch-operations.mdx | 156 +++++------------ sdk/arbiter/decision-submission.mdx | 172 ++++++++----------- sdk/arbiter/quickstart.mdx | 50 +++--- sdk/arbiter/registry.mdx | 23 +-- sdk/arbiter/subscriptions.mdx | 116 ++++--------- sdk/client/escrow-management.mdx | 131 ++++++-------- sdk/client/payment-queries.mdx | 96 +++-------- sdk/client/quickstart.mdx | 52 +++--- sdk/client/refund-operations.mdx | 85 +++++----- sdk/client/subscriptions.mdx | 186 ++++---------------- sdk/concepts.mdx | 68 ++++---- sdk/deploy-operator.mdx | 25 +-- sdk/helpers/refundable.mdx | 2 +- sdk/installation.mdx | 34 ++-- sdk/limitations.mdx | 35 ++-- sdk/merchant/payment-operations.mdx | 175 +++++-------------- sdk/merchant/quickstart.mdx | 149 ++++++---------- sdk/merchant/refund-handling.mdx | 254 +++++++++------------------- sdk/merchant/subscriptions.mdx | 173 ++++++------------- sdk/overview.mdx | 28 +-- 21 files changed, 708 insertions(+), 1340 deletions(-) diff --git a/sdk/arbiter/ai-integration.mdx b/sdk/arbiter/ai-integration.mdx index 333b400..773c9d4 100644 --- a/sdk/arbiter/ai-integration.mdx +++ b/sdk/arbiter/ai-integration.mdx @@ -23,9 +23,6 @@ interface CaseEvaluationContext { /** The payment information struct */ paymentInfo: PaymentInfo; - /** The record index (nonce) identifying which charge */ - nonce: bigint; - /** Current payment state */ paymentState: PaymentState; @@ -89,15 +86,15 @@ const handler = createWebhookHandler({ ``` -Setting `autoSubmitDecision: true` calls `approveRefundRequest` or `denyRefundRequest` on-chain automatically. This submits the decision only -- executing the actual refund transfer via `executeRefundInEscrow` is a separate step you handle after approval. +Setting `autoSubmitDecision: true` calls `refundInEscrow` (for approvals) or `denyRefundRequest` on-chain automatically. In v3, `refundInEscrow()` both executes the refund and auto-approves the pending request in one step. ## Webhook Handler Configuration ```typescript interface WebhookHandlerConfig { - /** X402rArbiter instance */ - arbiter: X402rArbiter; + /** Arbiter client instance */ + arbiter: ReturnType; /** Your evaluation function */ evaluationHook: ArbiterHook; @@ -127,8 +124,7 @@ interface WebhookResult extends DecisionResult { The most common pattern combines `watchNewCases` with the webhook handler to automatically evaluate incoming refund requests: ```typescript -import { X402rArbiter, createWebhookHandler } from '@x402r/arbiter'; -import type { CaseEvaluationContext, DecisionResult, RefundRequestEventLog } from '@x402r/arbiter'; +import { createArbiterClient } from '@x402r/sdk'; import { PaymentState, RequestStatus } from '@x402r/core'; import type { PaymentInfo } from '@x402r/core'; @@ -143,23 +139,21 @@ const handler = createWebhookHandler({ }); // Step 2: Watch for new cases and feed them to the handler -const { unsubscribe } = arbiter.watchNewCases(async (event: RefundRequestEventLog) => { - const paymentInfoHash = event.args.paymentInfoHash!; - const nonce = event.args.nonce ?? 0n; +const unsubscribe = arbiter.watch.onRefundRequest(async (event: any) => { + const paymentInfoHash = event.args?.paymentInfoHash; + if (!paymentInfoHash) return; - console.log(`[NEW CASE] ${paymentInfoHash} (nonce: ${nonce})`); + console.log(`[NEW CASE] ${paymentInfoHash}`); - // Build the evaluation context - // NOTE: You need to reconstruct the full PaymentInfo from your database or event logs - const paymentInfo = await lookupPaymentInfo(paymentInfoHash); + // Retrieve stored PaymentInfo from RefundRequest contract + const paymentInfo = await arbiter.refund.getStoredPaymentInfo(paymentInfoHash); const context: CaseEvaluationContext = { paymentInfo, - nonce, paymentState: PaymentState.InEscrow, refundStatus: RequestStatus.Pending, paymentInfoHash, - refundAmount: event.args.amount, + refundAmount: event.args?.amount, }; // Evaluate and optionally auto-submit @@ -170,15 +164,7 @@ const { unsubscribe } = arbiter.watchNewCases(async (event: RefundRequestEventLo if (result.executed) { console.log(`[ON-CHAIN] Decision submitted: ${result.txHash}`); - - // If approved, execute the refund transfer - if (result.decision === 'approve') { - const { txHash } = await arbiter.executeRefundInEscrow( - paymentInfo, - result.refundAmount // partial refund if specified - ); - console.log(`[REFUND] Executed: ${txHash}`); - } + // In v3, refundInEscrow already executes the refund and auto-approves } else { console.log('[SKIPPED] Confidence below threshold, requires manual review'); } diff --git a/sdk/arbiter/batch-operations.mdx b/sdk/arbiter/batch-operations.mdx index 7f32132..10dbce7 100644 --- a/sdk/arbiter/batch-operations.mdx +++ b/sdk/arbiter/batch-operations.mdx @@ -1,122 +1,92 @@ --- -title: "Batch Operations" +title: "Batch operations" description: "Process multiple refund decisions efficiently with the Arbiter SDK" icon: "layer-group" --- -The Arbiter SDK provides batch operations for processing multiple refund requests in a single call. Both `batchApprove` and `batchDeny` accept an array of `{ paymentInfo, nonce }` objects. +You can process multiple refund decisions by iterating over requests and calling individual methods. In v3, there is no separate approve step — calling `payment.refundInEscrow()` executes the refund and auto-approves the pending request in one transaction. -Batch items are processed **sequentially**, not atomically. If one item fails mid-batch, all previously processed items will **not** be rolled back. Design your error handling accordingly. +Each decision is a separate on-chain transaction. If one fails mid-batch, previously processed items will **not** be rolled back. Design your error handling accordingly. -## Batch Approve +## Batch refund -Approve multiple refund requests in one call: +Execute refunds for multiple pending requests: ```typescript -const items = [ - { paymentInfo: paymentInfo1, nonce: 0n }, - { paymentInfo: paymentInfo2, nonce: 0n }, - { paymentInfo: paymentInfo3, nonce: 1n }, -]; +const items = [paymentInfo1, paymentInfo2, paymentInfo3]; -const results = await arbiter.batchApprove(items); - -for (const { txHash } of results) { - console.log('Approved:', txHash); +for (const paymentInfo of items) { + const request = await arbiter.refund.get(paymentInfo); + const txHash = await arbiter.payment.refundInEscrow(paymentInfo, request.amount); + console.log('Refund executed:', txHash); } ``` -## Batch Deny +## Batch deny -Deny multiple refund requests in one call: +Deny multiple refund requests: ```typescript -const items = [ - { paymentInfo: paymentInfo4, nonce: 0n }, - { paymentInfo: paymentInfo5, nonce: 0n }, -]; - -const results = await arbiter.batchDeny(items); +const items = [paymentInfo4, paymentInfo5]; -for (const { txHash } of results) { +for (const paymentInfo of items) { + const txHash = await arbiter.refund.deny(paymentInfo); console.log('Denied:', txHash); } ``` -## Empty Batch Handling - -Both batch methods safely handle empty arrays and return an empty results array: - -```typescript -const results = await arbiter.batchApprove([]); -console.log(results.length); // 0 -``` - -## Item Format - -Each item in the batch array must include both the `paymentInfo` struct and the `nonce`: - -```typescript -interface BatchItem { - /** The full PaymentInfo struct identifying the payment */ - paymentInfo: PaymentInfo; - /** The record index (nonce) from PaymentIndexRecorder */ - nonce: bigint; -} -``` - - -The `nonce` identifies which specific charge record the refund request targets. For most single-charge payments, this is `0n`. - - -## Example: Triage and Batch Process Pending Cases +## Example: triage and batch process pending cases -Fetch all pending cases, evaluate each one, then batch approve and deny: +Fetch all pending cases, evaluate each one, then process: ```typescript -import { X402rArbiter } from '@x402r/arbiter'; +import { createArbiterClient } from '@x402r/sdk'; import { RequestStatus } from '@x402r/core'; import type { PaymentInfo, RefundRequestData } from '@x402r/core'; async function triageAndProcess( - arbiter: X402rArbiter, - receiverAddress: `0x${string}`, - lookupPaymentInfo: (hash: `0x${string}`) => Promise + arbiter: ReturnType, + operatorAddress: `0x${string}` ) { - // Step 1: Fetch pending cases - const { keys, total } = await arbiter.getPendingRefundRequests(0n, 100n, receiverAddress); - console.log(`Processing ${total} pending cases`); + // Step 1: Fetch cases for this operator + const { keys, total } = await arbiter.refund.getOperatorRequests( + operatorAddress, 0n, 100n + ); + console.log(`Processing ${total} cases`); - const toApprove: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> = []; - const toDeny: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> = []; + const toRefund: PaymentInfo[] = []; + const toDeny: PaymentInfo[] = []; // Step 2: Evaluate each case for (const key of keys) { - const request = await arbiter.getRefundRequestByKey(key); + const request = await arbiter.refund.getByKey(key); if (request.status !== RequestStatus.Pending) { continue; } - const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash); - const item = { paymentInfo, nonce: request.nonce }; + const paymentInfo = await arbiter.refund.getStoredPaymentInfo(request.paymentInfoHash); if (shouldApprove(request)) { - toApprove.push(item); + toRefund.push(paymentInfo); } else { - toDeny.push(item); + toDeny.push(paymentInfo); } } - // Step 3: Batch process decisions - const approveResults = await arbiter.batchApprove(toApprove); - const denyResults = await arbiter.batchDeny(toDeny); + // Step 3: Process decisions + for (const paymentInfo of toRefund) { + const request = await arbiter.refund.get(paymentInfo); + await arbiter.payment.refundInEscrow(paymentInfo, request.amount); + } - console.log(`Approved: ${approveResults.length}, Denied: ${denyResults.length}`); + for (const paymentInfo of toDeny) { + await arbiter.refund.deny(paymentInfo); + } - return { approved: approveResults, denied: denyResults }; + console.log(`Refunded: ${toRefund.length}, Denied: ${toDeny.length}`); } function shouldApprove(request: RefundRequestData): boolean { @@ -125,56 +95,20 @@ function shouldApprove(request: RefundRequestData): boolean { } ``` -## Example: Batch Approve with Refund Execution - -After batch approving, execute refunds individually for each approved payment: - -```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import type { PaymentInfo } from '@x402r/core'; - -async function batchApproveAndExecute( - arbiter: X402rArbiter, - items: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> -) { - // Step 1: Batch approve all items - const approveResults = await arbiter.batchApprove(items); - console.log(`Approved ${approveResults.length} refund requests`); - - // Step 2: Execute refunds individually - const executeResults: Array<{ txHash: `0x${string}` }> = []; - - for (const { paymentInfo } of items) { - try { - const { txHash } = await arbiter.executeRefundInEscrow(paymentInfo); - executeResults.push({ txHash }); - console.log('Refund executed:', txHash); - } catch (error) { - console.error(`Failed to execute refund for ${paymentInfo.payer}:`, error); - } - } - - return { - approved: approveResults, - executed: executeResults, - }; -} -``` - -## Performance Considerations +## Performance considerations -Each item in a batch results in a separate on-chain transaction. Gas costs scale linearly with the number of items. Plan batch sizes around your RPC provider's rate limits. +Each item results in a separate on-chain transaction. Gas costs scale linearly with the number of items. Plan batch sizes around your RPC provider's rate limits. | Factor | Detail | |--------|--------| | **Transaction ordering** | Items are processed sequentially to ensure correct nonce ordering. | -| **Gas costs** | Each item is a separate transaction. Batch methods save on SDK overhead, not gas. | +| **Gas costs** | Each item is a separate transaction. | | **Partial failures** | If one transaction fails, previous ones remain on-chain. Handle partial failures in your logic. | | **Rate limiting** | Large batches may hit RPC rate limits. Consider adding delays for 50+ item batches. | -## Next Steps +## Next steps @@ -184,7 +118,7 @@ Each item in a batch results in a separate on-chain transaction. Gas costs scale Watch for new cases in real-time. - Individual approve/deny methods and executeRefundInEscrow. + Individual deny methods and refundInEscrow. Review the complete arbiter setup guide. diff --git a/sdk/arbiter/decision-submission.mdx b/sdk/arbiter/decision-submission.mdx index 696479e..b75ee3f 100644 --- a/sdk/arbiter/decision-submission.mdx +++ b/sdk/arbiter/decision-submission.mdx @@ -1,58 +1,53 @@ --- -title: "Decision Submission" +title: "Decision submission" description: "Submit decisions on refund requests and execute refunds with the Arbiter SDK" icon: "gavel" --- -The Arbiter SDK provides methods for reviewing refund requests, making decisions, and executing refunds for disputed payments. +The arbiter client provides methods for reviewing refund requests, making decisions, and executing refunds for disputed payments. In v3, there is no separate approve step — calling `payment.refundInEscrow()` both executes the refund and auto-approves the pending request. -## Approve a Refund Request +## Execute refund in escrow -Approve a pending refund request. This updates the on-chain status but does not transfer funds. +Execute a refund to transfer funds back to the payer. This also auto-approves any pending refund request via the IRecorder plugin. ```typescript -const { txHash } = await arbiter.approveRefundRequest(paymentInfo, 0n); -console.log('Refund approved:', txHash); -``` +// Full refund +const txHash = await arbiter.payment.refundInEscrow( + paymentInfo, + paymentInfo.maxAmount +); +console.log('Full refund executed:', txHash); - -Approving a refund request updates the request status to `Approved` but does not transfer funds. You must call `executeRefundInEscrow()` separately to move funds back to the payer. - +// Partial refund +const partialAmount = BigInt('500000'); // 0.5 USDC +const partialTx = await arbiter.payment.refundInEscrow(paymentInfo, partialAmount); +console.log('Partial refund executed:', partialTx); +``` -## Deny a Refund Request +## Deny a refund request Deny a pending refund request: ```typescript -const { txHash } = await arbiter.denyRefundRequest(paymentInfo, 0n); +const txHash = await arbiter.refund.deny(paymentInfo); console.log('Refund denied:', txHash); ``` -## Execute Refund in Escrow +## Refuse a refund request -After approving a refund request, execute the actual fund transfer back to the payer: +Refuse a pending refund request (similar to deny, but signals a different intent): ```typescript -// Full refund (defaults to paymentInfo.maxAmount) -const { txHash } = await arbiter.executeRefundInEscrow(paymentInfo); -console.log('Full refund executed:', txHash); - -// Partial refund -const partialAmount = BigInt('500000'); // 0.5 USDC -const { txHash: partialTx } = await arbiter.executeRefundInEscrow(paymentInfo, partialAmount); -console.log('Partial refund executed:', partialTx); +const txHash = await arbiter.refund.refuse(paymentInfo); +console.log('Refund refused:', txHash); ``` - -When no `amount` is provided, `executeRefundInEscrow` defaults to `paymentInfo.maxAmount`, issuing a full refund. - - -## Check If a Refund Request Exists +## Check if a refund request exists -Verify whether a refund request has been submitted for a given payment and nonce: +Verify whether a refund request has been submitted for a given payment: ```typescript -const hasRequest = await arbiter.hasRefundRequest(paymentInfo, 0n); +const hasRequest = await arbiter.refund.has(paymentInfo); if (!hasRequest) { console.log('No refund request found for this payment'); @@ -60,47 +55,35 @@ if (!hasRequest) { } ``` -## Get Refund Request Data +## Get refund request data Retrieve the full refund request data, including amount and status: ```typescript import { RequestStatus } from '@x402r/core'; -const request = await arbiter.getRefundRequest(paymentInfo, 0n); +const request = await arbiter.refund.get(paymentInfo); console.log('Payment hash:', request.paymentInfoHash); -console.log('Nonce:', request.nonce); console.log('Refund amount:', request.amount); console.log('Status:', RequestStatus[request.status]); ``` -The `RefundRequestData` type contains: - -```typescript -interface RefundRequestData { - paymentInfoHash: `0x${string}`; - nonce: bigint; - amount: bigint; - status: RequestStatus; -} -``` - -## Get Refund Request Status +## Get refund request status Query the current status of a specific refund request: ```typescript import { RequestStatus } from '@x402r/core'; -const status = await arbiter.getRefundStatus(paymentInfo, 0n); +const status = await arbiter.refund.getStatus(paymentInfo); switch (status) { case RequestStatus.Pending: console.log('Awaiting decision'); break; case RequestStatus.Approved: - console.log('Already approved'); + console.log('Already approved (via refundInEscrow)'); break; case RequestStatus.Denied: console.log('Already denied'); @@ -111,58 +94,41 @@ switch (status) { } ``` -## Get Pending Refund Requests (Paginated) +## Get refund requests by operator (paginated) -Retrieve a paginated list of refund request keys for a receiver. You can optionally filter by receiver address: +Retrieve a paginated list of refund request keys for an operator: ```typescript -// Get the first 10 pending requests for a specific receiver -const { keys, total } = await arbiter.getPendingRefundRequests( +const { keys, total } = await arbiter.refund.getOperatorRequests( + operatorAddress, 0n, // offset - 10n, // count - '0xReceiverAddress...' // optional: defaults to the arbiter's wallet address + 10n // count ); console.log(`${total} total cases, showing first ${keys.length}`); -// Look up each request by its composite key for (const key of keys) { - const request = await arbiter.getRefundRequestByKey(key); + const request = await arbiter.refund.getByKey(key); console.log(`Amount: ${request.amount}, Status: ${RequestStatus[request.status]}`); } ``` -## Get Refund Request Count +## Get refund request by key -Get the total number of refund requests for a receiver: +Look up a specific refund request using its `paymentInfoHash`: ```typescript -const count = await arbiter.getRefundRequestCount('0xReceiverAddress...'); -console.log(`Total refund requests: ${count}`); - -// Defaults to the arbiter's wallet address if not specified -const myCount = await arbiter.getRefundRequestCount(); -console.log(`My refund requests: ${myCount}`); -``` - -## Get Refund Request by Composite Key - -Look up a specific refund request using its `keccak256(paymentInfoHash, nonce)` composite key: - -```typescript -const request = await arbiter.getRefundRequestByKey(compositeKey); +const request = await arbiter.refund.getByKey(paymentInfoHash); console.log('Payment hash:', request.paymentInfoHash); console.log('Amount:', request.amount); console.log('Status:', RequestStatus[request.status]); ``` -## Check If a Payment Is Frozen - -Verify whether a payment is currently frozen by a Freeze condition contract: +## Check if a payment is frozen ```typescript -const frozen = await arbiter.isFrozen(paymentInfo, freezeAddress); +const frozen = await arbiter.freeze.isFrozen(paymentInfo); if (frozen) { console.log('Payment is frozen - dispute in progress'); @@ -171,23 +137,28 @@ if (frozen) { } ``` -## Complete Decision Workflow +## Complete decision workflow This example shows the full arbiter workflow: fetching pending cases, reviewing them, making a decision, and executing the refund. ```typescript -import { X402rArbiter } from '@x402r/arbiter'; +import { createArbiterClient } from '@x402r/sdk'; import { RequestStatus } from '@x402r/core'; import type { PaymentInfo } from '@x402r/core'; -async function processAllPendingCases(arbiter: X402rArbiter, receiverAddress: `0x${string}`) { - // Step 1: Get all pending refund requests - const { keys, total } = await arbiter.getPendingRefundRequests(0n, 50n, receiverAddress); - console.log(`Found ${total} pending cases`); +async function processAllPendingCases( + arbiter: ReturnType, + operatorAddress: `0x${string}` +) { + // Step 1: Get all refund requests for this operator + const { keys, total } = await arbiter.refund.getOperatorRequests( + operatorAddress, 0n, 50n + ); + console.log(`Found ${total} cases`); for (const key of keys) { // Step 2: Retrieve request details - const request = await arbiter.getRefundRequestByKey(key); + const request = await arbiter.refund.getByKey(key); // Step 3: Skip if already decided if (request.status !== RequestStatus.Pending) { @@ -195,48 +166,41 @@ async function processAllPendingCases(arbiter: X402rArbiter, receiverAddress: `0 continue; } - // Step 4: Apply your decision logic - const shouldApprove = await evaluateCase(request); - - if (shouldApprove) { - // Step 5a: Approve and execute the refund - // NOTE: You need the full PaymentInfo struct to call these methods. - // Retrieve it from your application's database or event logs. - const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash); + // Step 4: Retrieve the stored PaymentInfo + const paymentInfo = await arbiter.refund.getStoredPaymentInfo(request.paymentInfoHash); - const { txHash: approveTx } = await arbiter.approveRefundRequest(paymentInfo, request.nonce); - console.log(`Approved: ${approveTx}`); + // Step 5: Apply your decision logic + const shouldRefund = await evaluateCase(request); - const { txHash: executeTx } = await arbiter.executeRefundInEscrow(paymentInfo); - console.log(`Refund executed: ${executeTx}`); + if (shouldRefund) { + // Execute refund (auto-approves the request) + const txHash = await arbiter.payment.refundInEscrow(paymentInfo, request.amount); + console.log(`Refund executed: ${txHash}`); } else { - // Step 5b: Deny the refund - const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash); - - const { txHash } = await arbiter.denyRefundRequest(paymentInfo, request.nonce); + // Deny the refund + const txHash = await arbiter.refund.deny(paymentInfo); console.log(`Denied: ${txHash}`); } } } ``` -## Decision Flow Diagram +## Decision flow diagram ```mermaid flowchart TD - A[Get Pending Requests] --> B[getRefundRequestByKey] + A[Get Operator Requests] --> B[refund.getByKey] B --> C{Status Pending?} C -->|No| D[Skip - Already Decided] C -->|Yes| E[Evaluate Case] E --> F{Decision} - F -->|Approve| G[approveRefundRequest] - F -->|Deny| H[denyRefundRequest] - G --> I[executeRefundInEscrow] - I --> J[Funds Returned to Payer] + F -->|Refund| G[payment.refundInEscrow] + F -->|Deny| H[refund.deny] + G --> I[Funds Returned + Request Auto-Approved] H --> K[Merchant Keeps Funds] ``` -## Next Steps +## Next steps diff --git a/sdk/arbiter/quickstart.mdx b/sdk/arbiter/quickstart.mdx index dc24b32..1024d7e 100644 --- a/sdk/arbiter/quickstart.mdx +++ b/sdk/arbiter/quickstart.mdx @@ -8,49 +8,53 @@ icon: "rocket" The Arbiter SDK is experimental. The dispute resolution system design is actively evolving. -The `@x402r/arbiter` package provides methods for arbiters to resolve disputes: reviewing refund requests, approving or denying them, and executing refunds. +The `@x402r/sdk` package provides methods for arbiters to resolve disputes: reviewing refund requests, denying them, and executing refunds via `refundInEscrow()`. ## Setup ```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { getNetworkConfig } from '@x402r/core'; +import { createArbiterClient } from '@x402r/sdk'; -const config = getNetworkConfig('eip155:84532')!; - -const arbiter = new X402rArbiter({ +const arbiter = createArbiterClient({ publicClient, walletClient, operatorAddress: '0x...', - refundRequestAddress: config.refundRequest, - arbiterRegistryAddress: config.arbiterRegistry, + // Optional: enable escrow and freeze features + escrowPeriodAddress: '0x...', + freezeAddress: '0x...', }); ``` -## Available Methods +Refund request and evidence addresses are auto-resolved from the chain config. + +## Available methods -The Arbiter SDK currently supports: +The arbiter client currently supports: -- **`approveRefundRequest()`** — Approve a pending refund request -- **`denyRefundRequest()`** — Deny a pending refund request -- **`executeRefundInEscrow()`** — Execute an approved refund to transfer funds back -- **`getPendingRefundRequests()`** — List refund requests awaiting decision -- **`getRefundRequestByKey()`** — Get details of a specific refund request -- **`registerArbiter()`** — Register in the on-chain ArbiterRegistry -- **`isArbiterRegistered()`** — Check registration status -- **`submitEvidence()`** — Attach evidence (IPFS CID) to a refund request -- **`getEvidence()`** — Retrieve a single evidence entry by index -- **`getAllEvidence()`** — Retrieve all evidence for a refund request +- **`payment.refundInEscrow()`** — Execute a refund (auto-approves pending request) +- **`refund.deny()`** — Deny a pending refund request +- **`refund.refuse()`** — Refuse a pending refund request +- **`refund.get()`** — Get full refund request data +- **`refund.getByKey()`** — Look up by paymentInfoHash +- **`refund.getStatus()`** — Check request status +- **`refund.has()`** — Check if a request exists +- **`refund.getOperatorRequests()`** — List requests by operator +- **`freeze.isFrozen()`** — Check if a payment is frozen +- **`freeze.unfreeze()`** — Unfreeze a frozen payment +- **`evidence.submit()`** — Attach evidence (IPFS CID) to a refund request +- **`evidence.get()`** — Retrieve a single evidence entry by index +- **`evidence.getBatch()`** — Retrieve multiple evidence entries +- **`operator.distributeFees()`** — Distribute accumulated fees -## Try It Now +## Try it now -The easiest way to try arbiter features is with the **arbiter-cli** example, which provides a command-line interface for all arbiter operations: +The easiest way to try arbiter features is with the **arbiter-cli** example: CLI tool for arbiters to review cases, make decisions, and manage registry. -## Next Steps +## Next steps diff --git a/sdk/arbiter/registry.mdx b/sdk/arbiter/registry.mdx index 1855c8d..1f88866 100644 --- a/sdk/arbiter/registry.mdx +++ b/sdk/arbiter/registry.mdx @@ -11,20 +11,22 @@ The ArbiterRegistry is an on-chain contract that allows arbiters to register the To use registry methods, provide the `arbiterRegistryAddress` when creating the arbiter instance: ```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { getNetworkConfig } from '@x402r/core'; +import { createArbiterClient } from '@x402r/sdk'; +import { getChainConfig } from '@x402r/core'; -const config = getNetworkConfig('eip155:84532')!; +const config = getChainConfig(84532); -const arbiter = new X402rArbiter({ +const arbiter = createArbiterClient({ publicClient, walletClient, operatorAddress: '0x...', - refundRequestAddress: config.refundRequest, - arbiterRegistryAddress: config.arbiterRegistry, }); ``` + +The arbiter registry address is resolved automatically from the chain config. Registry methods are not currently exposed on the preset client — use `createX402r()` for direct access if needed. + + ## Register as an Arbiter Register your address in the on-chain registry with a URI pointing to your metadata or API endpoint: @@ -120,17 +122,16 @@ interface ArbiterList { ## Complete Example ```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { getNetworkConfig } from '@x402r/core'; +import { createArbiterClient } from '@x402r/sdk'; +import { getChainConfig } from '@x402r/core'; async function main() { - const config = getNetworkConfig('eip155:84532')!; + const config = getChainConfig(84532); - const arbiter = new X402rArbiter({ + const arbiter = createArbiterClient({ publicClient, walletClient, operatorAddress: '0x...', - arbiterRegistryAddress: config.arbiterRegistry, }); // Register diff --git a/sdk/arbiter/subscriptions.mdx b/sdk/arbiter/subscriptions.mdx index e2436ae..062a152 100644 --- a/sdk/arbiter/subscriptions.mdx +++ b/sdk/arbiter/subscriptions.mdx @@ -1,120 +1,74 @@ --- -title: "Arbiter Events" +title: "Arbiter events" description: "Subscribe to dispute events and build real-time arbiter dashboards" icon: "bell" --- -The Arbiter SDK provides three subscription methods for monitoring dispute activity in real-time. Each returns an object with an `unsubscribe` function you call to stop watching. +The arbiter client provides watch methods for monitoring dispute activity in real-time. Each returns an unsubscribe function you call to stop watching. -## Watch New Cases +## Watch refund requests -Subscribe to `RefundRequested` events -- these are new refund requests that need your attention: +Subscribe to RefundRequest contract events — these include new requests, status updates, and cancellations: ```typescript -import type { RefundRequestEventLog } from '@x402r/core'; - -const { unsubscribe } = arbiter.watchNewCases((event: RefundRequestEventLog) => { - console.log('New refund request!'); - console.log('Payment hash:', event.args.paymentInfoHash); - console.log('Payer:', event.args.payer); - console.log('Receiver:', event.args.receiver); - console.log('Amount:', event.args.amount); - console.log('Nonce:', event.args.nonce); - console.log('Block:', event.blockNumber); +const unsubscribe = arbiter.watch.onRefundRequest((event) => { + console.log('Refund request event:', event); }); // Later: stop watching unsubscribe(); ``` -## Watch Decisions +## Watch refund execution -Subscribe to `RefundRequestStatusUpdated` events -- these fire when a refund request is approved or denied: +Subscribe to `RefundInEscrowExecuted` and `RefundPostEscrowExecuted` events on the operator: ```typescript -import type { RefundRequestEventLog } from '@x402r/core'; - -const { unsubscribe } = arbiter.watchDecisions((event: RefundRequestEventLog) => { - console.log('Decision made!'); - console.log('Payment hash:', event.args.paymentInfoHash); - console.log('New status:', event.args.status); - console.log('Transaction:', event.transactionHash); +const unsubscribe = arbiter.watch.onRefundExecuted((event) => { + console.log('Refund executed:', event); }); + +unsubscribe(); ``` -## Watch Freeze Events +## Watch payment events -Subscribe to `PaymentFrozen` and `PaymentUnfrozen` events from a Freeze condition contract: +Subscribe to `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted` events: ```typescript -import type { FreezeEventLog } from '@x402r/core'; - -const freezeAddress = '0xFreezeContractAddress...' as `0x${string}`; - -const { unsubscribe } = arbiter.watchFreezeEvents( - freezeAddress, - (event: FreezeEventLog) => { - if (event.eventName === 'PaymentFrozen') { - console.log('Payment frozen:', event.args.paymentInfoHash); - console.log('Frozen by:', event.args.caller); - } else if (event.eventName === 'PaymentUnfrozen') { - console.log('Payment unfrozen:', event.args.paymentInfoHash); - } - } -); -``` - -## Event Type Reference +const unsubscribe = arbiter.watch.onPayment((event) => { + console.log('Payment event:', event); +}); -| Method | Contract Event | Fires When | -|--------|---------------|------------| -| `watchNewCases` | `RefundRequested` | A payer submits a new refund request | -| `watchDecisions` | `RefundRequestStatusUpdated` | An arbiter or receiver approves/denies a request | -| `watchFreezeEvents` | `PaymentFrozen` / `PaymentUnfrozen` | A payment is frozen or unfrozen | +unsubscribe(); +``` -## Event Log Types +## Watch fee distribution -Both `watchNewCases` and `watchDecisions` emit `RefundRequestEventLog` events: +Subscribe to `FeesDistributed` events: ```typescript -interface RefundRequestEventLog { - eventName: 'RefundRequested' | 'RefundRequestStatusUpdated' | 'RefundRequestCancelled'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - nonce?: bigint; - status?: number; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} +const unsubscribe = arbiter.watch.onFeeDistribution((event) => { + console.log('Fees distributed:', event); +}); + +unsubscribe(); ``` -The `watchFreezeEvents` method emits `FreezeEventLog` events: +## Event type reference -```typescript -interface FreezeEventLog { - eventName: 'PaymentFrozen' | 'PaymentUnfrozen'; - args: { - paymentInfoHash?: `0x${string}`; - caller?: `0x${string}`; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} -``` +| Method | Events Watched | Contract | Fires When | +|--------|---------------|----------|------------| +| `watch.onRefundRequest` | All RefundRequest events | RefundRequest | Request created, status updated, or cancelled | +| `watch.onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | A refund is executed | +| `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | Payment lifecycle events | +| `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | Fees are distributed | -All subscription methods use viem's `watchContractEvent` under the hood. For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). +For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). -## Next Steps +## Next steps diff --git a/sdk/client/escrow-management.mdx b/sdk/client/escrow-management.mdx index 5f86fd3..40e7ec3 100644 --- a/sdk/client/escrow-management.mdx +++ b/sdk/client/escrow-management.mdx @@ -1,33 +1,28 @@ --- -title: "Escrow Management" +title: "Escrow management" description: "Manage escrow periods and freeze payments with the Client SDK" icon: "lock" --- -The Client SDK provides methods to interact with the escrow system, including freezing payments during disputes and querying escrow period timing. All methods documented on this page are **fully functional**. +The payer client provides methods to interact with the escrow system, including freezing payments during disputes and querying escrow period timing. -## Freeze Operations +## Freeze operations -Freezing a payment pauses the escrow timer, preventing the merchant from releasing funds while a dispute is being resolved. Freeze operations interact with the `Freeze` contract. +Freezing a payment pauses the escrow timer, preventing the merchant from releasing funds while a dispute is being resolved. -### freezePayment +### freeze Freeze a payment to pause the escrow timer. Only the payer can freeze a payment. ```typescript -const freezeAddress = '0x...'; // Freeze contract address - -const { txHash } = await client.freezePayment(paymentInfo, freezeAddress); +const txHash = await client.freeze.freeze(paymentInfo); console.log(`Payment frozen: ${txHash}`); ``` #### Signature ```typescript -freezePayment( - paymentInfo: PaymentInfo, - freezeAddress: `0x${string}` -): Promise<{ txHash: `0x${string}` }> +freeze(paymentInfo: PaymentInfo, data?: Hex): Promise ``` @@ -37,22 +32,19 @@ Freezing is useful when: - The merchant is unresponsive to your refund request -### unfreezePayment +### unfreeze Unfreeze a previously frozen payment. The receiver (merchant) or arbiter can unfreeze a payment once the dispute is resolved. ```typescript -const { txHash } = await client.unfreezePayment(paymentInfo, freezeAddress); +const txHash = await client.freeze.unfreeze(paymentInfo); console.log(`Payment unfrozen: ${txHash}`); ``` #### Signature ```typescript -unfreezePayment( - paymentInfo: PaymentInfo, - freezeAddress: `0x${string}` -): Promise<{ txHash: `0x${string}` }> +unfreeze(paymentInfo: PaymentInfo, data?: Hex): Promise ``` ### isFrozen @@ -60,7 +52,7 @@ unfreezePayment( Check whether a payment is currently frozen. ```typescript -const frozen = await client.isFrozen(paymentInfo, freezeAddress); +const frozen = await client.freeze.isFrozen(paymentInfo); if (frozen) { console.log('Payment is frozen - escrow timer paused'); @@ -72,24 +64,19 @@ if (frozen) { #### Signature ```typescript -isFrozen( - paymentInfo: PaymentInfo, - freezeAddress: `0x${string}` -): Promise +isFrozen(paymentInfo: PaymentInfo): Promise ``` -## Escrow Period Operations +## Escrow period operations -These methods interact with the `EscrowPeriod` contract to query timing information about a payment's escrow window. +These methods interact with the `EscrowPeriod` contract to query timing information about a payment's escrow window. You must provide an `escrowPeriodAddress` in your client config to use these methods. ### getAuthorizationTime Get the timestamp (in seconds) when a payment was authorized on-chain. This is the starting point of the escrow period. ```typescript -const escrowPeriodAddress = '0x...'; // EscrowPeriod contract address - -const authTime = await client.getAuthorizationTime(paymentInfo, escrowPeriodAddress); +const authTime = await client.escrow.getAuthorizationTime(paymentInfo); const authDate = new Date(Number(authTime) * 1000); console.log(`Payment authorized at: ${authDate.toISOString()}`); @@ -98,21 +85,15 @@ console.log(`Payment authorized at: ${authDate.toISOString()}`); #### Signature ```typescript -getAuthorizationTime( - paymentInfo: PaymentInfo, - escrowPeriodAddress: `0x${string}` -): Promise +getAuthorizationTime(paymentInfo: PaymentInfo): Promise ``` -### isDuringEscrowPeriod +### isDuringEscrow Check whether a payment is still within its escrow period. Returns `true` if the escrow period has not yet passed, meaning the payment can still be refunded. ```typescript -const duringEscrow = await client.isDuringEscrowPeriod( - paymentInfo, - escrowPeriodAddress -); +const duringEscrow = await client.escrow.isDuringEscrow(paymentInfo); if (duringEscrow) { console.log('Still in escrow period - refund is possible'); @@ -124,17 +105,25 @@ if (duringEscrow) { #### Signature ```typescript -isDuringEscrowPeriod( - paymentInfo: PaymentInfo, - escrowPeriodAddress: `0x${string}` -): Promise +isDuringEscrow(paymentInfo: PaymentInfo): Promise +``` + +### getDuration + +Get the configured escrow period duration in seconds. + +```typescript +const duration = await client.escrow.getDuration(); +console.log(`Escrow period: ${Number(duration) / 86400} days`); ``` - -The method name is `isDuringEscrowPeriod`, **not** `isEscrowPeriodPassed`. It returns `true` when the escrow period is still **active** (refund window is open), and `false` when it has passed. - +#### Signature + +```typescript +getDuration(): Promise +``` -## Understanding Escrow Timing +## Understanding escrow timing | Condition | Escrow Timer | Can Request Refund | Can Release | |-----------|--------------|-------------------|-------------| @@ -143,63 +132,47 @@ The method name is `isDuringEscrowPeriod`, **not** `isEscrowPeriodPassed`. It re | Escrow period passed (unfrozen) | Stopped | No | Full amount | -The escrow period length is configured at the contract level when the `EscrowPeriod` condition is deployed. Common values are 7 days, 14 days, or 30 days. You can calculate the remaining time from `getAuthorizationTime` and the configured period. +The escrow period length is configured at the contract level when the `EscrowPeriod` condition is deployed. Common values are 7 days, 14 days, or 30 days. You can use `getDuration()` to retrieve the configured period. -## Example: Freeze and Request Refund +## Example: freeze and request refund A common pattern is to freeze a payment before submitting a refund request, ensuring the merchant cannot release funds while the request is pending. ```typescript -import { X402rClient } from '@x402r/client'; +import { createPayerClient } from '@x402r/sdk'; import type { PaymentInfo } from '@x402r/core'; async function freezeAndRequestRefund( - client: X402rClient, + client: ReturnType, paymentInfo: PaymentInfo, - freezeAddress: `0x${string}`, refundAmount: bigint ) { // Step 1: Check if already frozen - const alreadyFrozen = await client.isFrozen(paymentInfo, freezeAddress); + const alreadyFrozen = await client.freeze.isFrozen(paymentInfo); if (!alreadyFrozen) { - // Freeze the payment first - const { txHash: freezeTx } = await client.freezePayment( - paymentInfo, - freezeAddress - ); + const freezeTx = await client.freeze.freeze(paymentInfo); console.log(`Payment frozen: ${freezeTx}`); } // Step 2: Check if refund request already exists - const nonce = 0n; - const hasRequest = await client.hasRefundRequest(paymentInfo, nonce); + const hasRequest = await client.refund.has(paymentInfo); if (!hasRequest) { - // Submit the refund request - const { txHash: refundTx } = await client.requestRefund( - paymentInfo, - refundAmount, - nonce - ); + const refundTx = await client.refund.request(paymentInfo, refundAmount); console.log(`Refund requested: ${refundTx}`); } // Step 3: Watch for resolution - const { unsubscribe } = client.watchFreezeEvents( - freezeAddress, - (event) => { - if (event.eventName === 'PaymentUnfrozen') { - console.log('Payment was unfrozen - dispute may be resolved'); - unsubscribe(); - } - } - ); + const unsubscribe = client.watch.onPayment((event) => { + console.log('Payment event received'); + unsubscribe(); + }); } ``` -## Freeze / Unfreeze Flow +## Freeze / unfreeze flow ```mermaid sequenceDiagram @@ -208,19 +181,19 @@ sequenceDiagram participant M as Merchant / Arbiter Note over F: Escrow timer running - P->>F: freezePayment() + P->>F: freeze() Note over F: Timer paused alt Dispute resolved favorably - M->>F: unfreezePayment() + M->>F: unfreeze() Note over F: Timer resumes else Payer cancels dispute - P->>F: unfreezePayment() + P->>F: unfreeze() Note over F: Timer resumes end ``` -## Next Steps +## Next steps @@ -230,7 +203,7 @@ sequenceDiagram Request refunds while payment is in escrow. - Planned query methods and current workarounds. + Query payment state and details. Full setup guide for the Client SDK. diff --git a/sdk/client/payment-queries.mdx b/sdk/client/payment-queries.mdx index 38d3103..ac84d15 100644 --- a/sdk/client/payment-queries.mdx +++ b/sdk/client/payment-queries.mdx @@ -1,108 +1,68 @@ --- -title: "Payment Queries" +title: "Payment queries" description: "Query payment states and details with the Client SDK" icon: "magnifying-glass" --- -The Client SDK provides five methods for querying payment state and history. These read directly from the escrow contract and on-chain event logs. +The payer client provides methods for querying payment state and retrieving payment information. -## getPaymentState +## getState Derive the lifecycle state of a payment from the escrow contract (amounts and expiry). ```typescript -import { PaymentState } from '@x402r/core'; +const [hasCollected, capturableAmount, refundableAmount] = await client.payment.getState(paymentInfo); -const state = await client.getPaymentState(paymentInfo); - -// PaymentState enum: -// 0 = NonExistent - Payment has never been authorized -// 1 = InEscrow - Funds locked, capturableAmount > 0 -// 2 = Released - Funds released to receiver, may still be refundable -// 3 = Settled - No funds in escrow or refundable -// 4 = Expired - Authorization expired, payer can reclaim -``` - -```typescript -getPaymentState(paymentInfo: PaymentInfo): Promise -``` - -## paymentExists - -Check whether a payment has been collected by reading the escrow's `hasCollectedPayment` flag. - -```typescript -const exists = await client.paymentExists(paymentInfoHash); -if (exists) { - console.log('Payment found'); -} -``` - -```typescript -paymentExists(paymentInfoHash: `0x${string}`): Promise -``` - -## isInEscrow - -Check if a payment currently has capturable funds in escrow. - -```typescript -const inEscrow = await client.isInEscrow(paymentInfoHash); -if (inEscrow) { +if (capturableAmount > 0n) { console.log('Payment has funds in escrow'); } ``` ```typescript -isInEscrow(paymentInfoHash: `0x${string}`): Promise +getState(paymentInfo: PaymentInfo): Promise ``` -## getPaymentDetails +## getAmounts -Retrieve the full `PaymentInfo` struct by scanning `AuthorizationCreated` events for the given hash. +Get the capturable and refundable amounts for a payment. ```typescript -const details = await client.getPaymentDetails(paymentInfoHash); +const amounts = await client.payment.getAmounts(paymentInfo); -console.log('Payer:', details.payer); -console.log('Receiver:', details.receiver); -console.log('Amount:', details.maxAmount); +console.log('Capturable:', amounts.capturableAmount); +console.log('Refundable:', amounts.refundableAmount); ``` ```typescript -getPaymentDetails( - paymentInfoHash: `0x${string}`, - fromBlock?: bigint -): Promise +getAmounts(paymentInfo: PaymentInfo): Promise ``` - -This method scans event logs. Pass `fromBlock` to limit the scan range if your RPC limits `eth_getLogs` responses (Base Sepolia typically caps at 10,000 blocks). - - -## getPayerPayments +## Query payments -List all payments where the connected wallet is the payer, by scanning `AuthorizationCreated` events. +If you configured a `paymentIndexRecorderAddress` or `paymentStore`, you can use the query action group to look up payments. ```typescript -const payments = await client.getPayerPayments(); +import { createPayerClient, queryActions } from '@x402r/sdk'; -for (const { hash, paymentInfo } of payments) { - console.log(`Payment ${hash}: ${paymentInfo.maxAmount}`); -} -``` +const client = createPayerClient({ + publicClient, + walletClient, + operatorAddress: '0x...', + paymentIndexRecorderAddress: '0x...', +}).extend(queryActions('0xRecorderAddress...')); -```typescript -getPayerPayments( - fromBlock?: bigint -): Promise> +// Get all payments where you are the payer +const payments = await client.query.getPayerPayments(payerAddress); + +// Get a specific payment by hash +const payment = await client.query.getPayment(paymentInfoHash); ``` -Like `getPaymentDetails`, this scans event logs. Pass `fromBlock` to limit the range for large histories. +The query plugin uses a tiered provider stack: in-memory store, on-chain recorder, then event logs (if `eventFromBlock` is configured). The first provider returning results wins. -## Next Steps +## Next steps diff --git a/sdk/client/quickstart.mdx b/sdk/client/quickstart.mdx index f451a5f..69d3955 100644 --- a/sdk/client/quickstart.mdx +++ b/sdk/client/quickstart.mdx @@ -8,43 +8,47 @@ icon: "rocket" The Client SDK is experimental. APIs will change as the refund and dispute system design evolves. -The `@x402r/client` package provides payer-side methods for interacting with X402r payments: requesting refunds, freezing payments, and querying escrow state. +The `@x402r/sdk` package provides payer-side methods for interacting with X402r payments: requesting refunds, freezing payments, and querying escrow state. ## Setup ```typescript -import { X402rClient } from '@x402r/client'; -import { getNetworkConfig } from '@x402r/core'; +import { createPayerClient } from '@x402r/sdk'; -const networkConfig = getNetworkConfig('eip155:84532')!; - -const client = new X402rClient({ +const client = createPayerClient({ publicClient, walletClient, operatorAddress: '0x...', // Your PaymentOperator address - refundRequestAddress: networkConfig.refundRequest, - escrowAddress: networkConfig.authCaptureEscrow, + // Optional: enable escrow, freeze, and refund features + escrowPeriodAddress: '0x...', + freezeAddress: '0x...', }); ``` -## Available Methods +Refund and evidence addresses are resolved automatically from the chain config. + +## Available methods -The Client SDK currently supports: +The payer client exposes action groups: -- **`requestRefund()`** — Submit a refund request for a payment in escrow -- **`getRefundStatus()`** — Check the status of a refund request -- **`freezePayment()`** — Freeze a payment to prevent release during a dispute -- **`isFrozen()`** — Check if a payment is frozen -- **`isDuringEscrowPeriod()`** — Check if a payment is still in its escrow window -- **`getAuthorizationTime()`** — Get when a payment was authorized -- **`getPaymentState()`** — Derive the lifecycle state of a payment -- **`paymentExists()`** — Check if a payment has been authorized -- **`isInEscrow()`** — Check if a payment has capturable funds -- **`getPaymentDetails()`** — Retrieve full PaymentInfo from event logs -- **`getPayerPayments()`** — List all payments for the connected wallet -- **`submitEvidence()`** — Attach evidence (IPFS CID) to a refund request -- **`getEvidence()`** — Retrieve a single evidence entry by index -- **`getAllEvidence()`** — Retrieve all evidence for a refund request +- **`client.payment.getState()`** — Derive the lifecycle state of a payment +- **`client.payment.getAmounts()`** — Get capturable and refundable amounts +- **`client.refund.request()`** — Submit a refund request for a payment +- **`client.refund.cancel()`** — Cancel a pending refund request +- **`client.refund.getStatus()`** — Check the status of a refund request +- **`client.refund.has()`** — Check if a refund request exists +- **`client.refund.get()`** — Get full refund request data +- **`client.refund.getByKey()`** — Look up by paymentInfoHash +- **`client.refund.getStoredPaymentInfo()`** — Retrieve stored PaymentInfo +- **`client.refund.getPayerRequests()`** — List refund requests by payer +- **`client.freeze.freeze()`** — Freeze a payment to prevent release +- **`client.freeze.isFrozen()`** — Check if a payment is frozen +- **`client.escrow.isDuringEscrow()`** — Check if still in escrow window +- **`client.escrow.getAuthorizationTime()`** — Get when payment was authorized +- **`client.escrow.getDuration()`** — Get the escrow period duration +- **`client.evidence.submit()`** — Attach evidence (IPFS CID) to a refund +- **`client.evidence.get()`** — Retrieve a single evidence entry by index +- **`client.evidence.getBatch()`** — Retrieve multiple evidence entries ## Try It Now diff --git a/sdk/client/refund-operations.mdx b/sdk/client/refund-operations.mdx index 7acd7f3..0fe2936 100644 --- a/sdk/client/refund-operations.mdx +++ b/sdk/client/refund-operations.mdx @@ -1,106 +1,115 @@ --- -title: "Refund Operations" +title: "Refund operations" description: "Request and manage refunds with the Client SDK - submit, cancel, and track refund requests" icon: "rotate-left" --- -The Client SDK provides complete refund management capabilities for payers. All refund methods interact directly with the `RefundRequest` contract on-chain. +The payer client provides complete refund management capabilities. All refund methods interact with the `RefundRequest` contract on-chain. Each payment supports one refund request, keyed by its `paymentInfoHash`. -**About the `nonce` parameter:** Every refund method requires a `nonce: bigint` parameter. This is the record index from the `PaymentIndexRecorder` and identifies which charge within a payment you are requesting a refund for. For the first (and most common) charge, use `0n`. +In v3, RefundRequest is wired as an IRecorder plugin. Refund approval happens automatically when the merchant calls `refundInEscrow()` — there is no separate approve step. -## requestRefund +## request Submit a refund request for a payment that is in escrow. The request goes on-chain and is visible to the merchant and any assigned arbiter. ```typescript -const { txHash } = await client.requestRefund( +const txHash = await client.refund.request( paymentInfo, - BigInt('1000000'), // amount to refund (e.g., 1 USDC with 6 decimals) - 0n // nonce: first charge + BigInt('1000000') // amount to refund (e.g., 1 USDC with 6 decimals) ); console.log(`Refund requested: ${txHash}`); ``` -## cancelRefundRequest +## cancel Cancel a pending refund request that you submitted. Only the original requester (payer) can cancel, and only while the request status is `Pending`. ```typescript -const { txHash } = await client.cancelRefundRequest(paymentInfo, 0n); +const txHash = await client.refund.cancel(paymentInfo); console.log(`Refund request cancelled: ${txHash}`); ``` -## Query Refund State +## Query refund state -These methods read on-chain state for refund requests. None require a wallet client. +These methods read on-chain state for refund requests. ### Check existence and status ```typescript // Check if a refund request exists -const hasRequest = await client.hasRefundRequest(paymentInfo, 0n); +const hasRequest = await client.refund.has(paymentInfo); // Get just the status -const status = await client.getRefundStatus(paymentInfo, 0n); +const status = await client.refund.getStatus(paymentInfo); // Returns: RequestStatus.Pending | Approved | Denied | Cancelled ``` ### Get full refund request data ```typescript -// By paymentInfo + nonce -const request = await client.getRefundRequest(paymentInfo, 0n); +// By paymentInfo +const request = await client.refund.get(paymentInfo); console.log(request.amount, request.status); -// By composite key (from getMyRefundRequests) -const request2 = await client.getRefundRequestByKey(compositeKey); +// By paymentInfoHash +const request2 = await client.refund.getByKey(paymentInfoHash); ``` ### List your refund requests ```typescript -// Get total count -const count = await client.getMyRefundRequestCount(); - -// Get paginated keys, then fetch details -const { keys, total } = await client.getMyRefundRequests(0n, 10n); +// Get paginated refund request keys for a payer address +const { keys, total } = await client.refund.getPayerRequests( + payerAddress, + 0n, // offset + 10n // count +); for (const key of keys) { - const request = await client.getRefundRequestByKey(key); + const request = await client.refund.getByKey(key); console.log(`Amount: ${request.amount}, Status: ${request.status}`); } ``` -## Refund Request Lifecycle +### Retrieve stored payment info + +```typescript +// Get PaymentInfo stored by RefundRequest for a given hash +const storedInfo = await client.refund.getStoredPaymentInfo(paymentInfoHash); +``` + +## Refund request lifecycle ```mermaid stateDiagram-v2 - [*] --> Pending: requestRefund() - Pending --> Approved: Merchant/Arbiter approves + [*] --> Pending: refund.request() + Pending --> Approved: Merchant calls refundInEscrow() Pending --> Denied: Merchant/Arbiter denies - Pending --> Cancelled: cancelRefundRequest() + Pending --> Cancelled: refund.cancel() Approved --> [*]: Funds returned Denied --> [*] Cancelled --> [*] ``` -## Method Reference +## Method reference | Method | Parameters | Returns | |--------|-----------|---------| -| `requestRefund` | `paymentInfo, amount, nonce` | `{ txHash }` | -| `cancelRefundRequest` | `paymentInfo, nonce` | `{ txHash }` | -| `hasRefundRequest` | `paymentInfo, nonce` | `boolean` | -| `getRefundStatus` | `paymentInfo, nonce` | `RequestStatus` | -| `getRefundRequest` | `paymentInfo, nonce` | `RefundRequestData` | -| `getRefundRequestByKey` | `compositeKey` | `RefundRequestData` | -| `getMyRefundRequests` | `offset, count` | `{ keys, total }` | -| `getMyRefundRequestCount` | none | `bigint` | - -## Next Steps +| `refund.request` | `paymentInfo, amount` | `Hash` | +| `refund.cancel` | `paymentInfo` | `Hash` | +| `refund.has` | `paymentInfo` | `boolean` | +| `refund.getStatus` | `paymentInfo` | `RefundRequestStatus` | +| `refund.get` | `paymentInfo` | `RefundRequestData` | +| `refund.getByKey` | `paymentInfoHash` | `RefundRequestData` | +| `refund.getStoredPaymentInfo` | `paymentInfoHash` | `PaymentInfo` | +| `refund.getPayerRequests` | `payer, offset, count` | `{ keys, total }` | +| `refund.getCancelCount` | `paymentInfo` | `bigint` | +| `refund.getCancelledAmount` | `paymentInfo, cancelIndex` | `bigint` | + +## Next steps diff --git a/sdk/client/subscriptions.mdx b/sdk/client/subscriptions.mdx index cac10ce..52dff2f 100644 --- a/sdk/client/subscriptions.mdx +++ b/sdk/client/subscriptions.mdx @@ -1,35 +1,19 @@ --- -title: "Client Events" +title: "Client events" description: "Subscribe to real-time payment, refund, and freeze events as a payer" icon: "bell" --- -The Client SDK provides methods to subscribe to blockchain events in real-time using viem's `watchContractEvent` under the hood. All subscription methods return an object with an `unsubscribe` function that you should call when you no longer need the watcher. +The SDK provides methods to subscribe to blockchain events in real-time using viem's `watchContractEvent` under the hood. All watch methods return an unsubscribe function. -## watchPaymentState +## onPayment -Watch for state changes on a specific payment. This subscribes to `ReleaseExecuted`, `RefundInEscrowExecuted`, and `RefundPostEscrowExecuted` events on the `PaymentOperator` contract. +Watch for payment lifecycle events on the operator contract. This subscribes to `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted` events. ```typescript -const { unsubscribe } = client.watchPaymentState( - paymentInfoHash, - (event) => { - console.log(`Payment event: ${event.eventName}`); - - switch (event.eventName) { - case 'ReleaseExecuted': - console.log('Funds released to merchant'); - console.log('Amount:', event.args.amount); - break; - case 'RefundInEscrowExecuted': - console.log('Funds refunded from escrow'); - break; - case 'RefundPostEscrowExecuted': - console.log('Funds refunded after escrow period'); - break; - } - } -); +const unsubscribe = client.watch.onPayment((event) => { + console.log('Payment event received:', event); +}); // Stop watching when done unsubscribe(); @@ -38,55 +22,16 @@ unsubscribe(); ### Signature ```typescript -watchPaymentState( - paymentInfoHash: `0x${string}`, - callback: (event: PaymentOperatorEventLog) => void -): { unsubscribe: () => void } -``` - -### PaymentOperatorEventLog Type - -```typescript -interface PaymentOperatorEventLog { - eventName: - | 'ReleaseExecuted' - | 'RefundInEscrowExecuted' - | 'RefundPostEscrowExecuted' - | 'AuthorizationCreated' - | 'ChargeExecuted'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} +onPayment(callback: (log: unknown) => void): () => void ``` -## watchRefundRequests +## onRefundRequest -Watch for refund request lifecycle events. This subscribes to `RefundRequested`, `RefundRequestStatusUpdated`, and `RefundRequestCancelled` events on the `RefundRequest` contract. +Watch for refund request lifecycle events on the RefundRequest contract. ```typescript -const { unsubscribe } = client.watchRefundRequests((event) => { - switch (event.eventName) { - case 'RefundRequested': - console.log('New refund request submitted'); - console.log('Payment:', event.args.paymentInfoHash); - console.log('Amount:', event.args.amount); - break; - case 'RefundRequestStatusUpdated': - console.log('Refund status changed:', event.args.status); - // 1 = Approved, 2 = Denied - break; - case 'RefundRequestCancelled': - console.log('Refund request cancelled'); - break; - } +const unsubscribe = client.watch.onRefundRequest((event) => { + console.log('Refund request event:', event); }); // Stop watching when done @@ -96,130 +41,63 @@ unsubscribe(); ### Signature ```typescript -watchRefundRequests( - callback: (event: RefundRequestEventLog) => void -): { unsubscribe: () => void } +onRefundRequest(callback: (log: unknown) => void): () => void ``` -Requires `refundRequestAddress` to be configured on the client. Throws an error if not set. +This is a no-op if no `refundRequestAddress` was resolved. The address is auto-resolved from the chain config. -### RefundRequestEventLog Type +## onRefundExecuted -```typescript -interface RefundRequestEventLog { - eventName: - | 'RefundRequested' - | 'RefundRequestStatusUpdated' - | 'RefundRequestCancelled'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - nonce?: bigint; - status?: number; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} -``` - -## watchMyPayments - -Watch for new payment authorizations where the connected wallet is the payer. This subscribes to `AuthorizationCreated` events on the `PaymentOperator` contract, filtered by the wallet's address. +Watch for refund execution events (`RefundInEscrowExecuted`, `RefundPostEscrowExecuted`) on the operator contract. ```typescript -const { unsubscribe } = client.watchMyPayments((event) => { - console.log('New payment authorized!'); - console.log('Event:', event.eventName); // 'AuthorizationCreated' - console.log('Hash:', event.args.paymentInfoHash); - console.log('Receiver:', event.args.receiver); - console.log('Amount:', event.args.amount); +const unsubscribe = client.watch.onRefundExecuted((event) => { + console.log('Refund executed:', event); }); -// Stop watching when done unsubscribe(); ``` ### Signature ```typescript -watchMyPayments( - callback: (event: PaymentOperatorEventLog) => void -): { unsubscribe: () => void } +onRefundExecuted(callback: (log: unknown) => void): () => void ``` - -Requires a `walletClient` with an account to be configured, since the events are filtered by the payer address. - - -## watchFreezeEvents +## onFeeDistribution -Watch for freeze and unfreeze events on a specific `Freeze` contract. This subscribes to `PaymentFrozen` and `PaymentUnfrozen` events. +Watch for `FeesDistributed` events on the operator contract. ```typescript -const freezeAddress = '0x...'; // Freeze contract address - -const { unsubscribe } = client.watchFreezeEvents( - freezeAddress, - (event) => { - if (event.eventName === 'PaymentFrozen') { - console.log('Payment frozen:', event.args.paymentInfoHash); - console.log('Frozen by:', event.args.caller); - } else if (event.eventName === 'PaymentUnfrozen') { - console.log('Payment unfrozen:', event.args.paymentInfoHash); - console.log('Unfrozen by:', event.args.caller); - } - } -); +const unsubscribe = client.watch.onFeeDistribution((event) => { + console.log('Fees distributed:', event); +}); -// Stop watching when done unsubscribe(); ``` ### Signature ```typescript -watchFreezeEvents( - freezeAddress: `0x${string}`, - callback: (event: FreezeEventLog) => void -): { unsubscribe: () => void } -``` - -### FreezeEventLog Type - -```typescript -interface FreezeEventLog { - eventName: 'PaymentFrozen' | 'PaymentUnfrozen'; - args: { - paymentInfoHash?: `0x${string}`; - caller?: `0x${string}`; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} +onFeeDistribution(callback: (log: unknown) => void): () => void ``` -## Event Types Reference +## Event types reference | Method | Events Watched | Contract | Use Case | |--------|---------------|----------|----------| -| `watchPaymentState` | `ReleaseExecuted`, `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | Track a single payment's lifecycle | -| `watchRefundRequests` | `RefundRequested`, `RefundRequestStatusUpdated`, `RefundRequestCancelled` | RefundRequest | Monitor refund request workflow | -| `watchMyPayments` | `AuthorizationCreated` (filtered by payer) | PaymentOperator | Track new payments for your wallet | -| `watchFreezeEvents` | `PaymentFrozen`, `PaymentUnfrozen` | Freeze | Monitor dispute freeze activity | +| `onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | Track payment lifecycle | +| `onRefundRequest` | All RefundRequest events | RefundRequest | Monitor refund request workflow | +| `onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | Track refund execution | +| `onFeeDistribution` | `FeesDistributed` | PaymentOperator | Monitor fee distribution | -All subscription methods use viem's `watchContractEvent` under the hood. For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). +For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). -## Next Steps +## Next steps diff --git a/sdk/concepts.mdx b/sdk/concepts.mdx index 3b66c61..2a49212 100644 --- a/sdk/concepts.mdx +++ b/sdk/concepts.mdx @@ -64,65 +64,66 @@ The **EscrowPeriod** contract tracks when a payment was authorized and enforces - **After the period** — merchants can release funds to themselves ```typescript -import { X402rClient } from '@x402r/client'; +import { createPayerClient } from '@x402r/sdk'; -// Check when payment was authorized -const authTime = await client.getAuthorizationTime(paymentInfo, escrowPeriodAddress); +// Check when payment was authorized (requires escrowPeriodAddress in config) +const authTime = await client.escrow.getAuthorizationTime(paymentInfo); // Check if still within escrow period -const inEscrow = await client.isDuringEscrowPeriod(paymentInfo, escrowPeriodAddress); +const inEscrow = await client.escrow.isDuringEscrow(paymentInfo); if (!inEscrow) { console.log('Escrow period has passed - funds can be released'); } ``` -## Refund Requests +## Refund requests -When a payer wants a refund, they create a refund request that goes through approval: +When a payer wants a refund, they create a refund request. Each payment supports one refund request, keyed by its `paymentInfoHash`. ```typescript import { RequestStatus } from '@x402r/core'; // RequestStatus values: RequestStatus.Pending // 0 - Awaiting decision -RequestStatus.Approved // 1 - Approved by merchant/arbiter +RequestStatus.Approved // 1 - Auto-approved via refundInEscrow RequestStatus.Denied // 2 - Denied by merchant/arbiter RequestStatus.Cancelled // 3 - Cancelled by payer ``` -### Refund Flow +### Refund flow + +In v3, RefundRequest is wired as an IRecorder plugin. Refund approval happens automatically when the merchant or arbiter calls `refundInEscrow()` on the operator — no separate approve step is needed. ```mermaid sequenceDiagram participant P as Payer - participant R as RefundRequest Contract + participant R as RefundRequest (IRecorder) participant M as Merchant + participant O as PaymentOperator participant A as Arbiter - P->>R: requestRefund(paymentInfo, amount, nonce) + P->>R: requestRefund(paymentInfo, amount) R-->>M: RefundRequested event - alt Merchant approves - M->>R: approveRefundRequest(paymentInfo, nonce) - M->>R: refundInEscrow(paymentInfo, amount) + alt Merchant refunds + M->>O: refundInEscrow(paymentInfo, amount) + O->>R: record() auto-approves pending request + O->>P: Funds returned else Merchant denies - M->>R: denyRefundRequest(paymentInfo, nonce) + M->>R: denyRefundRequest(paymentInfo) else Escalate to arbiter - A->>R: approveRefundRequest(paymentInfo, nonce) - A->>R: executeRefundInEscrow(paymentInfo, amount) + A->>O: refundInEscrow(paymentInfo, amount) + O->>R: record() auto-approves pending request + O->>P: Funds returned end ``` -### The Nonce Parameter - -All refund methods require a `nonce` parameter. This is the record index from the PaymentIndexRecorder that identifies which charge the refund request applies to. For the first charge, use `0n`. - ```typescript -// Request refund for the first charge -await client.requestRefund(paymentInfo, amount, 0n); +// Request a refund (one per payment) +await client.refund.request(paymentInfo, amount); -// Check status for the first charge -const status = await client.getRefundStatus(paymentInfo, 0n); +// Check status +const status = await client.refund.getStatus(paymentInfo); ``` ## Freeze / Unfreeze @@ -130,14 +131,14 @@ const status = await client.getRefundStatus(paymentInfo, 0n); The **Freeze** contract allows payers to freeze a payment during the escrow period, preventing release until the freeze expires or is lifted: ```typescript -// Payer freezes payment (requires payer authorization) -await client.freezePayment(paymentInfo, freezeAddress); +// Payer freezes payment (requires freezeAddress in config) +await client.freeze.freeze(paymentInfo); -// Merchant unfreezes payment (requires receiver authorization) -await merchant.unfreezePayment(paymentInfo, freezeAddress); +// Merchant unfreezes payment +await merchant.freeze.unfreeze(paymentInfo); // Check frozen status -const frozen = await client.isFrozen(paymentInfo, freezeAddress); +const frozen = await client.freeze.isFrozen(paymentInfo); ``` @@ -149,8 +150,8 @@ Freezing a payment does not automatically escalate to an arbiter. It pauses the | Role | Can Do | |------|--------| | **Payer** | Request refunds, freeze payments, cancel requests, query escrow state | -| **Merchant** | Release payments, charge, approve/deny refunds, unfreeze payments | -| **Arbiter** | Approve/deny disputed refunds, execute refunds, batch operations, registry | +| **Merchant** | Release payments, charge, refund in escrow (auto-approves requests), deny refunds | +| **Arbiter** | Deny/refuse disputed refunds, refund in escrow, review evidence | ## Contract Architecture @@ -167,9 +168,10 @@ flowchart TB subgraph Components[Supporting Contracts] direction LR - subgraph RR[RefundRequest] + subgraph RR[RefundRequest IRecorder] rr1[Request/Cancel] - rr2[Approve/Deny] + rr2[Deny/Refuse] + rr3[Auto-approve on refund] end subgraph EP[EscrowPeriod] ep1[Track auth time] diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index a97990b..4280541 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -16,12 +16,12 @@ A complete marketplace operator deployment includes: 1. **EscrowPeriod** — Records authorization time, enforces waiting period before release 2. **Freeze** — Allows payer to freeze payment during escrow, receiver to unfreeze -3. **StaticAddressCondition** — Restricts refund approval to the designated arbiter -4. **OrCondition** — Allows either the receiver OR the arbiter to approve in-escrow refunds +3. **ReceiverCondition** — Gates in-escrow refunds to the merchant (receiver) +4. **RefundRequest (IRecorder)** — Wired as `refundInEscrowRecorder`, auto-approves pending refund requests during `refundInEscrow()` 5. **StaticFeeCalculator** — Optional operator fee (basis points) 6. **PaymentOperator** — The main contract tying everything together -All contracts are deployed via factories using CREATE2, so identical configurations produce identical addresses across deployments. +All contracts are deployed via factories using CREATE3, so identical configurations produce identical addresses across all supported chains. ## Deploy Your Operator @@ -143,8 +143,8 @@ interface MarketplaceOperatorDeployment { operatorAddress: Address; // The PaymentOperator escrowPeriodAddress: Address; // EscrowPeriod recorder/condition freezeAddress: Address; // Freeze condition - arbiterConditionAddress: Address; // StaticAddressCondition for arbiter - refundInEscrowCondition: Address; // OR(Receiver, Arbiter) + refundInEscrowCondition: Address; // ReceiverCondition (merchant-gated) + refundInEscrowRecorder: Address; // RefundRequest (auto-approve on refund) feeCalculatorAddress: Address | null; // null if no fee txHashes: Hash[]; // All deployment tx hashes summary: { @@ -155,7 +155,7 @@ interface MarketplaceOperatorDeployment { ``` -Because all contracts use CREATE2, redeploying with the same parameters is idempotent — it will detect existing contracts and skip them. The `summary` tells you what was new vs reused. +Because all contracts use CREATE3, redeploying with the same parameters is idempotent — it will detect existing contracts and skip them. The `summary` tells you what was new vs reused. ## Preview Addresses (No Deploy) @@ -189,7 +189,8 @@ The deployed operator has the following slot configuration: | `AUTHORIZE_RECORDER` | EscrowPeriod | Records authorization timestamp | | `CHARGE_CONDITION` | (none) | No restrictions on charge | | `RELEASE_CONDITION` | EscrowPeriod | Blocks release during escrow period | -| `REFUND_IN_ESCROW_CONDITION` | OR(Receiver, Arbiter) | Receiver or arbiter can approve | +| `REFUND_IN_ESCROW_CONDITION` | ReceiverCondition | Only the receiver (merchant) can call | +| `REFUND_IN_ESCROW_RECORDER` | RefundRequest | Auto-approves pending requests during refund | | `REFUND_POST_ESCROW_CONDITION` | Receiver | Only receiver after escrow | | `FEE_CALCULATOR` | StaticFeeCalculator | Fixed percentage fee | | `FEE_RECIPIENT` | Your address | Receives fees | @@ -201,16 +202,18 @@ Deployment is supported on all configured networks: | Network | Chain ID | EIP-155 ID | |---------|----------|------------| | Base Sepolia | 84532 | `eip155:84532` | -| Base Mainnet | 8453 | `eip155:8453` | +| Base | 8453 | `eip155:8453` | | Ethereum | 1 | `eip155:1` | | Ethereum Sepolia | 11155111 | `eip155:11155111` | -| Arbitrum Sepolia | 421614 | `eip155:421614` | -| Polygon | 137 | `eip155:137` | | Arbitrum | 42161 | `eip155:42161` | +| Arbitrum Sepolia | 421614 | `eip155:421614` | | Optimism | 10 | `eip155:10` | -| Avalanche | 43114 | `eip155:43114` | +| Polygon | 137 | `eip155:137` | | Celo | 42220 | `eip155:42220` | +| Avalanche | 43114 | `eip155:43114` | +| Linea | 59144 | `eip155:59144` | | Monad | 143 | `eip155:143` | +| SKALE Base | 1187947933 | `eip155:1187947933` | Deployment requires gas fees. Ensure your wallet has ETH on the target network. On Base Sepolia, you can get testnet ETH from [Base network faucets](https://docs.base.org/base-chain/tools/network-faucets). diff --git a/sdk/helpers/refundable.mdx b/sdk/helpers/refundable.mdx index 1d5fbfe..b2d8d77 100644 --- a/sdk/helpers/refundable.mdx +++ b/sdk/helpers/refundable.mdx @@ -121,7 +121,7 @@ const option = refundable({ ## Supported Networks -The function resolves addresses from the network config for all supported networks. See `getNetworkConfig()` for the full list (Base Sepolia, Base, Ethereum, Ethereum Sepolia, Arbitrum Sepolia, Polygon, Arbitrum, Optimism, Avalanche, Celo, Monad). +The function resolves addresses from the chain config for all supported networks. See `getChainConfig()` for the full list (Base Sepolia, Base, Ethereum, Ethereum Sepolia, Arbitrum, Arbitrum Sepolia, Optimism, Polygon, Celo, Avalanche, Linea, Monad, SKALE Base). ```typescript refundable({ network: 'eip155:84532', ... }, '0x...'); // Base Sepolia diff --git a/sdk/installation.mdx b/sdk/installation.mdx index 7485080..a54373b 100644 --- a/sdk/installation.mdx +++ b/sdk/installation.mdx @@ -9,19 +9,9 @@ icon: "download" Install only the packages you need for your use case: - + ```bash - npm install @x402r/client @x402r/core viem - ``` - - - ```bash - npm install @x402r/merchant @x402r/helpers @x402r/core viem - ``` - - - ```bash - npm install @x402r/arbiter @x402r/core viem + npm install @x402r/sdk @x402r/core viem ``` @@ -31,27 +21,29 @@ Install only the packages you need for your use case: -## Setup viem Clients +The `@x402r/sdk` package includes factory functions for all roles (payer, merchant, arbiter). + +## Setup viem clients -Create `publicClient` and `walletClient` using [viem](https://viem.sh/docs/clients/public). All SDK classes require these as constructor arguments. +Create `publicClient` and `walletClient` using [viem](https://viem.sh/docs/clients/public). All SDK factory functions require these as config arguments. -## Contract Addresses +## Contract addresses -Get the deployed contract addresses from the network config: +Get the deployed contract addresses from the chain config: ```typescript -import { getNetworkConfig } from '@x402r/core'; +import { getChainConfig } from '@x402r/core'; -const config = getNetworkConfig('eip155:84532'); // Base Sepolia +const config = getChainConfig(84532); // Base Sepolia console.log(config.authCaptureEscrow); // Escrow contract -console.log(config.refundRequest); // RefundRequest contract console.log(config.arbiterRegistry); // ArbiterRegistry contract -console.log(config.usdc); // USDC token address +console.log(config.usdc); // USDC token address +console.log(config.factories); // Factory addresses ``` -Network identifiers use the [EIP-155](https://eips.ethereum.org/EIPS/eip-155) format: `eip155:`. For Base Sepolia, use `'eip155:84532'`. For Base Mainnet, use `'eip155:8453'`. +`getChainConfig()` accepts a numeric chain ID (e.g., `84532` for Base Sepolia). All v3 protocol addresses are identical across every supported chain thanks to CREATE3 deployment. diff --git a/sdk/limitations.mdx b/sdk/limitations.mdx index 7e3a23c..ae1904b 100644 --- a/sdk/limitations.mdx +++ b/sdk/limitations.mdx @@ -8,38 +8,43 @@ The SDK provides full coverage of core payment flows including authorization, re ## API Constraints -### EIP-155 Network Identifiers +### Chain ID lookups -Network configuration requires EIP-155 format strings, not chain ID numbers: +Network configuration uses numeric chain IDs: ```typescript // Correct -const config = getNetworkConfig('eip155:84532'); +const config = getChainConfig(84532); -// Incorrect - will return undefined -const config = getNetworkConfig(84532); +// Incorrect - EIP-155 strings are no longer used for chain config +// const config = getNetworkConfig('eip155:84532'); ``` -### PaymentInfo Must Be Complete +Use `toNetworkId(chainId)` and `fromNetworkId(networkId)` to convert between numeric chain IDs and EIP-155 format strings when needed. -All SDK methods require a complete `PaymentInfo` object. You cannot query by hash alone: +### PaymentInfo must be complete + +Most SDK methods require a complete `PaymentInfo` object. You can use `refund.getStoredPaymentInfo(paymentInfoHash)` to retrieve stored PaymentInfo from the RefundRequest contract, or use the query plugin to look up payments by hash: ```typescript -// Works - full PaymentInfo -const status = await client.getRefundStatus(paymentInfo, 0n); +// Retrieve stored PaymentInfo from RefundRequest contract +const paymentInfo = await client.refund.getStoredPaymentInfo(paymentInfoHash); -// Not supported - hash-only queries require the full struct -// const state = await client.getPaymentStateByHash(hash); +// Or use the query plugin +const paymentInfo = await client.query.getPayment(paymentInfoHash); ``` ### Event Log Scanning Limits -`getPayerPayments()`, `getReceiverPayments()`, and `getPaymentDetails()` scan `AuthorizationCreated` events using `eth_getLogs`. Base Sepolia RPCs typically limit responses to 10,000 blocks. Pass a `fromBlock` parameter for large ranges: +The event-based query provider scans `AuthorizationCreated` and `ChargeExecuted` events using `eth_getLogs`. Base Sepolia RPCs typically limit responses to 10,000 blocks. Configure `eventFromBlock` in your client config to set the scan start: ```typescript -// Scan only recent blocks to avoid RPC limits -const payments = await client.getPayerPayments(recentBlockNumber); -const details = await client.getPaymentDetails(hash, recentBlockNumber); +const client = createPayerClient({ + publicClient, + walletClient, + operatorAddress: '0x...', + eventFromBlock: recentBlockNumber, // Required to enable event fallback provider +}); ``` ### No Express/Hono Middleware diff --git a/sdk/merchant/payment-operations.mdx b/sdk/merchant/payment-operations.mdx index e5bdd20..b6aa703 100644 --- a/sdk/merchant/payment-operations.mdx +++ b/sdk/merchant/payment-operations.mdx @@ -1,66 +1,64 @@ --- -title: "Payment Operations" +title: "Payment operations" description: "Release funds, charge payments, process refunds, and query escrow state with the Merchant SDK" icon: "coins" --- -The `X402rMerchant` class provides methods for managing the full payment lifecycle: releasing escrowed funds, charging directly for subscriptions, processing refunds, and querying operator configuration. +The merchant client provides methods for managing the full payment lifecycle: releasing escrowed funds, charging directly for subscriptions, processing refunds, and querying operator configuration. -## Payment Operations +## Payment operations -### Release Funds from Escrow +### Release funds from escrow -Use `release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required** and specifies the exact amount to release in token units. +Use `payment.release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required**. ```typescript -import { X402rMerchant } from '@x402r/merchant'; - // Release 10 USDC (6 decimals) from escrow -const { txHash } = await merchant.release(paymentInfo, BigInt('10000000')); +const txHash = await merchant.payment.release(paymentInfo, BigInt('10000000')); console.log('Released:', txHash); ``` -For partial releases, specify a smaller amount. The remaining funds stay in escrow and can be released or refunded later. +For partial releases, specify a smaller amount. The remaining funds stay in escrow. ```typescript // Release 3 USDC of a 10 USDC escrow -const { txHash } = await merchant.release(paymentInfo, BigInt('3000000')); +const txHash = await merchant.payment.release(paymentInfo, BigInt('3000000')); console.log('Partial release:', txHash); // Check what remains -const { capturableAmount } = await merchant.getPaymentAmounts(paymentInfo); -console.log('Remaining in escrow:', capturableAmount); // 7000000n +const amounts = await merchant.payment.getAmounts(paymentInfo); +console.log('Remaining in escrow:', amounts.capturableAmount); // 7000000n ``` -The `amount` parameter is always required. There is no default "release all" behavior. Always query `getPaymentAmounts()` first to determine the available capturable amount. +The `amount` parameter is always required. There is no default "release all" behavior. Always query `payment.getAmounts()` first to determine the available capturable amount. -### Refund While in Escrow +### Refund while in escrow -Use `refundInEscrow()` to return escrowed funds to the payer before release. The `amount` parameter is **required**. +Use `payment.refundInEscrow()` to return escrowed funds to the payer. This also auto-approves any pending RefundRequest. ```typescript // Full refund of 10 USDC -const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('10000000')); +const txHash = await merchant.payment.refundInEscrow(paymentInfo, BigInt('10000000')); console.log('Refunded from escrow:', txHash); ``` ```typescript // Partial refund: return 2 USDC, keep 8 USDC in escrow -const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('2000000')); +const txHash = await merchant.payment.refundInEscrow(paymentInfo, BigInt('2000000')); console.log('Partial refund:', txHash); ``` -### Charge Directly +### Charge directly -Use `charge()` for non-escrow flows such as subscriptions or session-based payments. This pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`). +Use `payment.charge()` for non-escrow flows such as subscriptions or session-based payments. ```typescript const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...'; const collectorData: `0x${string}` = '0xSignatureOrCalldata...'; -const { txHash } = await merchant.charge( +const txHash = await merchant.payment.charge( paymentInfo, BigInt('5000000'), // 5 USDC tokenCollectorAddress, // token collector contract @@ -73,15 +71,15 @@ console.log('Charged:', txHash); The `charge()` method is designed for recurring payments and session-based billing where funds are not pre-escrowed. The token collector contract handles the actual token transfer. -### Refund After Release (Post-Escrow) +### Refund after release (post-escrow) -Use `refundPostEscrow()` to refund funds that have already been released to the receiver. This requires a token collector to source the refund from the merchant's balance. +Use `payment.refundPostEscrow()` to refund funds that have already been released to the receiver. ```typescript const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...'; const collectorData: `0x${string}` = '0xSignatureOrCalldata...'; -const { txHash } = await merchant.refundPostEscrow( +const txHash = await merchant.payment.refundPostEscrow( paymentInfo, BigInt('5000000'), // 5 USDC to refund tokenCollectorAddress, // token collector that sources the refund @@ -94,167 +92,80 @@ console.log('Post-escrow refund:', txHash); Post-escrow refunds require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer. -## Query Methods - -### Get Payment Amounts +## Query methods -Use `getPaymentAmounts()` to query the current capturable and refundable amounts for a payment. This method reads directly from the escrow contract. +### Get payment amounts ```typescript -const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo); +const amounts = await merchant.payment.getAmounts(paymentInfo); -console.log('Capturable:', capturableAmount); // Funds available to release -console.log('Refundable:', refundableAmount); // Funds available to refund +console.log('Capturable:', amounts.capturableAmount); // Funds available to release +console.log('Refundable:', amounts.refundableAmount); // Funds available to refund -if (capturableAmount > 0n) { - // Release available funds - const { txHash } = await merchant.release(paymentInfo, capturableAmount); +if (amounts.capturableAmount > 0n) { + const txHash = await merchant.payment.release(paymentInfo, amounts.capturableAmount); console.log('Released all capturable funds:', txHash); } ``` - -`getPaymentAmounts()` requires the `escrowAddress` to be configured when creating the `X402rMerchant` instance. - - -### Get Operator Configuration +### Get operator configuration -Use `getOperatorConfig()` to retrieve all 14 immutable slot addresses from the PaymentOperator contract. This includes the escrow address, fee configuration, all 5 condition slots, and all 5 recorder slots. +Use `operator.getConfig()` to retrieve all immutable slot addresses from the PaymentOperator contract, including conditions and recorders. ```typescript -const config = await merchant.getOperatorConfig(); +const config = await merchant.operator.getConfig(); // Core state -console.log('Escrow:', config.escrow); console.log('Fee recipient:', config.feeRecipient); console.log('Fee calculator:', config.feeCalculator); -console.log('Protocol fee config:', config.protocolFeeConfig); // Condition slots (address(0) = always allow) -console.log('Authorize condition:', config.authorizeCondition); -console.log('Charge condition:', config.chargeCondition); console.log('Release condition:', config.releaseCondition); console.log('Refund in-escrow condition:', config.refundInEscrowCondition); -console.log('Refund post-escrow condition:', config.refundPostEscrowCondition); // Recorder slots (address(0) = no-op) -console.log('Authorize recorder:', config.authorizeRecorder); -console.log('Charge recorder:', config.chargeRecorder); -console.log('Release recorder:', config.releaseRecorder); console.log('Refund in-escrow recorder:', config.refundInEscrowRecorder); -console.log('Refund post-escrow recorder:', config.refundPostEscrowRecorder); ``` -### Get Fee Structure +### Get fee addresses -Use `getFeeStructure()` to retrieve the fee-related addresses for the operator. This is a lighter alternative to `getOperatorConfig()` when you only need fee information. +Use `operator.getFeeAddresses()` for the fee-related addresses. This is a lighter alternative to `operator.getConfig()`. ```typescript -const fees = await merchant.getFeeStructure(); +const fees = await merchant.operator.getFeeAddresses(); console.log('Fee calculator:', fees.feeCalculator); console.log('Protocol fee config:', fees.protocolFeeConfig); console.log('Fee recipient:', fees.feeRecipient); ``` -The returned `FeeStructure` contains three fields: - -| Field | Type | Description | -|-------|------|-------------| -| `feeCalculator` | `0x${string}` | Contract that computes fee amounts | -| `protocolFeeConfig` | `0x${string}` | Protocol-level fee configuration | -| `feeRecipient` | `0x${string}` | Address that receives the operator's fee share | - -### Get Release Conditions - -Use `getReleaseConditions()` to check which condition contract governs release operations. A zero address means releases are always allowed. - -```typescript -const releaseCondition = await merchant.getReleaseConditions(); - -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; -if (releaseCondition === ZERO_ADDRESS) { - console.log('No release conditions configured - releases always allowed'); -} else { - console.log('Release condition contract:', releaseCondition); -} -``` - -### Get Payment State - -Use `getPaymentState()` to derive the lifecycle state of a payment from the escrow contract. - -```typescript -import { PaymentState } from '@x402r/core'; - -const state = await merchant.getPaymentState(paymentInfo); -// PaymentState: NonExistent, InEscrow, Released, Settled, or Expired -``` - -```typescript -getPaymentState(paymentInfo: PaymentInfo): Promise -``` - -### Get Receiver Payments - -Use `getReceiverPayments()` to list all payments where the connected wallet is the receiver, by scanning `AuthorizationCreated` events. - -```typescript -const payments = await merchant.getReceiverPayments(); - -for (const { hash, paymentInfo } of payments) { - console.log(`Payment ${hash}: ${paymentInfo.maxAmount}`); -} -``` - -```typescript -getReceiverPayments( - fromBlock?: bigint -): Promise> -``` - - -This method scans event logs. Pass `fromBlock` to limit the scan range if your RPC limits `eth_getLogs` responses (Base Sepolia typically caps at 10,000 blocks). - - -### Get Payment Details - -Use `getPaymentDetails()` to retrieve the full `PaymentInfo` struct by scanning `AuthorizationCreated` events for a given hash. - -```typescript -const details = await merchant.getPaymentDetails(paymentInfoHash); -console.log('Payer:', details.payer); -console.log('Amount:', details.maxAmount); -``` +### Get payment state ```typescript -getPaymentDetails( - paymentInfoHash: `0x${string}`, - fromBlock?: bigint -): Promise +const [hasCollected, capturableAmount, refundableAmount] = await merchant.payment.getState(paymentInfo); ``` -## Release vs Refund Decision Flow +## Release vs refund decision flow ```mermaid flowchart TD - A[Payment in Escrow] --> B{Check getPaymentAmounts} + A[Payment in Escrow] --> B{Check payment.getAmounts} B --> C{capturableAmount > 0?} C -->|Yes| D{Has refund request?} C -->|No| E[Nothing to release] D -->|No| F[Safe to release] D -->|Yes| G{Approve refund?} - F --> H["release(paymentInfo, amount)"] - G -->|Yes| I["refundInEscrow(paymentInfo, amount)"] - G -->|No| J[Deny request, then release] + F --> H["payment.release(paymentInfo, amount)"] + G -->|Yes| I["payment.refundInEscrow(paymentInfo, amount)"] + G -->|No| J["refund.deny(paymentInfo), then release"] J --> H ``` -## Next Steps +## Next steps - Process incoming refund requests with approve/deny workflows. + Process incoming refund requests with deny workflows. Watch for real-time payment and refund events. diff --git a/sdk/merchant/quickstart.mdx b/sdk/merchant/quickstart.mdx index 4929d3f..dae3664 100644 --- a/sdk/merchant/quickstart.mdx +++ b/sdk/merchant/quickstart.mdx @@ -1,19 +1,19 @@ --- title: "Merchant SDK" -description: "Release funds, charge payments, process refunds, and query escrow state with @x402r/merchant" +description: "Release funds, charge payments, process refunds, and query escrow state with @x402r/sdk" icon: "rocket" --- -The `@x402r/merchant` package provides everything merchants need for the post-payment lifecycle: releasing escrowed funds, charging directly, processing refunds, and querying operator state. +The `@x402r/sdk` package provides everything merchants need for the post-payment lifecycle: releasing escrowed funds, charging directly, processing refunds, and querying operator state. -**Looking for server setup?** The [Merchant Server Quickstart](/sdk/merchant/getting-started) shows how to accept escrow payments via Express middleware. This page covers the `X402rMerchant` class for managing payments after they arrive. +**Looking for server setup?** The [Merchant Server Quickstart](/sdk/merchant/getting-started) shows how to accept escrow payments via Express middleware. This page covers the merchant client for managing payments after they arrive. ## Installation ```bash -npm install @x402r/merchant @x402r/helpers @x402r/core viem +npm install @x402r/sdk @x402r/helpers @x402r/core viem ``` ## Setup @@ -21,27 +21,27 @@ npm install @x402r/merchant @x402r/helpers @x402r/core viem Create viem clients as described in [Installation](/sdk/installation), then: ```typescript -import { X402rMerchant } from '@x402r/merchant'; -import { getNetworkConfig } from '@x402r/core'; +import { createMerchantClient } from '@x402r/sdk'; -const config = getNetworkConfig('eip155:84532')!; - -const merchant = new X402rMerchant({ +const merchant = createMerchantClient({ publicClient, walletClient, operatorAddress: '0x...', // Your PaymentOperator address - escrowAddress: config.authCaptureEscrow, - refundRequestAddress: config.refundRequest, + // Optional: enable escrow and freeze features + escrowPeriodAddress: '0x...', + freezeAddress: '0x...', }); ``` -## Release Funds from Escrow +Refund request and evidence addresses are auto-resolved from the chain config. + +## Release funds from escrow -Use `release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required** and specifies the exact amount to release in token units. +Use `payment.release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required** and specifies the exact amount to release in token units. ```typescript // Release 10 USDC (6 decimals) from escrow -const { txHash } = await merchant.release(paymentInfo, BigInt('10000000')); +const txHash = await merchant.payment.release(paymentInfo, BigInt('10000000')); console.log('Released:', txHash); ``` @@ -49,43 +49,47 @@ For partial releases, specify a smaller amount. The remaining funds stay in escr ```typescript // Release 3 USDC of a 10 USDC escrow -const { txHash } = await merchant.release(paymentInfo, BigInt('3000000')); +const txHash = await merchant.payment.release(paymentInfo, BigInt('3000000')); console.log('Partial release:', txHash); // Check what remains -const { capturableAmount } = await merchant.getPaymentAmounts(paymentInfo); -console.log('Remaining in escrow:', capturableAmount); // 7000000n +const amounts = await merchant.payment.getAmounts(paymentInfo); +console.log('Remaining in escrow:', amounts.capturableAmount); // 7000000n ``` -The `amount` parameter is always required. There is no default "release all" behavior. Always query `getPaymentAmounts()` first to determine the available capturable amount. +The `amount` parameter is always required. There is no default "release all" behavior. Always query `payment.getAmounts()` first to determine the available capturable amount. -## Refund While in Escrow +## Refund while in escrow -Use `refundInEscrow()` to return escrowed funds to the payer before release. The `amount` parameter is **required**. +Use `payment.refundInEscrow()` to return escrowed funds to the payer before release. This also auto-approves any pending RefundRequest for this payment. ```typescript // Full refund of 10 USDC -const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('10000000')); +const txHash = await merchant.payment.refundInEscrow(paymentInfo, BigInt('10000000')); console.log('Refunded from escrow:', txHash); ``` ```typescript // Partial refund: return 2 USDC, keep 8 USDC in escrow -const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('2000000')); +const txHash = await merchant.payment.refundInEscrow(paymentInfo, BigInt('2000000')); console.log('Partial refund:', txHash); ``` -## Charge Directly + +In v3, calling `refundInEscrow()` automatically approves any pending RefundRequest for this payment via the IRecorder plugin. You do not need a separate approve step. + + +## Charge directly -Use `charge()` for non-escrow flows such as subscriptions or session-based payments. This pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`). +Use `payment.charge()` for non-escrow flows such as subscriptions or session-based payments. This pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`). ```typescript const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...'; const collectorData: `0x${string}` = '0xSignatureOrCalldata...'; -const { txHash } = await merchant.charge( +const txHash = await merchant.payment.charge( paymentInfo, BigInt('5000000'), // 5 USDC tokenCollectorAddress, // token collector contract @@ -98,15 +102,15 @@ console.log('Charged:', txHash); The `charge()` method is designed for recurring payments and session-based billing where funds are not pre-escrowed. The token collector contract handles the actual token transfer. -## Refund After Release (Post-Escrow) +## Refund after release (post-escrow) -Use `refundPostEscrow()` to refund funds that have already been released to the receiver. This requires a token collector to source the refund from the merchant's balance. +Use `payment.refundPostEscrow()` to refund funds that have already been released to the receiver. This requires a token collector to source the refund from the merchant's balance. ```typescript const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...'; const collectorData: `0x${string}` = '0xSignatureOrCalldata...'; -const { txHash } = await merchant.refundPostEscrow( +const txHash = await merchant.payment.refundPostEscrow( paymentInfo, BigInt('5000000'), // 5 USDC to refund tokenCollectorAddress, // token collector that sources the refund @@ -119,118 +123,73 @@ console.log('Post-escrow refund:', txHash); Post-escrow refunds require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer. -## Query Methods - -### Get Payment Amounts +## Query methods -Use `getPaymentAmounts()` to query the current capturable and refundable amounts for a payment. +### Get payment amounts ```typescript -const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo); +const amounts = await merchant.payment.getAmounts(paymentInfo); -console.log('Capturable:', capturableAmount); // Funds available to release -console.log('Refundable:', refundableAmount); // Funds available to refund +console.log('Capturable:', amounts.capturableAmount); // Funds available to release +console.log('Refundable:', amounts.refundableAmount); // Funds available to refund -if (capturableAmount > 0n) { - const { txHash } = await merchant.release(paymentInfo, capturableAmount); +if (amounts.capturableAmount > 0n) { + const txHash = await merchant.payment.release(paymentInfo, amounts.capturableAmount); console.log('Released all capturable funds:', txHash); } ``` - -`getPaymentAmounts()` requires the `escrowAddress` to be configured when creating the `X402rMerchant` instance. - - -### Get Payment State - -Use `getPaymentState()` to derive the lifecycle state of a payment from the escrow contract. - -```typescript -import { PaymentState } from '@x402r/core'; - -const state = await merchant.getPaymentState(paymentInfo); -// PaymentState: NonExistent, InEscrow, Released, Settled, or Expired -``` - -### Get Receiver Payments - -Use `getReceiverPayments()` to list all payments where the connected wallet is the receiver. - -```typescript -const payments = await merchant.getReceiverPayments(); - -for (const { hash, paymentInfo } of payments) { - console.log(`Payment ${hash}: ${paymentInfo.maxAmount}`); -} -``` - - -This method scans event logs. Pass `fromBlock` to limit the scan range if your RPC limits `eth_getLogs` responses (Base Sepolia typically caps at 10,000 blocks). - - -### Get Payment Details - -Use `getPaymentDetails()` to retrieve the full `PaymentInfo` struct by scanning `AuthorizationCreated` events for a given hash. +### Get payment state ```typescript -const details = await merchant.getPaymentDetails(paymentInfoHash); -console.log('Payer:', details.payer); -console.log('Amount:', details.maxAmount); +const [hasCollected, capturableAmount, refundableAmount] = await merchant.payment.getState(paymentInfo); ``` -### Get Operator Configuration +### Get operator configuration -Use `getOperatorConfig()` to retrieve all 14 immutable slot addresses from the PaymentOperator contract. +Use `operator.getConfig()` to retrieve all immutable slot addresses from the PaymentOperator contract. ```typescript -const config = await merchant.getOperatorConfig(); +const config = await merchant.operator.getConfig(); -console.log('Escrow:', config.escrow); console.log('Fee recipient:', config.feeRecipient); console.log('Fee calculator:', config.feeCalculator); console.log('Release condition:', config.releaseCondition); ``` -### Get Fee Structure +### Get fee structure -Use `getFeeStructure()` for just the fee-related addresses — a lighter alternative to `getOperatorConfig()`. +Use `operator.getFeeAddresses()` for just the fee-related addresses. ```typescript -const fees = await merchant.getFeeStructure(); +const fees = await merchant.operator.getFeeAddresses(); console.log('Fee calculator:', fees.feeCalculator); console.log('Protocol fee config:', fees.protocolFeeConfig); console.log('Fee recipient:', fees.feeRecipient); ``` -### Get Release Conditions - -```typescript -const releaseCondition = await merchant.getReleaseConditions(); -// address(0) means releases are always allowed -``` - -## Release vs Refund Decision Flow +## Release vs refund decision flow ```mermaid flowchart TD - A[Payment in Escrow] --> B{Check getPaymentAmounts} + A[Payment in Escrow] --> B{Check payment.getAmounts} B --> C{capturableAmount > 0?} C -->|Yes| D{Has refund request?} C -->|No| E[Nothing to release] D -->|No| F[Safe to release] D -->|Yes| G{Approve refund?} - F --> H["release(paymentInfo, amount)"] - G -->|Yes| I["refundInEscrow(paymentInfo, amount)"] - G -->|No| J[Deny request, then release] + F --> H["payment.release(paymentInfo, amount)"] + G -->|Yes| I["payment.refundInEscrow(paymentInfo, amount)"] + G -->|No| J["refund.deny(paymentInfo), then release"] J --> H ``` -## Next Steps +## Next steps - Process incoming refund requests with approve/deny workflows. + Process incoming refund requests with deny workflows. Mark payment options as refundable with your operator. diff --git a/sdk/merchant/refund-handling.mdx b/sdk/merchant/refund-handling.mdx index 02e8b31..bc298c6 100644 --- a/sdk/merchant/refund-handling.mdx +++ b/sdk/merchant/refund-handling.mdx @@ -1,23 +1,21 @@ --- -title: "Refund Handling" -description: "Process, approve, deny, and manage refund requests with the Merchant SDK" +title: "Refund handling" +description: "Process, deny, and manage refund requests with the Merchant SDK" icon: "rotate-left" --- -The `X402rMerchant` class provides a complete set of methods for handling refund requests from payers. Every refund-related method requires a `nonce: bigint` parameter that identifies which specific charge the refund targets. +The merchant client provides methods for handling refund requests from payers. In v3, refund approval happens automatically when you call `payment.refundInEscrow()` — the RefundRequest IRecorder plugin auto-approves any pending request during execution. -The `nonce` parameter corresponds to the record index from the `PaymentIndexRecorder`. For the first charge against a payment, the nonce is `0n`. Each subsequent charge increments the nonce. +Each payment supports one refund request, keyed by `paymentInfoHash`. There is no nonce parameter — one request per payment. -## Refund Request Queries +## Refund request queries -### Check If a Refund Request Exists - -Use `hasRefundRequest()` to check whether a payer has submitted a refund request for a specific payment and nonce. +### Check if a refund request exists ```typescript -const hasRequest = await merchant.hasRefundRequest(paymentInfo, 0n); +const hasRequest = await merchant.refund.has(paymentInfo); if (hasRequest) { console.log('Refund request exists for this payment'); @@ -26,21 +24,19 @@ if (hasRequest) { } ``` -### Get Refund Request Status - -Use `getRefundStatus()` to retrieve the current status of a refund request. Returns a `RequestStatus` enum value. +### Get refund request status ```typescript import { RequestStatus } from '@x402r/core'; -const status = await merchant.getRefundStatus(paymentInfo, 0n); +const status = await merchant.refund.getStatus(paymentInfo); switch (status) { case RequestStatus.Pending: console.log('Awaiting your decision'); break; case RequestStatus.Approved: - console.log('You approved this refund'); + console.log('Auto-approved via refundInEscrow'); break; case RequestStatus.Denied: console.log('You denied this refund'); @@ -51,118 +47,59 @@ switch (status) { } ``` -### Get Full Refund Request Data - -Use `getRefundRequest()` to retrieve the complete refund request data, including the amount and status. +### Get full refund request data ```typescript -import type { RefundRequestData } from '@x402r/core'; - -const request: RefundRequestData = await merchant.getRefundRequest(paymentInfo, 0n); +const request = await merchant.refund.get(paymentInfo); console.log('Payment hash:', request.paymentInfoHash); -console.log('Nonce:', request.nonce); console.log('Requested amount:', request.amount); console.log('Status:', request.status); ``` -The `RefundRequestData` type contains: - -| Field | Type | Description | -|-------|------|-------------| -| `paymentInfoHash` | `0x${string}` | Hash of the PaymentInfo struct | -| `nonce` | `bigint` | Record index this refund targets | -| `amount` | `bigint` | Amount requested for refund (uint120) | -| `status` | `RequestStatus` | Current status (Pending, Approved, Denied, Cancelled) | - -### Get Refund Request by Composite Key +### Get refund request by key -Use `getRefundRequestByKey()` to look up a refund request directly by its composite key (the `keccak256(paymentInfoHash, nonce)` value returned from paginated queries). +Use `getByKey()` to look up a refund request directly by its `paymentInfoHash` (returned from paginated queries). ```typescript -const request = await merchant.getRefundRequestByKey(compositeKey); +const request = await merchant.refund.getByKey(paymentInfoHash); console.log('Amount:', request.amount); console.log('Status:', request.status); ``` -## Paginated Refund Request Listing +## Paginated refund request listing -### Get Pending Refund Requests +### Get refund requests for a receiver -Use `getPendingRefundRequests()` to retrieve paginated refund request keys for the current receiver address. This method uses the wallet address associated with your `X402rMerchant` instance. +Use `getReceiverRequests()` to retrieve paginated refund request keys for a receiver address. ```typescript +const receiverAddress = walletClient.account.address; + // Get the first 10 refund request keys -const { keys, total } = await merchant.getPendingRefundRequests(0n, 10n); +const { keys, total } = await merchant.refund.getReceiverRequests( + receiverAddress, + 0n, // offset + 10n // count +); console.log(`Showing ${keys.length} of ${total} total refund requests`); -// Look up each request by its composite key for (const key of keys) { - const request = await merchant.getRefundRequestByKey(key); - console.log(`Key: ${key}`); - console.log(` Amount: ${request.amount}`); - console.log(` Status: ${request.status}`); -} -``` - -For pagination, adjust the `offset` and `count` parameters: - -```typescript -// Page through all refund requests, 20 at a time -const pageSize = 20n; -let offset = 0n; -let hasMore = true; - -while (hasMore) { - const { keys, total } = await merchant.getPendingRefundRequests(offset, pageSize); - - for (const key of keys) { - const request = await merchant.getRefundRequestByKey(key); - // Process each request... - } - - offset += pageSize; - hasMore = offset < total; -} -``` - -### Get Refund Request Count - -Use `getRefundRequestCount()` to get the total number of refund requests targeting the current receiver. - -```typescript -const count = await merchant.getRefundRequestCount(); -console.log(`Total refund requests: ${count}`); - -if (count > 0n) { - const { keys } = await merchant.getPendingRefundRequests(0n, count); - console.log(`Retrieved all ${keys.length} request keys`); + const request = await merchant.refund.getByKey(key); + console.log(`Key: ${key}, Amount: ${request.amount}, Status: ${request.status}`); } ``` -## Refund Request Actions +## Refund request actions -### Approve a Refund Request +### Deny a refund request -Use `approveRefundRequest()` to approve a pending refund request. This changes the request status to `Approved`. +Use `refund.deny()` to deny a pending refund request. This changes the request status to `Denied`. ```typescript -const { txHash } = await merchant.approveRefundRequest(paymentInfo, 0n); -console.log('Refund approved:', txHash); -``` - - -Approving a refund request changes its status but does **not** transfer funds. You must also call `refundInEscrow()` or `refundPostEscrow()` to execute the actual token transfer. - - -### Deny a Refund Request - -Use `denyRefundRequest()` to deny a pending refund request. This changes the request status to `Denied`. - -```typescript -const { txHash } = await merchant.denyRefundRequest(paymentInfo, 0n); +const txHash = await merchant.refund.deny(paymentInfo); console.log('Refund denied:', txHash); ``` @@ -170,60 +107,37 @@ console.log('Refund denied:', txHash); If you deny a request, the payer may escalate to an arbiter for dispute resolution. Consider providing a reason off-chain to reduce escalation risk. -## Freeze Management - -### Check If a Payment Is Frozen - -Use `isFrozen()` to check whether a payment has been frozen by the payer or an arbiter. Frozen payments cannot be released until unfrozen. - -```typescript -const freezeAddress: `0x${string}` = '0xFreezeContract...'; - -const frozen = await merchant.isFrozen(paymentInfo, freezeAddress); +### Refund in escrow (auto-approves) -if (frozen) { - console.log('Payment is frozen - cannot release until unfrozen'); -} else { - console.log('Payment is not frozen'); -} -``` - -### Unfreeze a Payment - -Use `unfreezePayment()` to remove a freeze on a payment. Only the receiver (merchant) or an authorized party can unfreeze. +Instead of a separate approve step, you call `payment.refundInEscrow()` directly. The RefundRequest recorder plugin auto-approves any pending request during execution. ```typescript -const freezeAddress: `0x${string}` = '0xFreezeContract...'; - -const { txHash } = await merchant.unfreezePayment(paymentInfo, freezeAddress); -console.log('Payment unfrozen:', txHash); +const txHash = await merchant.payment.refundInEscrow(paymentInfo, request.amount); +console.log('Refund executed (request auto-approved):', txHash); ``` -## Complete Refund Workflow +## Complete refund workflow Here is a full workflow showing how to detect a refund request, review it, make a decision, and execute the refund if approved. ```typescript -import { createPublicClient, createWalletClient, http } from 'viem'; -import { baseSepolia } from 'viem/chains'; -import { privateKeyToAccount } from 'viem/accounts'; -import { X402rMerchant } from '@x402r/merchant'; -import { getNetworkConfig, RequestStatus } from '@x402r/core'; +import { createMerchantClient } from '@x402r/sdk'; +import { RequestStatus } from '@x402r/core'; +import type { PaymentInfo } from '@x402r/core'; async function handleRefundWorkflow( - merchant: X402rMerchant, - paymentInfo: PaymentInfo, - nonce: bigint + merchant: ReturnType, + paymentInfo: PaymentInfo ) { // Step 1: Check if a refund request exists - const hasRequest = await merchant.hasRefundRequest(paymentInfo, nonce); + const hasRequest = await merchant.refund.has(paymentInfo); if (!hasRequest) { - console.log('No refund request for this payment/nonce'); + console.log('No refund request for this payment'); return; } // Step 2: Get the full request data - const request = await merchant.getRefundRequest(paymentInfo, nonce); + const request = await merchant.refund.get(paymentInfo); console.log(`Refund request: ${request.amount} tokens, status: ${request.status}`); // Step 3: Only process pending requests @@ -232,82 +146,68 @@ async function handleRefundWorkflow( return; } - // Step 4: Check if the payment is frozen - const freezeAddress: `0x${string}` = '0xFreezeContract...'; - const frozen = await merchant.isFrozen(paymentInfo, freezeAddress); - if (frozen) { - console.log('Payment is frozen - resolve dispute before processing refund'); - return; - } - - // Step 5: Check available amounts - const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo); - console.log(`Available to refund: ${refundableAmount}`); - - // Step 6: Make a decision - const shouldApprove = request.amount <= refundableAmount; + // Step 4: Check available amounts + const amounts = await merchant.payment.getAmounts(paymentInfo); + console.log(`Available to refund: ${amounts.refundableAmount}`); - if (shouldApprove) { - // Approve the request - const { txHash: approveTx } = await merchant.approveRefundRequest(paymentInfo, nonce); - console.log('Approved:', approveTx); + // Step 5: Make a decision + const shouldRefund = request.amount <= amounts.refundableAmount; - // Execute the refund from escrow - const { txHash: refundTx } = await merchant.refundInEscrow(paymentInfo, request.amount); - console.log('Refund executed:', refundTx); + if (shouldRefund) { + // Execute the refund (auto-approves the request) + const txHash = await merchant.payment.refundInEscrow(paymentInfo, request.amount); + console.log('Refund executed:', txHash); } else { // Deny the request - const { txHash: denyTx } = await merchant.denyRefundRequest(paymentInfo, nonce); - console.log('Denied:', denyTx); + const txHash = await merchant.refund.deny(paymentInfo); + console.log('Denied:', txHash); } } ``` -## Refund Request Lifecycle +## Refund request lifecycle ```mermaid sequenceDiagram - participant P as Payer (Client SDK) - participant R as RefundRequest Contract - participant M as Merchant (Merchant SDK) + participant P as Payer + participant R as RefundRequest (IRecorder) + participant M as Merchant participant O as PaymentOperator - P->>R: requestRefund(paymentInfo, amount, nonce) + P->>R: refund.request(paymentInfo, amount) R-->>M: RefundRequested event - M->>R: hasRefundRequest(paymentInfo, nonce) + M->>R: refund.has(paymentInfo) R-->>M: true - M->>R: getRefundRequest(paymentInfo, nonce) + M->>R: refund.get(paymentInfo) R-->>M: RefundRequestData M->>M: Review request (policy check) - alt Approve - M->>R: approveRefundRequest(paymentInfo, nonce) - M->>O: refundInEscrow(paymentInfo, amount) + alt Refund + M->>O: payment.refundInEscrow(paymentInfo, amount) + O->>R: record() auto-approves pending request O->>P: Funds returned to payer else Deny - M->>R: denyRefundRequest(paymentInfo, nonce) + M->>R: refund.deny(paymentInfo) Note over P: Payer may escalate to arbiter end ``` -## Method Reference +## Method reference | Method | Parameters | Returns | Description | |--------|-----------|---------|-------------| -| `hasRefundRequest` | `paymentInfo, nonce: bigint` | `Promise` | Check if refund request exists | -| `getRefundStatus` | `paymentInfo, nonce: bigint` | `Promise` | Get request status | -| `getRefundRequest` | `paymentInfo, nonce: bigint` | `Promise` | Get full request data | -| `approveRefundRequest` | `paymentInfo, nonce: bigint` | `Promise<{ txHash }>` | Approve a pending request | -| `denyRefundRequest` | `paymentInfo, nonce: bigint` | `Promise<{ txHash }>` | Deny a pending request | -| `getPendingRefundRequests` | `offset: bigint, count: bigint` | `Promise<{ keys, total }>` | Paginated request keys | -| `getRefundRequestCount` | _(none)_ | `Promise` | Total requests for receiver | -| `getRefundRequestByKey` | `compositeKey: hex` | `Promise` | Look up by composite key | -| `unfreezePayment` | `paymentInfo, freezeAddress: hex` | `Promise<{ txHash }>` | Remove payment freeze | -| `isFrozen` | `paymentInfo, freezeAddress: hex` | `Promise` | Check if payment is frozen | - -## Next Steps +| `refund.has` | `paymentInfo` | `Promise` | Check if refund request exists | +| `refund.getStatus` | `paymentInfo` | `Promise` | Get request status | +| `refund.get` | `paymentInfo` | `Promise` | Get full request data | +| `refund.deny` | `paymentInfo` | `Promise` | Deny a pending request | +| `refund.getReceiverRequests` | `receiver, offset, count` | `Promise<{ keys, total }>` | Paginated request keys | +| `refund.getByKey` | `paymentInfoHash` | `Promise` | Look up by key | +| `payment.refundInEscrow` | `paymentInfo, amount, data?` | `Promise` | Refund and auto-approve | +| `freeze.isFrozen` | `paymentInfo` | `Promise` | Check if payment is frozen | + +## Next steps diff --git a/sdk/merchant/subscriptions.mdx b/sdk/merchant/subscriptions.mdx index 813b31d..0d1dba5 100644 --- a/sdk/merchant/subscriptions.mdx +++ b/sdk/merchant/subscriptions.mdx @@ -1,190 +1,115 @@ --- -title: "Merchant Events" +title: "Merchant events" description: "Subscribe to real-time refund, release, and freeze events with the Merchant SDK" icon: "bell" --- -The `X402rMerchant` class provides three subscription methods for watching blockchain events in real-time. Each returns an object with an `unsubscribe` function for cleanup. +The merchant client provides watch methods for subscribing to blockchain events in real-time. Each returns an unsubscribe function for cleanup. -## Watch Refund Requests +## Watch refund requests -Use `watchRefundRequests()` to subscribe to `RefundRequested` events emitted by the RefundRequest contract. The callback receives a `RefundRequestEventLog` object for each event. +Use `watch.onRefundRequest()` to subscribe to events emitted by the RefundRequest contract. ```typescript -const { unsubscribe } = merchant.watchRefundRequests((event) => { - console.log('Event:', event.eventName); - console.log('Payment hash:', event.args.paymentInfoHash); - console.log('Payer:', event.args.payer); - console.log('Receiver:', event.args.receiver); - console.log('Amount:', event.args.amount); - console.log('Nonce:', event.args.nonce); - console.log('Block:', event.blockNumber); - console.log('Tx hash:', event.transactionHash); +const unsubscribe = merchant.watch.onRefundRequest((event) => { + console.log('Refund request event:', event); }); // Later: stop watching unsubscribe(); ``` -The `RefundRequestEventLog` type has the following shape: - -```typescript -interface RefundRequestEventLog { - eventName: 'RefundRequested' | 'RefundRequestStatusUpdated' | 'RefundRequestCancelled'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - nonce?: bigint; - status?: number; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} -``` - -`watchRefundRequests()` requires the `refundRequestAddress` to be configured when creating the `X402rMerchant` instance. +This is a no-op if no `refundRequestAddress` was resolved. The address is auto-resolved from the chain config. -### Example: Auto-respond to Small Refund Requests +### Example: auto-respond to small refund requests ```typescript -import { X402rMerchant } from '@x402r/merchant'; -import { RequestStatus } from '@x402r/core'; +import { createMerchantClient } from '@x402r/sdk'; const AUTO_APPROVE_THRESHOLD = BigInt('5000000'); // 5 USDC -const { unsubscribe } = merchant.watchRefundRequests(async (event) => { - const amount = event.args.amount; - const paymentHash = event.args.paymentInfoHash; +const unsubscribe = merchant.watch.onRefundRequest(async (event: any) => { + const amount = event.args?.amount; - console.log(`New refund request: ${paymentHash}, amount: ${amount}`); + console.log('New refund request event'); if (amount && amount < AUTO_APPROVE_THRESHOLD) { - console.log('Auto-approving small refund request'); - // You would look up the paymentInfo from your database - // and call merchant.approveRefundRequest(paymentInfo, nonce) + console.log('Auto-refunding small request'); + // Look up paymentInfo from your database, then: + // await merchant.payment.refundInEscrow(paymentInfo, amount) } else { console.log('Queuing for manual review'); } }); ``` -## Watch Releases +## Watch payment events -Use `watchReleases()` to subscribe to `ReleaseExecuted` events emitted by the PaymentOperator contract. The callback receives a `PaymentOperatorEventLog` object for each event. +Use `watch.onPayment()` to subscribe to `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted` events on the PaymentOperator contract. ```typescript -const { unsubscribe } = merchant.watchReleases((event) => { - console.log('Release executed!'); - console.log('Payment hash:', event.args.paymentInfoHash); - console.log('Amount:', event.args.amount); - console.log('Payer:', event.args.payer); - console.log('Receiver:', event.args.receiver); - console.log('Block:', event.blockNumber); - console.log('Tx hash:', event.transactionHash); +const unsubscribe = merchant.watch.onPayment((event) => { + console.log('Payment event:', event); }); // Later: stop watching unsubscribe(); ``` -The `PaymentOperatorEventLog` type has the following shape: - -```typescript -interface PaymentOperatorEventLog { - eventName: 'ReleaseExecuted' | 'RefundInEscrowExecuted' | 'RefundPostEscrowExecuted' - | 'AuthorizationCreated' | 'ChargeExecuted'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} -``` - -### Example: Revenue Tracking +### Example: revenue tracking ```typescript let totalReleased = 0n; -const { unsubscribe } = merchant.watchReleases((event) => { - const amount = event.args.amount ?? 0n; - totalReleased += amount; - - console.log(`Release: +${amount} tokens`); - console.log(`Total released: ${totalReleased}`); +const unsubscribe = merchant.watch.onPayment((event: any) => { + if (event.eventName === 'ReleaseExecuted') { + const amount = event.args?.amount ?? 0n; + totalReleased += amount; + console.log(`Release: +${amount} tokens, Total: ${totalReleased}`); + } }); ``` -## Watch Freeze Events +## Watch refund execution -Use `watchFreezeEvents()` to subscribe to `PaymentFrozen` and `PaymentUnfrozen` events from a specific Freeze contract. You must provide the Freeze contract address as the first argument. +Use `watch.onRefundExecuted()` to subscribe to `RefundInEscrowExecuted` and `RefundPostEscrowExecuted` events. ```typescript -const freezeAddress: `0x${string}` = '0xFreezeContract...'; - -const { unsubscribe } = merchant.watchFreezeEvents( - freezeAddress, - (event) => { - if (event.eventName === 'PaymentFrozen') { - console.log('Payment FROZEN:', event.args.paymentInfoHash); - console.log('Frozen by:', event.args.caller); - // Alert: a dispute may be in progress - } else if (event.eventName === 'PaymentUnfrozen') { - console.log('Payment UNFROZEN:', event.args.paymentInfoHash); - console.log('Unfrozen by:', event.args.caller); - // The payment can now be released - } - } -); +const unsubscribe = merchant.watch.onRefundExecuted((event) => { + console.log('Refund executed:', event); +}); -// Later: stop watching unsubscribe(); ``` -The `FreezeEventLog` type has the following shape: +## Watch fee distribution + +Use `watch.onFeeDistribution()` to subscribe to `FeesDistributed` events. ```typescript -interface FreezeEventLog { - eventName: 'PaymentFrozen' | 'PaymentUnfrozen'; - args: { - paymentInfoHash?: `0x${string}`; - caller?: `0x${string}`; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} -``` +const unsubscribe = merchant.watch.onFeeDistribution((event) => { + console.log('Fees distributed:', event); +}); - -The `freezeAddress` parameter is the address of the Freeze condition contract, not the PaymentOperator. You can retrieve it from your operator config via `merchant.getOperatorConfig()`. - +unsubscribe(); +``` -## Event Types Reference +## Event types reference -| Method | Event Name | Callback Type | Use Case | -|--------|-----------|---------------|----------| -| `watchRefundRequests` | `RefundRequested` | `RefundRequestEventLog` | Detect incoming refund requests | -| `watchReleases` | `ReleaseExecuted` | `PaymentOperatorEventLog` | Track revenue and release confirmations | -| `watchFreezeEvents` | `PaymentFrozen` / `PaymentUnfrozen` | `FreezeEventLog` | Monitor dispute-related freezes | +| Method | Events Watched | Contract | Use Case | +|--------|---------------|----------|----------| +| `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | Track payment lifecycle | +| `watch.onRefundRequest` | All RefundRequest events | RefundRequest | Detect incoming refund requests | +| `watch.onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | Track refund execution | +| `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | Monitor fee distribution | -All subscription methods use viem's `watchContractEvent` under the hood. For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). +For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). -## Next Steps +## Next steps @@ -194,7 +119,7 @@ All subscription methods use viem's `watchContractEvent` under the hood. For rel Learn about dispute resolution from the arbiter perspective. - Process refund requests with approve/deny workflows. + Process refund requests with deny workflows. See how clients subscribe to the same events. diff --git a/sdk/overview.mdx b/sdk/overview.mdx index c0e9ceb..ffe2c55 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -5,7 +5,7 @@ icon: "cube" --- -The X402r SDK is in active development (v0.0.2). APIs may change between releases. Always test on Base Sepolia before using real funds on mainnet. +The X402r SDK is in active development. APIs may change between releases. Always test on Base Sepolia before using real funds on mainnet. The X402r SDK provides a complete TypeScript implementation for integrating with the X402r refundable payments protocol. It enables clients, merchants, and arbiters to interact with smart contracts for payment authorization, escrow management, and dispute resolution. @@ -32,18 +32,22 @@ The SDK is organized into packages designed for specific roles in the payment ec -## Network Support +## Network support + +All v3 contracts are deployed to the same address on every chain via CREATE3. | Network | Chain ID | Status | |---------|----------|--------| | Base Sepolia | 84532 | Tested | -| Base Mainnet | 8453 | Deployed, not yet tested | -| Ethereum | 1 | Deployed, not yet tested | -| Ethereum Sepolia | 11155111 | Deployed, not yet tested | -| Arbitrum Sepolia | 421614 | Deployed, not yet tested | -| Polygon | 137 | Deployed, not yet tested | -| Arbitrum | 42161 | Deployed, not yet tested | -| Optimism | 10 | Deployed, not yet tested | -| Avalanche | 43114 | Deployed, not yet tested | -| Celo | 42220 | Deployed, not yet tested | -| Monad | 143 | Deployed, not yet tested | +| Base | 8453 | Deployed | +| Ethereum | 1 | Deployed | +| Ethereum Sepolia | 11155111 | Deployed | +| Arbitrum | 42161 | Deployed | +| Arbitrum Sepolia | 421614 | Deployed | +| Optimism | 10 | Deployed | +| Polygon | 137 | Deployed | +| Celo | 42220 | Deployed | +| Avalanche | 43114 | Deployed | +| Linea | 59144 | Deployed | +| Monad | 143 | Deployed | +| SKALE Base | 1187947933 | Deployed (Shanghai EVM) | From 5226b01c173ca7b302614a90760348a54ca07fec Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 05:13:15 +0000 Subject: [PATCH 03/37] Replace refundable() with forwardToArbiter() in SDK docs Generated-By: mintlify-agent --- docs.json | 2 +- sdk/deploy-operator.mdx | 4 +- sdk/examples.mdx | 12 +- sdk/helpers/forward-to-arbiter.mdx | 161 +++++++++++++++++++++++++++ sdk/helpers/refundable.mdx | 167 ---------------------------- sdk/limitations.mdx | 2 +- sdk/merchant/getting-started.mdx | 40 ++++--- sdk/merchant/payment-operations.mdx | 4 +- sdk/merchant/quickstart.mdx | 6 +- sdk/overview.mdx | 4 +- 10 files changed, 201 insertions(+), 201 deletions(-) create mode 100644 sdk/helpers/forward-to-arbiter.mdx delete mode 100644 sdk/helpers/refundable.mdx diff --git a/docs.json b/docs.json index 3ae8129..d9c2da8 100644 --- a/docs.json +++ b/docs.json @@ -101,7 +101,7 @@ "group": "Merchant", "pages": [ "sdk/merchant/getting-started", - "sdk/helpers/refundable", + "sdk/helpers/forward-to-arbiter", "sdk/merchant/quickstart", "sdk/merchant/refund-handling" ] diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index a97990b..e0dae0b 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -225,8 +225,8 @@ Deployment requires gas fees. Ensure your wallet has ETH on the target network. See working merchant and client examples. - - Mark payment options as refundable with your operator. + + Forward escrow settlements to an arbiter service. Understand the underlying contract architecture. diff --git a/sdk/examples.mdx b/sdk/examples.mdx index 44df86f..bedb02c 100644 --- a/sdk/examples.mdx +++ b/sdk/examples.mdx @@ -16,10 +16,10 @@ The SDK includes working examples in the `x402r-sdk/` repository. Each is a stan Deploy a complete marketplace operator with escrow, freeze, and arbiter support. - Express merchant server using `EscrowServerScheme`, `HTTPFacilitatorClient`, and `refundable()` to accept escrow payments via x402 middleware. + Express merchant server using `EscrowServerScheme` and `HTTPFacilitatorClient` to accept escrow payments via x402 middleware. - Hono merchant server using `EscrowServerScheme`, `HTTPFacilitatorClient`, and `refundable()` to accept escrow payments via x402 middleware. + Hono merchant server using `EscrowServerScheme` and `HTTPFacilitatorClient` to accept escrow payments via x402 middleware. CLI tool for merchants to release payments, approve/deny refunds, and query escrow state. @@ -92,9 +92,9 @@ See [Deploy an Operator](/sdk/deploy-operator) for the full guide. ## Server Examples -Demonstrates minimal merchant servers (Express and Hono variants) that use `EscrowServerScheme`, `HTTPFacilitatorClient`, and `refundable()` via x402's standard middleware: +Demonstrates minimal merchant servers (Express and Hono variants) that use `EscrowServerScheme` and `HTTPFacilitatorClient` via x402's standard middleware: -1. Returns 402 with `refundable()` payment options +1. Returns 402 with inline escrow payment options 2. Delegates payment verification to the facilitator via `HTTPFacilitatorClient` 3. Delegates on-chain settlement to the facilitator after the handler runs 4. Returns weather data after successful payment @@ -105,8 +105,8 @@ Demonstrates minimal merchant servers (Express and Hono variants) that use `Escr Deploy a PaymentOperator with escrow and freeze support. - - Mark payment options as refundable with escrow configuration. + + Forward escrow settlements to an arbiter service. Understand the payment lifecycle and key concepts. diff --git a/sdk/helpers/forward-to-arbiter.mdx b/sdk/helpers/forward-to-arbiter.mdx new file mode 100644 index 0000000..7984d2d --- /dev/null +++ b/sdk/helpers/forward-to-arbiter.mdx @@ -0,0 +1,161 @@ +--- +title: "forwardToArbiter()" +description: "Forward escrow settlement responses to an arbiter service for dispute evaluation" +icon: "wrench" +--- + +The `@x402r/helpers` package provides `forwardToArbiter()` — an `onAfterSettle` hook that forwards the response body to an arbiter service after successful escrow settlements. It is fire-and-forget and does not block the client response. + + +This function lives in `@x402r/helpers`. Install it separately: +```bash +npm install @x402r/helpers +``` + +Peer dependency: `@x402/core >=2.5.0` + + +## Usage + +```typescript +import { forwardToArbiter } from '@x402r/helpers'; + +const resourceServer = new x402ResourceServer(facilitatorClient) + .register(networkId, new EscrowServerScheme()) + .onAfterSettle( + forwardToArbiter('http://arbiter:3001') + ); +``` + +After a successful escrow settlement, the hook POSTs `{ responseBody, network, transaction, scheme }` to `{arbiterUrl}/verify`. Non-escrow schemes, failed settlements, and missing response bodies are silently skipped. + +## Function signature + +```typescript +function forwardToArbiter( + arbiterUrl: string, + options?: ForwardToArbiterOptions +): (context: SettleResultContext) => Promise +``` + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `arbiterUrl` | `string` | Base URL of your arbiter service (e.g., `http://arbiter:3001`) | +| `options` | `ForwardToArbiterOptions` | Optional configuration (see below) | + +### Options + +```typescript +interface ForwardToArbiterOptions { + /** Custom error handler. Defaults to `console.warn`. */ + onError?: (error: unknown) => void; +} +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `onError` | `console.warn` | Called when the POST to the arbiter fails. Use this to integrate with your error tracking (e.g., Sentry). | + +### Return value + +Returns an async callback compatible with `onAfterSettle`. The callback: + +1. Skips if the settlement failed (`context.result.success === false`) +2. Skips if the scheme is not `escrow` +3. Skips if there is no response body in the transport context +4. POSTs to `{arbiterUrl}/verify` with the settlement data +5. Catches errors via the `onError` handler (default: `console.warn`) + +## Examples + +### Basic usage + +```typescript +import { forwardToArbiter } from '@x402r/helpers'; + +const resourceServer = new x402ResourceServer(facilitatorClient) + .register(networkId, new EscrowServerScheme()) + .onAfterSettle( + forwardToArbiter('http://arbiter:3001') + ); +``` + +### Custom error handling + +```typescript +import { forwardToArbiter } from '@x402r/helpers'; + +const resourceServer = new x402ResourceServer(facilitatorClient) + .register(networkId, new EscrowServerScheme()) + .onAfterSettle( + forwardToArbiter('http://arbiter:3001', { + onError: (err) => sentry.captureException(err), + }) + ); +``` + + +You should always provide an `onError` handler in production. The x402 `onAfterSettle` runner does not catch errors internally — without a handler, an unreachable arbiter could break your post-settlement flow. + + +### POST payload + +The hook sends a JSON POST to `{arbiterUrl}/verify` with this shape: + +```json +{ + "responseBody": "", + "network": "eip155:84532", + "transaction": "0xabc...", + "scheme": "escrow" +} +``` + +## Behavior details + +- **Escrow-only** — Only fires for `scheme: "escrow"` settlements. Non-escrow schemes (e.g., `exact`) are silently skipped. +- **Fire-and-forget** — The POST happens asynchronously and does not block the HTTP response to the client. +- **Trailing slash safe** — `forwardToArbiter('http://arbiter:3001/')` resolves to `http://arbiter:3001/verify`. + +## Migration from refundable() + +The `refundable()` helper has been removed. If you were using it to wrap payment options with escrow configuration, you should now inline the escrow `extra` config directly: + +```typescript +// Before (removed) +import { refundable } from '@x402r/helpers'; +const option = refundable( + { scheme: 'escrow', price: '$0.01', network: 'eip155:84532', payTo: address }, + operatorAddress +); + +// After — inline the extra config +const option = { + scheme: 'escrow', + network: 'eip155:84532', + price: '$0.01', + payTo: address, + extra: { + escrowAddress: authCaptureEscrow, // from getChainConfig() + operatorAddress, + feeReceiver: operatorAddress, + maxFeeBps: 500, + }, +}; +``` + +## Next steps + + + + Build an arbiter service that receives forwarded settlements. + + + Deploy a PaymentOperator for escrow payments. + + + See merchant and arbiter examples. + + diff --git a/sdk/helpers/refundable.mdx b/sdk/helpers/refundable.mdx deleted file mode 100644 index 1d5fbfe..0000000 --- a/sdk/helpers/refundable.mdx +++ /dev/null @@ -1,167 +0,0 @@ ---- -title: "refundable()" -description: "Configure HTTP 402 responses with escrow-backed refundable payment options" -icon: "wrench" ---- - -The `@x402r/helpers` package provides the `refundable()` function — a framework-agnostic helper that adds escrow configuration to x402 payment options. - - -This function lives in `@x402r/helpers`, not `@x402r/merchant`. Install it separately: -```bash -npm install @x402r/helpers @x402r/core -``` - - -## Usage - -```typescript -import { refundable } from '@x402r/helpers'; - -const option = refundable( - { - scheme: 'escrow', - network: 'eip155:84532', - payTo: '0xMerchantAddress...', - price: '$0.01', - }, - '0xOperatorAddress...' -); -``` - -This returns the original payment option with an `extra` field populated with escrow addresses and fee bounds. - -## Function Signature - -```typescript -function refundable( - option: T, - operatorAddress: `0x${string}`, - options?: RefundableOptions -): T & { extra: EscrowExtra } -``` - -### Parameters - -| Parameter | Type | Description | -|-----------|------|-------------| -| `option` | `PaymentOption` | Base payment option (must include `network`) | -| `operatorAddress` | `Address` | Your PaymentOperator contract address | -| `options` | `RefundableOptions` | Optional overrides (see below) | - -### Options & Defaults - -| Option | Default | Description | -|--------|---------|-------------| -| `escrowAddress` | From network config | AuthCaptureEscrow contract address | -| `tokenCollector` | From network config | ERC3009PaymentCollector contract address | -| `minFeeBps` | `0` | Minimum acceptable fee (0% = accept zero fees) | -| `maxFeeBps` | `1000` | Maximum acceptable fee (1000 bps = 10%) | - -### Return Value - -The function returns the original option object with an `extra` field added: - -```typescript -interface EscrowExtra { - escrowAddress: `0x${string}`; - operatorAddress: `0x${string}`; - tokenCollector: `0x${string}`; - minFeeBps: number; - maxFeeBps: number; -} -``` - -## Examples - -### Basic usage (defaults) - -```typescript -const option = refundable({ - scheme: 'escrow', - network: 'eip155:84532', - payTo: '0xMerchant...', - price: '$0.01', -}, '0xOperator...'); - -// Result includes: -// option.extra.escrowAddress → from network config -// option.extra.operatorAddress → '0xOperator...' -// option.extra.tokenCollector → from network config -// option.extra.minFeeBps → 0 -// option.extra.maxFeeBps → 1000 -``` - -### Custom fee bounds - -```typescript -const option = refundable({ - scheme: 'escrow', - network: 'eip155:84532', - payTo: '0xMerchant...', - price: '$10.00', -}, '0xOperator...', { - maxFeeBps: 500, // Accept up to 5% fee -}); -``` - -### Custom escrow address - -```typescript -const option = refundable({ - scheme: 'escrow', - network: 'eip155:84532', - payTo: '0xMerchant...', - price: '$100.00', -}, '0xOperator...', { - escrowAddress: '0xCustomEscrow...', - tokenCollector: '0xCustomCollector...', -}); -``` - -## Supported Networks - -The function resolves addresses from the network config for all supported networks. See `getNetworkConfig()` for the full list (Base Sepolia, Base, Ethereum, Ethereum Sepolia, Arbitrum Sepolia, Polygon, Arbitrum, Optimism, Avalanche, Celo, Monad). - -```typescript -refundable({ network: 'eip155:84532', ... }, '0x...'); // Base Sepolia -refundable({ network: 'eip155:8453', ... }, '0x...'); // Base Mainnet -refundable({ network: 'eip155:1', ... }, '0x...'); // Ethereum -``` - -## Integration with x402 - -Use `refundable()` when constructing your 402 Payment Required response: - -```typescript -import { refundable } from '@x402r/helpers'; - -// In your server handler: -app.get('/api/resource', (req, res) => { - res.status(402).json({ - x402Version: 2, - accepts: [ - refundable({ - scheme: 'escrow', - network: 'eip155:84532', - payTo: merchantAddress, - price: '$1.00', - }, operatorAddress), - ], - }); -}); -``` - -## Next Steps - - - - Deploy a PaymentOperator to use with refundable(). - - - See merchant server examples using refundable(). - - - Understand the escrow scheme. - - diff --git a/sdk/limitations.mdx b/sdk/limitations.mdx index 7e3a23c..0d180c9 100644 --- a/sdk/limitations.mdx +++ b/sdk/limitations.mdx @@ -44,7 +44,7 @@ const details = await client.getPaymentDetails(hash, recentBlockNumber); ### No Express/Hono Middleware -The `refundable()` helper in `@x402r/helpers` is framework-agnostic. There is no dedicated Express or Hono middleware — use `refundable()` directly when constructing payment options. +The `forwardToArbiter()` hook in `@x402r/helpers` is framework-agnostic. There is no dedicated Express or Hono middleware — configure escrow payment options inline and use `forwardToArbiter()` as an `onAfterSettle` hook. ## Getting Updates diff --git a/sdk/merchant/getting-started.mdx b/sdk/merchant/getting-started.mdx index 9c75abb..67b7676 100644 --- a/sdk/merchant/getting-started.mdx +++ b/sdk/merchant/getting-started.mdx @@ -24,7 +24,7 @@ The full source code for this example is available on [GitHub](https://github.co ```bash mkdir merchant-server && cd merchant-server npm init -y - npm install express @x402/core @x402/express @x402r/evm @x402r/helpers dotenv + npm install express @x402/core @x402/express @x402r/evm dotenv ``` @@ -35,6 +35,7 @@ The full source code for this example is available on [GitHub](https://github.co ADDRESS=0xYourMerchantAddress OPERATOR_ADDRESS=0xYourPaymentOperatorAddress FACILITATOR_URL=http://localhost:4022 + ``` @@ -46,7 +47,7 @@ The full source code for this example is available on [GitHub](https://github.co import express from "express"; import { paymentMiddleware, x402ResourceServer } from "@x402/express"; import { EscrowServerScheme } from "@x402r/evm/escrow/server"; - import { refundable } from "@x402r/helpers"; + import { getChainConfig } from "@x402r/core"; import { HTTPFacilitatorClient } from "@x402/core/server"; const address = process.env.ADDRESS as `0x${string}`; @@ -63,6 +64,9 @@ The full source code for this example is available on [GitHub](https://github.co } const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); + const networkId = "eip155:84532"; + const { authCaptureEscrow } = getChainConfig(networkId); + const app = express(); app.use( @@ -70,22 +74,25 @@ The full source code for this example is available on [GitHub](https://github.co { "GET /weather": { accepts: [ - refundable( - { - scheme: "escrow", - price: "$0.01", - network: "eip155:84532", - payTo: address, + { + scheme: "escrow", + price: "$0.01", + network: networkId, + payTo: address, + extra: { + escrowAddress: authCaptureEscrow, + operatorAddress, + feeReceiver: operatorAddress, + maxFeeBps: 500, }, - operatorAddress, - ), + }, ], description: "Weather data", mimeType: "application/json", }, }, new x402ResourceServer(facilitatorClient).register( - "eip155:84532", + networkId, new EscrowServerScheme() as never, ), ), @@ -132,9 +139,8 @@ The full source code for this example is available on [GitHub](https://github.co "extra": { "escrowAddress": "0x...", "operatorAddress": "0x...", - "tokenCollector": "0x...", - "minFeeBps": 0, - "maxFeeBps": 1000 + "feeReceiver": "0x...", + "maxFeeBps": 500 } }] } @@ -144,7 +150,7 @@ The full source code for this example is available on [GitHub](https://github.co ## How it works -- **`refundable()`** wraps a standard x402 payment option with escrow configuration (contract addresses, fee bounds) from the network config. +- **Escrow `extra` config** specifies the escrow contract address, operator address, fee receiver, and maximum fee bounds directly on the payment option. - **`EscrowServerScheme`** registers the escrow payment scheme with the x402 resource server so it can validate escrow-backed payments. - **`paymentMiddleware`** intercepts requests, checks for a valid payment header, and returns 402 if no payment is provided. - **`HTTPFacilitatorClient`** connects to the facilitator service that verifies and settles payments on-chain. @@ -152,8 +158,8 @@ The full source code for this example is available on [GitHub](https://github.co ## Next Steps - - Configure escrow options and fee bounds. + + Forward escrow settlements to an arbiter service. Release payments, handle refunds, and manage escrow. diff --git a/sdk/merchant/payment-operations.mdx b/sdk/merchant/payment-operations.mdx index e5bdd20..9f28e64 100644 --- a/sdk/merchant/payment-operations.mdx +++ b/sdk/merchant/payment-operations.mdx @@ -262,7 +262,7 @@ flowchart TD Understand the underlying PaymentOperator contract methods. - - Mark payment options as refundable with your operator. + + Forward escrow settlements to an arbiter service. diff --git a/sdk/merchant/quickstart.mdx b/sdk/merchant/quickstart.mdx index 4929d3f..d8d19b9 100644 --- a/sdk/merchant/quickstart.mdx +++ b/sdk/merchant/quickstart.mdx @@ -13,7 +13,7 @@ The `@x402r/merchant` package provides everything merchants need for the post-pa ## Installation ```bash -npm install @x402r/merchant @x402r/helpers @x402r/core viem +npm install @x402r/merchant @x402r/core viem ``` ## Setup @@ -232,8 +232,8 @@ flowchart TD Process incoming refund requests with approve/deny workflows. - - Mark payment options as refundable with your operator. + + Forward escrow settlements to an arbiter service. Understand the underlying PaymentOperator contract methods. diff --git a/sdk/overview.mdx b/sdk/overview.mdx index c0e9ceb..1637dd9 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -27,8 +27,8 @@ The SDK is organized into packages designed for specific roles in the payment ec SDK for arbiters to resolve disputes and manage refund decisions. - - Framework-agnostic helper to mark x402 payment options as refundable with escrow configuration. + + `onAfterSettle` hook to forward escrow settlement responses to an arbiter service. From 70f865fd8eefbb67ed9cac2da325ecc6afcf5585 Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 06:44:26 +0000 Subject: [PATCH 04/37] docs(sdk): add deployDeliveryProtectionOperator preset, fix marketplace operator signature Generated-By: mintlify-agent --- sdk/deploy-operator.mdx | 186 ++++++++++++++++++++++++++++++++-------- sdk/examples.mdx | 21 ++++- 2 files changed, 171 insertions(+), 36 deletions(-) diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index a97990b..5cbc2ce 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -1,16 +1,27 @@ --- -title: "Deploy an Operator" +title: "Deploy an operator" description: "Deploy a PaymentOperator with escrow, freeze, and dispute resolution in one call" icon: "rocket" --- -The `@x402r/core` package includes deployment utilities that handle the full lifecycle of deploying a PaymentOperator and all its supporting contracts. +The `@x402r/core` package includes deployment presets that handle the full lifecycle of deploying a PaymentOperator and all its supporting contracts. Clone the deploy-operator example and deploy your own operator in minutes. -## Overview +## Presets + +The SDK ships two deployment presets. Pick the one that matches your use case: + +| Preset | Use case | Freeze | Fees | RefundRequest | +|--------|----------|--------|------|---------------| +| `deployMarketplaceOperator` | General marketplace with dispute resolution | Yes (optional) | Yes (optional) | Yes | +| `deployDeliveryProtectionOperator` | Garbage detection / delivery verification | No | No | No | + +All contracts are deployed via factories using CREATE2, so identical configurations produce identical addresses across deployments. + +## Marketplace operator A complete marketplace operator deployment includes: @@ -21,9 +32,7 @@ A complete marketplace operator deployment includes: 5. **StaticFeeCalculator** — Optional operator fee (basis points) 6. **PaymentOperator** — The main contract tying everything together -All contracts are deployed via factories using CREATE2, so identical configurations produce identical addresses across deployments. - -## Deploy Your Operator +## Deploy your operator **Prerequisites:** - Node.js 20+, pnpm 9.15+ @@ -82,7 +91,7 @@ PRIVATE_KEY=0x... pnpm tsx examples/deploy-operator/deploy-short-escrow.ts ``` -## Using the SDK Directly +## Using the SDK directly If you want to integrate deployment into your own code: @@ -107,49 +116,54 @@ const walletClient = createWalletClient({ const result = await deployMarketplaceOperator( walletClient, publicClient, - 'eip155:84532', // Base Sepolia { - feeRecipient: account.address, // receives operator fees - arbiter: '0xArbiterAddress...', // dispute resolver - escrowPeriodSeconds: 604800n, // 7 days - freezeDurationSeconds: 259200n, // 3 days max freeze - operatorFeeBps: 100n, // 1% fee (optional) + chainId: 84532, // Base Sepolia + feeRecipient: account.address, // receives operator fees + arbiter: '0xArbiterAddress...', // dispute resolver + escrowPeriodSeconds: 604800n, // 7 days + freezeDurationSeconds: 259200n, // 3 days max freeze + operatorFeeBps: 100n, // 1% fee (optional) } ); console.log('Operator:', result.operatorAddress); console.log('EscrowPeriod:', result.escrowPeriodAddress); console.log('Freeze:', result.freezeAddress); -console.log('New deployments:', result.summary.newDeployments); -console.log('Existing (reused):', result.summary.existingContracts); +console.log('New deployments:', result.summary.newCount); +console.log('Existing (reused):', result.summary.existingCount); ``` -## Configuration Options +## Configuration options | Option | Type | Description | |--------|------|-------------| +| `chainId` | `number` | Target chain ID (e.g., `84532` for Base Sepolia) | | `feeRecipient` | `Address` | Address that receives operator fees | | `arbiter` | `Address` | Arbiter address for dispute resolution | | `escrowPeriodSeconds` | `bigint` | Escrow waiting period (e.g., `604800n` for 7 days) | | `freezeDurationSeconds` | `bigint` | How long freezes last. Default: `0n` (permanent until unfrozen) | | `operatorFeeBps` | `bigint` | Fee in basis points. Default: `0n` (no fee). `100n` = 1% | +| `authorizedCodehash` | `Hex` | Optional codehash restriction. Default: `bytes32(0)` (no restriction) | -## Deployment Result +## Deployment result The `deployMarketplaceOperator` function returns: ```typescript interface MarketplaceOperatorDeployment { - operatorAddress: Address; // The PaymentOperator - escrowPeriodAddress: Address; // EscrowPeriod recorder/condition - freezeAddress: Address; // Freeze condition - arbiterConditionAddress: Address; // StaticAddressCondition for arbiter - refundInEscrowCondition: Address; // OR(Receiver, Arbiter) - feeCalculatorAddress: Address | null; // null if no fee - txHashes: Hash[]; // All deployment tx hashes + operatorAddress: Address; // The PaymentOperator + escrowPeriodAddress: Address; // EscrowPeriod recorder/condition + freezeAddress: Address | null; // Freeze condition (null if disabled) + refundRequestAddress: Address; // RefundRequest contract + refundRequestEvidenceAddress: Address; // RefundRequestEvidence contract + refundInEscrowConditionAddress: Address; // OR(Receiver, Arbiter) + feeCalculatorAddress: Address | null; // null if no fee + operatorConfig: OperatorConfig; // Full operator slot configuration + deployments: DeployResult[]; // Per-contract deploy details summary: { - newDeployments: number; // Newly deployed contracts - existingContracts: number; // Reused existing contracts + newCount: number; // Newly deployed contracts + existingCount: number; // Reused existing contracts + txHashes: `0x${string}`[]; // All deployment tx hashes }; } ``` @@ -158,7 +172,7 @@ interface MarketplaceOperatorDeployment { Because all contracts use CREATE2, redeploying with the same parameters is idempotent — it will detect existing contracts and skip them. The `summary` tells you what was new vs reused. -## Preview Addresses (No Deploy) +## Preview addresses (no deploy) You can preview what addresses will be created without actually deploying: @@ -167,8 +181,8 @@ import { previewMarketplaceOperator } from '@x402r/core/deploy'; const preview = await previewMarketplaceOperator( publicClient, - 'eip155:84532', { + chainId: 84532, feeRecipient: '0xYourAddress...', arbiter: '0xArbiterAddress...', escrowPeriodSeconds: 604800n, @@ -179,22 +193,126 @@ console.log('Operator will be at:', preview.operatorAddress); console.log('EscrowPeriod will be at:', preview.escrowPeriodAddress); ``` -## Operator Slot Configuration +## Marketplace operator slot configuration -The deployed operator has the following slot configuration: +The deployed marketplace operator has the following slot configuration: | Slot | Contract | Purpose | |------|----------|---------| | `AUTHORIZE_CONDITION` | UsdcTvlLimit | Safety limit on authorization | | `AUTHORIZE_RECORDER` | EscrowPeriod | Records authorization timestamp | | `CHARGE_CONDITION` | (none) | No restrictions on charge | -| `RELEASE_CONDITION` | EscrowPeriod | Blocks release during escrow period | +| `RELEASE_CONDITION` | EscrowPeriod (or AND(EscrowPeriod, Freeze) if freeze enabled) | Blocks release during escrow period | | `REFUND_IN_ESCROW_CONDITION` | OR(Receiver, Arbiter) | Receiver or arbiter can approve | +| `REFUND_IN_ESCROW_RECORDER` | RefundRequest | Tracks refund request state | | `REFUND_POST_ESCROW_CONDITION` | Receiver | Only receiver after escrow | -| `FEE_CALCULATOR` | StaticFeeCalculator | Fixed percentage fee | +| `FEE_CALCULATOR` | StaticFeeCalculator | Fixed percentage fee (if configured) | | `FEE_RECIPIENT` | Your address | Receives fees | -## Network Support +--- + +## Delivery protection operator + +The delivery protection preset is a simpler operator designed for garbage detection and content verification use cases. An arbiter verifies that content was delivered correctly and releases funds. If the arbiter does nothing, the escrow window expires and anyone can trigger a refund. + +This preset has **no freeze, no fees, and no RefundRequest** contracts — making it cheaper to deploy. + +### Condition layout + +| Slot | Contract | Purpose | +|------|----------|---------| +| `RELEASE_CONDITION` | StaticAddressCondition(arbiter) | Only the arbiter can release | +| `AUTHORIZE_RECORDER` | EscrowPeriod | Records authorization timestamp | +| `REFUND_IN_ESCROW_CONDITION` | EscrowPeriod | Anyone can refund after escrow window expires | +| `REFUND_POST_ESCROW_CONDITION` | Receiver | Receiver can refund post-escrow | + +### Deploy a delivery protection operator + +```typescript +import { createPublicClient, createWalletClient, http } from 'viem'; +import { baseSepolia } from 'viem/chains'; +import { privateKeyToAccount } from 'viem/accounts'; +import { deployDeliveryProtectionOperator } from '@x402r/core/deploy'; + +const publicClient = createPublicClient({ + chain: baseSepolia, + transport: http(), +}); + +const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); +const walletClient = createWalletClient({ + account, + chain: baseSepolia, + transport: http(), +}); + +const result = await deployDeliveryProtectionOperator( + walletClient, + publicClient, + { + chainId: 84532, // Base Sepolia + arbiter: '0xArbiterAddress...', // Delivery verifier + feeRecipient: account.address, // Required (non-zero), future-proofs for fees + escrowPeriodSeconds: 604800n, // 7 days + } +); + +console.log('Operator:', result.operatorAddress); +console.log('EscrowPeriod:', result.escrowPeriodAddress); +console.log('Arbiter condition:', result.arbiterConditionAddress); +``` + + +The `feeRecipient` is required by the factory even though no fee calculator is set. This future-proofs the operator so you can add a fee calculator later without redeploying. + + +### Configuration options + +| Option | Type | Description | +|--------|------|-------------| +| `chainId` | `number` | Target chain ID (e.g., `84532` for Base Sepolia) | +| `arbiter` | `Address` | Arbiter address that verifies delivery | +| `feeRecipient` | `Address` | Fee recipient (required, non-zero) | +| `escrowPeriodSeconds` | `bigint` | Escrow waiting period (e.g., `604800n` for 7 days) | +| `authorizedCodehash` | `Hex` | Optional codehash restriction. Default: `bytes32(0)` (no restriction) | + +### Deployment result + +```typescript +interface DeliveryProtectionOperatorDeployment { + operatorAddress: Address; // The PaymentOperator + escrowPeriodAddress: Address; // EscrowPeriod recorder/condition + arbiterConditionAddress: Address; // StaticAddressCondition for arbiter + operatorConfig: OperatorConfig; // Full operator slot configuration + deployments: DeployResult[]; // Per-contract deploy details + summary: { + newCount: number; // Newly deployed contracts + existingCount: number; // Reused existing contracts + txHashes: `0x${string}`[]; // All deployment tx hashes + }; +} +``` + +### Preview addresses (no deploy) + +```typescript +import { previewDeliveryProtectionOperator } from '@x402r/core/deploy'; + +const preview = await previewDeliveryProtectionOperator( + publicClient, + { + chainId: 84532, + arbiter: '0xArbiterAddress...', + feeRecipient: '0xYourAddress...', + escrowPeriodSeconds: 604800n, + } +); + +console.log('Operator will be at:', preview.operatorAddress); +console.log('Arbiter condition:', preview.arbiterConditionAddress); +``` + +## Network support Deployment is supported on all configured networks: @@ -216,7 +334,7 @@ Deployment is supported on all configured networks: Deployment requires gas fees. Ensure your wallet has ETH on the target network. On Base Sepolia, you can get testnet ETH from [Base network faucets](https://docs.base.org/base-chain/tools/network-faucets). -## Next Steps +## Next steps diff --git a/sdk/examples.mdx b/sdk/examples.mdx index 44df86f..2d75a04 100644 --- a/sdk/examples.mdx +++ b/sdk/examples.mdx @@ -78,8 +78,8 @@ import { deployMarketplaceOperator } from '@x402r/core/deploy'; const result = await deployMarketplaceOperator( walletClient, publicClient, - 'eip155:84532', { + chainId: 84532, // Base Sepolia feeRecipient: account.address, arbiter: arbiterAddress, escrowPeriodSeconds: 604800n, // 7 days @@ -88,7 +88,24 @@ const result = await deployMarketplaceOperator( ); ``` -See [Deploy an Operator](/sdk/deploy-operator) for the full guide. +You can also deploy a simpler delivery protection operator for content verification use cases: + +```typescript +import { deployDeliveryProtectionOperator } from '@x402r/core/deploy'; + +const result = await deployDeliveryProtectionOperator( + walletClient, + publicClient, + { + chainId: 84532, + arbiter: arbiterAddress, + feeRecipient: account.address, + escrowPeriodSeconds: 604800n, + } +); +``` + +See [Deploy an operator](/sdk/deploy-operator) for the full guide. ## Server Examples From 831d9221d00fa4f8c8a12194d423a5f8ea38aadc Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 07:34:00 +0000 Subject: [PATCH 05/37] docs(sdk): add forwardToArbiter page and document named address constants - New page for forwardToArbiter() with payload shape, error handling, and address re-exports - Updated installation page to show named constant imports alongside getChainConfig - Updated helpers card description in SDK overview - Added forwardToArbiter link to refundable() next steps Generated-By: mintlify-agent --- docs.json | 1 + sdk/helpers/forward-to-arbiter.mdx | 140 +++++++++++++++++++++++++++++ sdk/helpers/refundable.mdx | 3 + sdk/installation.mdx | 43 +++++++-- sdk/overview.mdx | 2 +- 5 files changed, 179 insertions(+), 10 deletions(-) create mode 100644 sdk/helpers/forward-to-arbiter.mdx diff --git a/docs.json b/docs.json index 3ae8129..a3858d0 100644 --- a/docs.json +++ b/docs.json @@ -102,6 +102,7 @@ "pages": [ "sdk/merchant/getting-started", "sdk/helpers/refundable", + "sdk/helpers/forward-to-arbiter", "sdk/merchant/quickstart", "sdk/merchant/refund-handling" ] diff --git a/sdk/helpers/forward-to-arbiter.mdx b/sdk/helpers/forward-to-arbiter.mdx new file mode 100644 index 0000000..deb2dcc --- /dev/null +++ b/sdk/helpers/forward-to-arbiter.mdx @@ -0,0 +1,140 @@ +--- +title: "forwardToArbiter()" +description: "Forward settlement data to an arbiter service for quality evaluation" +icon: "arrow-right" +--- + +The `forwardToArbiter()` function creates an `onAfterSettle` hook that forwards the response body and payment payload to an arbiter service. It runs fire-and-forget so it never blocks the response to the client. + +- Only fires for successful **commerce** scheme settlements +- POSTs `{ responseBody, transaction, paymentPayload }` to `{arbiterUrl}/verify` +- Errors are silently caught so an unavailable arbiter cannot break the payment flow + +## Usage + +```typescript +import { forwardToArbiter } from '@x402r/helpers'; + +const resourceServer = new x402ResourceServer(facilitatorClient) + .register(networkId, new EscrowServerScheme()) + .onAfterSettle( + forwardToArbiter('http://arbiter:3001') + ); +``` + +## Function signature + +```typescript +function forwardToArbiter( + arbiterUrl: string, + options?: ForwardToArbiterOptions +): (context: SettleResultContext) => Promise +``` + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `arbiterUrl` | `string` | Base URL of your arbiter service (e.g. `http://arbiter:3001`) | +| `options` | `ForwardToArbiterOptions` | Optional configuration (see below) | + +### Options + +```typescript +interface ForwardToArbiterOptions { + /** Custom error handler. Defaults to `console.warn`. */ + onError?: (error: unknown) => void; +} +``` + +## Payload shape + +When a commerce settlement succeeds, the hook POSTs the following JSON to `{arbiterUrl}/verify`: + +```typescript +{ + responseBody: string; // UTF-8 encoded response body + transaction: string; // Settlement transaction hash + paymentPayload: { + x402Version: number; + accepted: { + scheme: string; // e.g. "commerce" + network: string; // e.g. "eip155:84532" + // ...other accepted fields + }; + payload: { + paymentInfo: { + operator: string; + payer: string; + receiver: string; + // ...full PaymentInfo + }; + }; + }; +} +``` + + +Arbiters that need `paymentInfo` for `release()` can read it directly from `paymentPayload.payload.paymentInfo` — no extra RPC call needed. + + +## Error handling + +By default, fetch errors are logged with `console.warn`. You can override this with a custom handler: + +```typescript +import { forwardToArbiter } from '@x402r/helpers'; + +const resourceServer = new x402ResourceServer(facilitatorClient) + .register(networkId, new EscrowServerScheme()) + .onAfterSettle( + forwardToArbiter('http://arbiter:3001', { + onError: (err) => sentry.captureException(err), + }) + ); +``` + +Errors are wrapped in an `X402rError` with the arbiter URL and request details for easier debugging. + +## Skipped scenarios + +The hook silently returns without making a request when: + +- The settlement was not successful (`context.result.success === false`) +- The scheme is not `commerce` (e.g. direct or other custom schemes) +- No response body is available in the transport context + +## Address re-exports + +The `@x402r/helpers` package re-exports chain-invariant address constants from `@x402r/core` for convenience: + +```typescript +import { + authCaptureEscrow, + tokenCollector, + protocolFeeConfig, + arbiterRegistry, + receiverRefundCollector, + usdcTvlLimit, + factories, + conditions, + getChainConfig, + supportedChainIds, +} from '@x402r/helpers'; +``` + +These are the same CREATE3 addresses available from `@x402r/core`. You can import from either package depending on which you already have installed. + +## Next steps + + + + Configure escrow options and fee bounds. + + + Build an arbiter that processes forwarded settlements. + + + See working merchant server examples. + + diff --git a/sdk/helpers/refundable.mdx b/sdk/helpers/refundable.mdx index 1d5fbfe..7e67163 100644 --- a/sdk/helpers/refundable.mdx +++ b/sdk/helpers/refundable.mdx @@ -155,6 +155,9 @@ app.get('/api/resource', (req, res) => { ## Next Steps + + Forward settlement data to an arbiter for evaluation. + Deploy a PaymentOperator to use with refundable(). diff --git a/sdk/installation.mdx b/sdk/installation.mdx index 7485080..f78b6f6 100644 --- a/sdk/installation.mdx +++ b/sdk/installation.mdx @@ -37,18 +37,43 @@ Create `publicClient` and `walletClient` using [viem](https://viem.sh/docs/clien ## Contract Addresses -Get the deployed contract addresses from the network config: +All protocol contracts use CREATE3 addresses that are the same on every supported chain. You can import them directly as named constants or look them up from a chain config: -```typescript -import { getNetworkConfig } from '@x402r/core'; + + + ```typescript + import { + authCaptureEscrow, + tokenCollector, + protocolFeeConfig, + arbiterRegistry, + receiverRefundCollector, + usdcTvlLimit, + factories, + conditions, + } from '@x402r/core'; + + console.log(authCaptureEscrow); // 0xBC15… + console.log(factories.paymentOperator); // 0x3Cd5… + console.log(conditions.payer); // 0x808b… + ``` + + + ```typescript + import { getChainConfig } from '@x402r/core'; -const config = getNetworkConfig('eip155:84532'); // Base Sepolia + const config = getChainConfig(84532); // Base Sepolia + + console.log(config.authCaptureEscrow); // Escrow contract + console.log(config.arbiterRegistry); // ArbiterRegistry contract + console.log(config.usdc); // USDC token address (chain-specific) + ``` + + -console.log(config.authCaptureEscrow); // Escrow contract -console.log(config.refundRequest); // RefundRequest contract -console.log(config.arbiterRegistry); // ArbiterRegistry contract -console.log(config.usdc); // USDC token address -``` + +The named constants and `getChainConfig()` return identical addresses. Use named constants when you don't need the chain-specific USDC address. + Network identifiers use the [EIP-155](https://eips.ethereum.org/EIPS/eip-155) format: `eip155:`. For Base Sepolia, use `'eip155:84532'`. For Base Mainnet, use `'eip155:8453'`. diff --git a/sdk/overview.mdx b/sdk/overview.mdx index c0e9ceb..ba1510c 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -28,7 +28,7 @@ The SDK is organized into packages designed for specific roles in the payment ec SDK for arbiters to resolve disputes and manage refund decisions. - Framework-agnostic helper to mark x402 payment options as refundable with escrow configuration. + Framework-agnostic helpers: `refundable()` for escrow configuration, `forwardToArbiter()` for arbiter integration, and re-exported address constants. From 7a233fa89d41ab39464bc3fce0e27e45ed8e75cf Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:05:29 +0000 Subject: [PATCH 06/37] docs(sdk): document DeliveryProtectionOperator preset exports Generated-By: mintlify-agent --- sdk/deploy-operator.mdx | 73 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index ca8f7a1..a06805f 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -52,6 +52,7 @@ console.log('Existing (reused):', result.summary.existingCount) | `escrowPeriodSeconds` | `bigint` | Escrow waiting period (e.g., `604800n` for 7 days) | | `freezeDurationSeconds` | `bigint` | How long freezes last. Default: `0n` (permanent until unfrozen) | | `operatorFeeBps` | `bigint` | Fee in basis points. Default: `0n` (no fee). `100n` = 1% | +| `authorizedCodehash` | `Hex` | Optional. Restricts which contract codehashes can record. Defaults to `bytes32(0)` (no restriction) | @@ -120,8 +121,23 @@ Deployment requires gas fees. Ensure your wallet has ETH on the target network. For automated quality verification (AI garbage detection, schema validation), use the simpler delivery protection preset. No RefundRequest, Evidence, or Freeze contracts. The arbiter is the only address that can release funds. ```typescript +import { createPublicClient, createWalletClient, http } from 'viem'; +import { baseSepolia } from 'viem/chains'; +import { privateKeyToAccount } from 'viem/accounts'; import { deployDeliveryProtectionOperator } from '@x402r/core' +const publicClient = createPublicClient({ + chain: baseSepolia, + transport: http(), +}) + +const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) +const walletClient = createWalletClient({ + account, + chain: baseSepolia, + transport: http(), +}) + const deployment = await deployDeliveryProtectionOperator( walletClient, publicClient, @@ -144,15 +160,56 @@ console.log('ArbiterCondition:', deployment.arbiterConditionAddress) | `arbiter` | `Address` | Only address that can call `release()` | | `feeRecipient` | `Address` | Receives protocol fees | | `escrowPeriodSeconds` | `bigint` | Verification window before auto-refund | +| `authorizedCodehash` | `Hex` | Optional. Restricts which contract codehashes can record. Defaults to `bytes32(0)` (no restriction) | - - | Slot | Contract | Purpose | - |------|----------|---------| - | `RELEASE_CONDITION` | StaticAddressCondition(arbiter) | Only arbiter can release | - | `AUTHORIZE_RECORDER` | EscrowPeriod | Records authorization time | - | `REFUND_IN_ESCROW_CONDITION` | EscrowPeriod | Anyone can refund after escrow expires | - | `REFUND_POST_ESCROW_CONDITION` | Receiver | Receiver can refund post-escrow | - + + + ```typescript + interface DeliveryProtectionOperatorDeployment { + operatorAddress: Address + escrowPeriodAddress: Address + arbiterConditionAddress: Address + operatorConfig: OperatorConfig + deployments: DeployResult[] + summary: { + newCount: number + existingCount: number + txHashes: `0x${string}`[] + } + } + ``` + + Redeploying with the same parameters is idempotent (CREATE3). It detects existing contracts and skips them. Check `summary` for what was new vs reused. + + + + Compute addresses without deploying: + + ```typescript + import { previewDeliveryProtectionOperator } from '@x402r/core' + + const preview = await previewDeliveryProtectionOperator(publicClient, { + chainId: 84532, + arbiter: '0xArbiterServiceAddress', + feeRecipient: '0xYourAddress...', + escrowPeriodSeconds: 300n, + }) + + console.log('Operator will be at:', preview.operatorAddress) + console.log('EscrowPeriod will be at:', preview.escrowPeriodAddress) + console.log('ArbiterCondition will be at:', preview.arbiterConditionAddress) + ``` + + + + | Slot | Contract | Purpose | + |------|----------|---------| + | `RELEASE_CONDITION` | StaticAddressCondition(arbiter) | Only arbiter can release | + | `AUTHORIZE_RECORDER` | EscrowPeriod | Records authorization time | + | `REFUND_IN_ESCROW_CONDITION` | EscrowPeriod | Anyone can refund after escrow expires | + | `REFUND_POST_ESCROW_CONDITION` | Receiver | Receiver can refund post-escrow | + + ## Next Steps From e3c8f7423ab4cedf8ed044eb5dea13838ca3193b Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 02:22:40 +0000 Subject: [PATCH 07/37] docs(sdk): update delivery protection docs for v2 OrConditions and PaymentIndexRecorder Generated-By: mintlify-agent --- sdk/delivery-arbiter.mdx | 6 +++- sdk/delivery-protection.mdx | 7 ++-- sdk/deploy-operator.mdx | 71 +++++++++++++++++++++++++++++++------ sdk/overview.mdx | 2 +- 4 files changed, 71 insertions(+), 15 deletions(-) diff --git a/sdk/delivery-arbiter.mdx b/sdk/delivery-arbiter.mdx index 66b3290..6ac4f48 100644 --- a/sdk/delivery-arbiter.mdx +++ b/sdk/delivery-arbiter.mdx @@ -76,7 +76,9 @@ app.post('/verify', async (req, res) => { await arbiter.payment.release(paymentInfo, amounts.capturableAmount) res.json({ verdict: 'PASS' }) } else { - // Do nothing. Funds auto-refund after escrow expires. + // Arbiter can refund immediately without waiting for escrow expiry. + const amounts = await arbiter.payment.getAmounts(paymentInfo) + await arbiter.refund.refundInEscrow(paymentInfo, amounts.capturableAmount) res.json({ verdict: 'FAIL' }) } }) @@ -108,6 +110,8 @@ async function evaluate(responseBody: string): Promise { ### 5. What Happens on Failure +With delivery protection v2, the arbiter can call `refundInEscrow()` immediately on a FAIL verdict. You do not need to wait for escrow expiry. The receiver (merchant) can also trigger a voluntary refund at any time. + If your service goes down, no payments get evaluated and funds stay in escrow until timeout. The escrow period protects payers, but add uptime monitoring and alerting. diff --git a/sdk/delivery-protection.mdx b/sdk/delivery-protection.mdx index a466e2d..e4867e1 100644 --- a/sdk/delivery-protection.mdx +++ b/sdk/delivery-protection.mdx @@ -4,16 +4,17 @@ description: "Automated quality verification for every transaction." icon: "shield-check" --- -In the delivery protection model, the arbiter evaluates every transaction automatically. Only the arbiter can release funds. If the arbiter does not release, funds auto-refund to the payer after escrow expires. +In the delivery protection model, the arbiter evaluates every transaction automatically. The arbiter or a satisfied payer can release funds. If the arbiter issues a FAIL verdict, it can trigger an immediate refund without waiting for escrow expiry. If nobody acts, funds auto-refund to the payer after escrow expires. This is different from the [marketplace model](/sdk/overview) where the merchant releases funds and the arbiter only gets involved when a payer files a dispute. | | Marketplace | Delivery Protection | |---|---|---| -| Who releases funds | Merchant (after escrow) | Arbiter only | +| Who releases funds | Merchant (after escrow) | Arbiter or payer | +| Refund during escrow | Receiver or arbiter | Escrow expiry, receiver, or arbiter | | Dispute process | Payer files refund request | No disputes needed | | Arbiter involvement | Only on disputes | Every transaction | -| Contracts needed | Operator + EscrowPeriod + RefundRequest + Evidence + Freeze | Operator + EscrowPeriod + StaticAddressCondition | +| Contracts deployed | ~8 (Operator, EscrowPeriod, RefundRequest, Evidence, Freeze, etc.) | 6 (Operator, EscrowPeriod, SAC, 2x OrCondition, RecorderCombinator) | | Deploy preset | `deployMarketplaceOperator()` | `deployDeliveryProtectionOperator()` | Use this when every response needs automated quality checks: AI content verification, garbage detection, schema validation. diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index ca8f7a1..8c19e62 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -117,7 +117,7 @@ Deployment requires gas fees. Ensure your wallet has ETH on the target network. ## Delivery Protection Operator -For automated quality verification (AI garbage detection, schema validation), use the simpler delivery protection preset. No RefundRequest, Evidence, or Freeze contracts. The arbiter is the only address that can release funds. +For automated quality verification (AI garbage detection, schema validation), use the delivery protection preset. No RefundRequest, Evidence, or Freeze contracts. The arbiter or payer can release funds, and the arbiter can issue immediate refunds without waiting for escrow expiry. ```typescript import { deployDeliveryProtectionOperator } from '@x402r/core' @@ -136,23 +136,74 @@ const deployment = await deployDeliveryProtectionOperator( console.log('Operator:', deployment.operatorAddress) console.log('EscrowPeriod:', deployment.escrowPeriodAddress) console.log('ArbiterCondition:', deployment.arbiterConditionAddress) +console.log('ReleaseCondition:', deployment.releaseConditionAddress) +console.log('AuthorizeRecorder:', deployment.authorizeRecorderAddress) ``` | Option | Type | Description | |--------|------|-------------| | `chainId` | `number` | Target chain | -| `arbiter` | `Address` | Only address that can call `release()` | +| `arbiter` | `Address` | Arbiter address for release and refund decisions | | `feeRecipient` | `Address` | Receives protocol fees | | `escrowPeriodSeconds` | `bigint` | Verification window before auto-refund | +| `authorizedCodehash` | `Hex` | Override the default `recorderCombinatorCodehash`. Optional | +| `paymentIndexRecorderAddress` | `Address` | Override the default PaymentIndexRecorder. Pass `zeroAddress` to skip on-chain payment indexing. Optional | +| `allowArbiterRefund` | `boolean` | Allow arbiter to refund immediately during escrow. Default: `true` | - - | Slot | Contract | Purpose | - |------|----------|---------| - | `RELEASE_CONDITION` | StaticAddressCondition(arbiter) | Only arbiter can release | - | `AUTHORIZE_RECORDER` | EscrowPeriod | Records authorization time | - | `REFUND_IN_ESCROW_CONDITION` | EscrowPeriod | Anyone can refund after escrow expires | - | `REFUND_POST_ESCROW_CONDITION` | Receiver | Receiver can refund post-escrow | - + + + ```typescript + interface DeliveryProtectionOperatorDeployment { + operatorAddress: Address + escrowPeriodAddress: Address + arbiterConditionAddress: Address + releaseConditionAddress: Address // OrCondition([arbiter, payer]) + refundInEscrowConditionAddress: Address // OrCondition([escrowPeriod, receiver, arbiter]) + authorizeRecorderAddress: Address // RecorderCombinator([escrowPeriod, paymentIndexRecorder]) + paymentIndexRecorderAddress: Address + operatorConfig: OperatorConfig + deployments: DeployResult[] + summary: { + newCount: number + existingCount: number + txHashes: `0x${string}`[] + } + } + ``` + + Deploys 6 contracts by default: EscrowPeriod, StaticAddressCondition(arbiter), OrCondition(release), OrCondition(refund), RecorderCombinator, and the Operator. If you pass `paymentIndexRecorderAddress: zeroAddress`, the RecorderCombinator is skipped (5 contracts). + + Redeploying with the same parameters is idempotent (CREATE3). It detects existing contracts and skips them. + + + + Compute addresses without deploying: + + ```typescript + import { previewDeliveryProtectionOperator } from '@x402r/core' + + const preview = await previewDeliveryProtectionOperator(publicClient, { + chainId: 84532, + arbiter: '0xArbiterServiceAddress', + feeRecipient: account.address, + escrowPeriodSeconds: 300n, + }) + + console.log('Operator will be at:', preview.operatorAddress) + console.log('EscrowPeriod will be at:', preview.escrowPeriodAddress) + console.log('AuthorizeRecorder will be at:', preview.authorizeRecorderAddress) + ``` + + + + | Slot | Contract | Purpose | + |------|----------|---------| + | `RELEASE_CONDITION` | OrCondition([SAC(arbiter), PayerCondition]) | Arbiter or satisfied payer can release | + | `AUTHORIZE_RECORDER` | RecorderCombinator([EscrowPeriod, PaymentIndexRecorder]) | Records authorization time and indexes payments on-chain | + | `REFUND_IN_ESCROW_CONDITION` | OrCondition([EscrowPeriod, ReceiverCondition, SAC(arbiter)]) | Escrow expiry, receiver voluntary refund, or arbiter immediate refund | + | `REFUND_POST_ESCROW_CONDITION` | ReceiverCondition | Only receiver after escrow | + + ## Next Steps diff --git a/sdk/overview.mdx b/sdk/overview.mdx index 4b43e59..14a0e90 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -16,7 +16,7 @@ Three roles interact with the protocol: **Marketplace** (`deployMarketplaceOperator`): The merchant releases funds after escrow. If the payer contests, they file a refund request and an arbiter resolves it. Use this for general commerce where most transactions are uncontested. -**Delivery Protection** (`deployDeliveryProtectionOperator`): The arbiter evaluates every transaction automatically and is the only address that can release funds. If the arbiter does not release, funds auto-refund after escrow. Use this for AI content verification, schema validation, or automated quality checks. +**Delivery Protection** (`deployDeliveryProtectionOperator`): The arbiter evaluates every transaction automatically. The arbiter or a satisfied payer can release funds. On a FAIL verdict, the arbiter can trigger an immediate refund. If nobody acts, funds auto-refund after escrow. Use this for AI content verification, schema validation, or automated quality checks. ### Packages From 0646a31832b1ab3e23cfae27820e4e8540a7c479 Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 05:30:30 +0000 Subject: [PATCH 08/37] docs(sdk): remove ArbiterRegistry references after contract removal Generated-By: mintlify-agent --- sdk/arbiter/ai-integration.mdx | 4 +- sdk/arbiter/decision-submission.mdx | 4 +- sdk/arbiter/quickstart.mdx | 2 - sdk/arbiter/registry.mdx | 162 ++++------------------------ 4 files changed, 27 insertions(+), 145 deletions(-) diff --git a/sdk/arbiter/ai-integration.mdx b/sdk/arbiter/ai-integration.mdx index 333b400..1976e14 100644 --- a/sdk/arbiter/ai-integration.mdx +++ b/sdk/arbiter/ai-integration.mdx @@ -215,7 +215,7 @@ AI-powered dispute resolution handles financial decisions. Always implement prom Approve/deny individual cases and execute refunds. - - Register your AI arbiter for on-chain discovery. + + How arbiters are discovered by merchants and clients. diff --git a/sdk/arbiter/decision-submission.mdx b/sdk/arbiter/decision-submission.mdx index 696479e..35f2f4c 100644 --- a/sdk/arbiter/decision-submission.mdx +++ b/sdk/arbiter/decision-submission.mdx @@ -245,8 +245,8 @@ flowchart TD Automate decisions with AI evaluation hooks. - - Register as an arbiter for on-chain discovery. + + How arbiters are discovered by merchants and clients. RefundRequest contract and state machine details. diff --git a/sdk/arbiter/quickstart.mdx b/sdk/arbiter/quickstart.mdx index dc24b32..6da45f1 100644 --- a/sdk/arbiter/quickstart.mdx +++ b/sdk/arbiter/quickstart.mdx @@ -23,7 +23,6 @@ const arbiter = new X402rArbiter({ walletClient, operatorAddress: '0x...', refundRequestAddress: config.refundRequest, - arbiterRegistryAddress: config.arbiterRegistry, }); ``` @@ -36,7 +35,6 @@ The Arbiter SDK currently supports: - **`executeRefundInEscrow()`** — Execute an approved refund to transfer funds back - **`getPendingRefundRequests()`** — List refund requests awaiting decision - **`getRefundRequestByKey()`** — Get details of a specific refund request -- **`registerArbiter()`** — Register in the on-chain ArbiterRegistry - **`isArbiterRegistered()`** — Check registration status - **`submitEvidence()`** — Attach evidence (IPFS CID) to a refund request - **`getEvidence()`** — Retrieve a single evidence entry by index diff --git a/sdk/arbiter/registry.mdx b/sdk/arbiter/registry.mdx index 1855c8d..ea7a35b 100644 --- a/sdk/arbiter/registry.mdx +++ b/sdk/arbiter/registry.mdx @@ -1,14 +1,26 @@ --- -title: "Arbiter Registry" -description: "Register, update, and discover arbiters using the on-chain ArbiterRegistry" +title: "Arbiter discovery" +description: "How arbiters are discovered by merchants and clients" icon: "clipboard-list" --- -The ArbiterRegistry is an on-chain contract that allows arbiters to register themselves, publish metadata URIs, and be discovered by merchants and clients. + +The on-chain `ArbiterRegistry` contract has been removed. Arbiter discovery is now handled off-chain. If you were using `arbiterRegistry`, `arbiterRegistryAbi`, or `config.arbiterRegistry`, remove those references from your code. + -## Setup +## What changed -To use registry methods, provide the `arbiterRegistryAddress` when creating the arbiter instance: +The `ArbiterRegistry` contract was deleted from x402r-contracts. ERC-8004 replaces on-chain arbiter discovery with off-chain mechanisms. + +The following exports have been removed from `@x402r/core` and `@x402r/helpers`: + +- `arbiterRegistry` (constant address) +- `arbiterRegistryAbi` +- `X402rChainConfig.arbiterRegistry` field + +## Migrating existing code + +Remove any references to `arbiterRegistryAddress` from your client setup: ```typescript import { X402rArbiter } from '@x402r/arbiter'; @@ -21,151 +33,23 @@ const arbiter = new X402rArbiter({ walletClient, operatorAddress: '0x...', refundRequestAddress: config.refundRequest, - arbiterRegistryAddress: config.arbiterRegistry, + // arbiterRegistryAddress is no longer needed }); ``` -## Register as an Arbiter - -Register your address in the on-chain registry with a URI pointing to your metadata or API endpoint: - -```typescript -const { txHash } = await arbiter.registerArbiter( - 'https://arbiter.example.com/api/disputes' -); -console.log('Registered:', txHash); -``` - -The URI can point to: -- An API endpoint for receiving dispute notifications -- A JSON metadata file describing your arbitration services -- An IPFS hash with your arbiter profile - -## Update Your URI - -Change your registered URI: - -```typescript -const { txHash } = await arbiter.updateArbiterUri( - 'https://new-arbiter.example.com/api' -); -console.log('URI updated:', txHash); -``` - -## Deregister - -Remove yourself from the registry: - -```typescript -const { txHash } = await arbiter.deregisterArbiter(); -console.log('Deregistered:', txHash); -``` - -## Query the Registry - -### Check if an Address is Registered - -```typescript -const isRegistered = await arbiter.isArbiterRegistered('0xArbiterAddress...'); -console.log('Is registered:', isRegistered); -``` - -### Get an Arbiter's URI - -```typescript -const uri = await arbiter.getArbiterUri('0xArbiterAddress...'); -console.log('URI:', uri); // empty string if not registered -``` - -### Get Total Arbiter Count - -```typescript -const count = await arbiter.getArbiterCount(); -console.log('Total arbiters:', count); -``` - -### List Arbiters (Paginated) - -```typescript -const { arbiters, uris, total } = await arbiter.listArbiters(0n, 10n); -console.log(`Showing ${arbiters.length} of ${total} arbiters`); - -for (let i = 0; i < arbiters.length; i++) { - console.log(`${arbiters[i]}: ${uris[i]}`); -} -``` - -## Method Reference - -| Method | Parameters | Returns | Description | -|--------|-----------|---------|-------------| -| `registerArbiter` | `uri: string` | `{ txHash }` | Register with a URI | -| `updateArbiterUri` | `newUri: string` | `{ txHash }` | Update registered URI | -| `deregisterArbiter` | (none) | `{ txHash }` | Remove from registry | -| `getArbiterUri` | `arbiter: Address` | `string` | Get arbiter's URI | -| `isArbiterRegistered` | `arbiter: Address` | `boolean` | Check registration | -| `getArbiterCount` | (none) | `bigint` | Total registered count | -| `listArbiters` | `offset: bigint, count: bigint` | `ArbiterList` | Paginated list | - -## ArbiterList Type - -```typescript -interface ArbiterList { - arbiters: readonly `0x${string}`[]; // Arbiter addresses - uris: readonly string[]; // Corresponding URIs - total: bigint; // Total registered count -} -``` - -## Complete Example - -```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { getNetworkConfig } from '@x402r/core'; - -async function main() { - const config = getNetworkConfig('eip155:84532')!; - - const arbiter = new X402rArbiter({ - publicClient, - walletClient, - operatorAddress: '0x...', - arbiterRegistryAddress: config.arbiterRegistry, - }); - - // Register - await arbiter.registerArbiter('https://my-arbiter.example.com/api'); - - // Verify - const isRegistered = await arbiter.isArbiterRegistered( - walletClient.account!.address - ); - console.log('Registered:', isRegistered); - - // Browse all arbiters - const { arbiters, uris, total } = await arbiter.listArbiters(0n, 100n); - console.log(`${total} arbiters registered:`); - for (let i = 0; i < arbiters.length; i++) { - console.log(` ${arbiters[i]} → ${uris[i]}`); - } -} - -main().catch(console.error); -``` - -## Next Steps +## Next steps - + Approve and deny refund requests. - + Automate dispute resolution with AI. - + Complete arbiter setup guide. - + Understand arbiter conditions in contracts. From 869fa47aa63bf0b43cce9446ab04779bc34a73d9 Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 05:49:49 +0000 Subject: [PATCH 09/37] docs(sdk): add ERC-8004 plugin reference for identity, reputation, and discovery Generated-By: mintlify-agent --- sdk/create-client.mdx | 79 +++++++++++++++++++++++++++++++++++++++++++ sdk/overview.mdx | 2 +- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/sdk/create-client.mdx b/sdk/create-client.mdx index 5df2a78..cc1dd71 100644 --- a/sdk/create-client.mdx +++ b/sdk/create-client.mdx @@ -96,6 +96,85 @@ const extended = client.extend( const payments = await extended.query.getPayerPayments(payerAddress) ``` +### ERC-8004 plugin + +The `erc8004Actions()` plugin adds on-chain identity, reputation, and service discovery. Registry addresses are resolved automatically from the chain config, but you can override them via options. + +```typescript +import { createX402r, erc8004Actions } from '@x402r/sdk' + +const client = createX402r({ publicClient, walletClient, operatorAddress: '0x...' }) + .extend(erc8004Actions()) + +// Identity +const isReg = await client.identity.isRegistered('0xABC...') +const txHash = await client.identity.register('https://example.com/agent.json') +const verified = await client.identity.verifyAgentId(7n, '0xABC...') +const agent = await client.identity.resolveAgent(7n) + +// Reputation +await client.reputation.rate(7n, 90) +const summary = await client.reputation.getSummary(7n, ['0xReviewer1...']) +await client.reputation.giveFeedback(7n, { + value: 500n, + valueDecimals: 2, + tag1: 'quality', + tag2: 'x402', +}) + +// Discovery +const endpoint = await client.discovery.resolveServiceEndpoint(7n, 'x402r.arbiter') +``` + + +`rate()` is a convenience wrapper around `giveFeedback()` that accepts a 0-100 integer score and uses the default tags (`starred` / `x402`). Use `giveFeedback()` when you need custom tags or decimal precision. + + +#### Plugin options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `registryAddress` | `Address` | Auto-resolved | Identity registry address override | +| `reputationRegistryAddress` | `Address` | Auto-resolved | Reputation registry address override | +| `defaultTag1` | `string` | `'starred'` | Default `tag1` for `rate()` and `getSummary()` | +| `defaultTag2` | `string` | `'x402'` | Default `tag2` for `rate()` and `getSummary()` | + +#### Identity methods + +| Method | Parameters | Returns | Description | +|--------|-----------|---------|-------------| +| `register` | `agentURI?: string` | `Hash` | Register the connected wallet as an agent | +| `verifyAgentId` | `agentId: bigint, claimedAddress: Address` | `boolean` | Check if an agent ID belongs to the claimed address | +| `resolveAgent` | `agentId: bigint` | `ResolvedAgent` | Look up full agent record by ID | +| `isRegistered` | `address: Address` | `boolean` | Check if an address is registered | + +#### Reputation methods + +| Method | Parameters | Returns | Description | +|--------|-----------|---------|-------------| +| `rate` | `agentId: bigint, score: number` | `Hash` | Submit a 0-100 rating using default tags | +| `getSummary` | `agentId: bigint, reviewers: Address[], options?` | `ReputationSummary` | Aggregate reputation from specific reviewers | +| `giveFeedback` | `agentId: bigint, params: Erc8004GiveFeedbackParams` | `Hash` | Submit feedback with full control over tags and metadata | + +#### Discovery methods + +| Method | Parameters | Returns | Description | +|--------|-----------|---------|-------------| +| `resolveServiceEndpoint` | `agentId: bigint, serviceName: string` | `ResolvedServiceEndpoint` | Resolve an agent's service endpoint by name | + +#### Re-exported types + +The following types from `@x402r/erc8004` are re-exported by `@x402r/sdk` for convenience: + +- `ResolvedAgent` - full agent identity record +- `ReputationSummary` - aggregated reputation data +- `ResolvedServiceEndpoint` - resolved service endpoint record +- `Erc8004PluginOptions` - plugin configuration +- `Erc8004GiveFeedbackParams` - parameters for `giveFeedback()` +- `Erc8004IdentityActions` - identity namespace type +- `Erc8004ReputationActions` - reputation namespace type +- `Erc8004DiscoveryActions` - discovery namespace type + ## Next Steps diff --git a/sdk/overview.mdx b/sdk/overview.mdx index 4b43e59..ddd1dc5 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -32,7 +32,7 @@ bun add @x402r/sdk ``` -`@x402r/sdk` is the only package most developers need. It includes role-scoped client factories, 8 action groups (payment, escrow, refund, evidence, freeze, query, operator, watch), and an `.extend()` plugin system. +`@x402r/sdk` is the only package most developers need. It includes role-scoped client factories, 8 action groups (payment, escrow, refund, evidence, freeze, query, operator, watch), and an `.extend()` plugin system. The `erc8004Actions()` plugin adds on-chain identity, reputation, and service discovery via [ERC-8004](/sdk/create-client#erc-8004-plugin). For low-level access to contract ABIs and deploy utilities: From 00b6300b695198f2efd6ce0fd3317f19b51b87f0 Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:10:54 +0000 Subject: [PATCH 10/37] Update SDK docs to match @x402r/sdk 0.2.x API surface Replace references to removed packages (@x402r/client, @x402r/merchant, @x402r/arbiter) and class-based APIs (X402rClient, X402rMerchant, X402rArbiter) with the unified @x402r/sdk factory pattern (createPayerClient, createMerchantClient, createArbiterClient) and action group APIs (client.payment.*, client.refund.*, client.watch.*, etc.). Fix method counts, add missing config fields, update enum names, and rewrite the arbiter registry page to use the ERC-8004 plugin. Generated-By: mintlify-agent --- sdk/arbiter/ai-integration.mdx | 283 ++++++++++--------------- sdk/arbiter/batch-operations.mdx | 201 ++++++------------ sdk/arbiter/decision-submission.mdx | 260 ++++++++++------------- sdk/arbiter/quickstart.mdx | 69 +++--- sdk/arbiter/registry.mdx | 185 ++++++++-------- sdk/arbiter/subscriptions.mdx | 134 +++++------- sdk/client/escrow-management.mdx | 173 +++++---------- sdk/client/payment-queries.mdx | 114 +++++----- sdk/client/quickstart.mdx | 74 +++---- sdk/client/refund-operations.mdx | 116 ++++++----- sdk/client/subscriptions.mdx | 238 +++++---------------- sdk/concepts.mdx | 38 ++-- sdk/create-client.mdx | 3 +- sdk/limitations.mdx | 33 +-- sdk/merchant/payment-operations.mdx | 230 +++++++------------- sdk/merchant/quickstart.mdx | 218 +++++++++---------- sdk/merchant/refund-handling.mdx | 313 ++++++++++++---------------- sdk/merchant/subscriptions.mdx | 217 +++++++------------ 18 files changed, 1144 insertions(+), 1755 deletions(-) diff --git a/sdk/arbiter/ai-integration.mdx b/sdk/arbiter/ai-integration.mdx index 333b400..4bc4358 100644 --- a/sdk/arbiter/ai-integration.mdx +++ b/sdk/arbiter/ai-integration.mdx @@ -1,209 +1,144 @@ --- -title: "AI Integration" -description: "Automate dispute resolution with AI using the Arbiter SDK's evaluation hooks" +title: "AI integration" +description: "Automate dispute resolution with AI using the arbiter client" icon: "robot" --- -The Arbiter SDK includes built-in support for AI-powered dispute resolution through evaluation hooks and a webhook handler. You can plug in any AI model -- LLMs, rule engines, or hybrid systems -- to automatically evaluate and decide on refund requests. +You can build AI-powered dispute resolution on top of the arbiter client by combining the `watch`, `refund`, `evidence`, and `payment` action groups. Plug in any AI model (LLMs, rule engines, or hybrid systems) to automatically evaluate and decide on refund requests. -## Core Concepts +## Watch and auto-evaluate pattern -The AI integration is built around three types: - -1. **CaseEvaluationContext** -- the data your AI receives for each case -2. **DecisionResult** -- the decision your AI returns -3. **createWebhookHandler** -- wires your AI evaluation function to the arbiter - -## Case Evaluation Context - -When a case needs evaluation, the handler provides a structured context: +The most common pattern combines `watch.onRefundRequest` with your evaluation logic to automatically process incoming refund requests: ```typescript -interface CaseEvaluationContext { - /** The payment information struct */ - paymentInfo: PaymentInfo; - - /** The record index (nonce) identifying which charge */ - nonce: bigint; - - /** Current payment state */ - paymentState: PaymentState; - - /** Current refund request status */ - refundStatus: number; - - /** Unique hash of the payment */ - paymentInfoHash: `0x${string}`; - - /** Amount being requested for refund */ - refundAmount?: bigint; +import { createArbiterClient, RefundRequestStatus } from '@x402r/sdk' +import type { PaymentInfo } from '@x402r/sdk' + +const arbiter = createArbiterClient({ + publicClient, + walletClient, + operatorAddress: '0x...', + refundRequestAddress: '0x...', + refundRequestEvidenceAddress: '0x...', + escrowPeriodAddress: '0x...', +}) + +// Watch for new refund requests +const unwatch = arbiter.watch.onRefundRequest(async (logs) => { + for (const log of logs) { + console.log('New refund event:', log.eventName) + + // Look up the full payment info from your database or the refund contract + const paymentInfo = await lookupPaymentInfo(log) + + // Evaluate the case + const decision = await evaluateWithAI(arbiter, paymentInfo) + + if (decision.approve && decision.confidence >= 0.9) { + const request = await arbiter.refund?.get(paymentInfo) + if (request && request.status === RefundRequestStatus.Pending) { + await arbiter.payment.refundInEscrow(paymentInfo, request.amount) + console.log('Auto-approved refund') + } + } else if (!decision.approve && decision.confidence >= 0.9) { + await arbiter.refund?.deny(paymentInfo) + console.log('Auto-denied refund') + } else { + console.log('Low confidence, queuing for manual review') + } + } +}) - /** Optional evidence/metadata */ - evidence?: unknown; -} +// Graceful shutdown +process.on('SIGINT', () => { + unwatch() + process.exit() +}) ``` -## Decision Result +## Building the evaluation context -Your AI evaluation function returns a `DecisionResult`: +Gather all available on-chain data before sending to your AI model: ```typescript -interface DecisionResult { - /** The decision: approve or deny */ - decision: 'approve' | 'deny'; - - /** Optional reasoning for the decision */ - reasoning?: string; - - /** Optional partial refund amount */ - refundAmount?: bigint; +async function buildEvaluationContext( + arbiter: ReturnType, + paymentInfo: PaymentInfo, +) { + // Get payment state and amounts + const state = await arbiter.payment.getState(paymentInfo) + const amounts = await arbiter.payment.getAmounts(paymentInfo) + + // Get the refund request + const request = await arbiter.refund?.get(paymentInfo) + + // Check escrow timing + const inEscrow = await arbiter.escrow?.isDuringEscrow(paymentInfo) + + // Get evidence if available + const evidenceCount = await arbiter.evidence?.count(paymentInfo) + let evidence: Array<{ cid: string; submitter: string }> = [] + + if (evidenceCount && evidenceCount > 0n) { + const batch = await arbiter.evidence?.getBatch(paymentInfo, 0n, evidenceCount) + evidence = (batch?.entries ?? []).map((e) => ({ + cid: e.cid, + submitter: e.submitter, + })) + } - /** Confidence score (0-1) */ - confidence?: number; + return { + paymentInfo, + state, + amounts, + request, + inEscrow, + evidence, + } } ``` -## Create a Webhook Handler - -Use `createWebhookHandler` to connect your evaluation function to the arbiter: - -```typescript -import { createWebhookHandler } from '@x402r/arbiter'; -import type { CaseEvaluationContext, DecisionResult } from '@x402r/arbiter'; - -const handler = createWebhookHandler({ - arbiter, - evaluationHook: async (context: CaseEvaluationContext): Promise => { - // Your AI evaluation logic here - const result = await myAIModel.evaluate(context); - - return { - decision: result.shouldApprove ? 'approve' : 'deny', - reasoning: result.explanation, - confidence: result.confidence, - }; - }, - autoSubmitDecision: true, // Auto-submit approve/deny on-chain - confidenceThreshold: 0.9, // Only auto-submit if confidence >= 0.9 -}); -``` +## Evaluation patterns - -Setting `autoSubmitDecision: true` calls `approveRefundRequest` or `denyRefundRequest` on-chain automatically. This submits the decision only -- executing the actual refund transfer via `executeRefundInEscrow` is a separate step you handle after approval. - +Three common approaches for the evaluation function: -## Webhook Handler Configuration +- **LLM-based**: send the structured context to an LLM (GPT-4, Claude, etc.) with a system prompt that outputs JSON `{decision, reasoning, confidence}`. Sanitize inputs to prevent prompt injection. +- **Rule-based**: apply deterministic rules (amount thresholds, blocklists) for predictable, high-confidence decisions. Best for clear-cut cases. +- **Hybrid**: apply hard rules first; if no rule matches with high confidence, fall back to LLM evaluation. Deny and flag for manual review when confidence is low. ```typescript -interface WebhookHandlerConfig { - /** X402rArbiter instance */ - arbiter: X402rArbiter; - - /** Your evaluation function */ - evaluationHook: ArbiterHook; - - /** Auto-submit decisions on-chain (default: false) */ - autoSubmitDecision?: boolean; - - /** Minimum confidence for auto-submission (default: 0.8) */ - confidenceThreshold?: number; -} -``` - -The handler returns a `WebhookResult` that extends `DecisionResult`: - -```typescript -interface WebhookResult extends DecisionResult { - /** Transaction hash if auto-submitted */ - txHash?: `0x${string}`; - - /** Whether the decision was submitted on-chain */ - executed: boolean; +interface DecisionResult { + approve: boolean + reasoning: string + confidence: number } -``` -## Watch and Auto-Evaluate Pattern +async function evaluateWithAI( + arbiter: ReturnType, + paymentInfo: PaymentInfo, +): Promise { + const context = await buildEvaluationContext(arbiter, paymentInfo) -The most common pattern combines `watchNewCases` with the webhook handler to automatically evaluate incoming refund requests: - -```typescript -import { X402rArbiter, createWebhookHandler } from '@x402r/arbiter'; -import type { CaseEvaluationContext, DecisionResult, RefundRequestEventLog } from '@x402r/arbiter'; -import { PaymentState, RequestStatus } from '@x402r/core'; -import type { PaymentInfo } from '@x402r/core'; - -// Step 1: Create the webhook handler with your AI evaluation -const handler = createWebhookHandler({ - arbiter, - evaluationHook: async (context: CaseEvaluationContext): Promise => { - return evaluateWithAI(context); - }, - autoSubmitDecision: true, - confidenceThreshold: 0.85, -}); - -// Step 2: Watch for new cases and feed them to the handler -const { unsubscribe } = arbiter.watchNewCases(async (event: RefundRequestEventLog) => { - const paymentInfoHash = event.args.paymentInfoHash!; - const nonce = event.args.nonce ?? 0n; - - console.log(`[NEW CASE] ${paymentInfoHash} (nonce: ${nonce})`); - - // Build the evaluation context - // NOTE: You need to reconstruct the full PaymentInfo from your database or event logs - const paymentInfo = await lookupPaymentInfo(paymentInfoHash); - - const context: CaseEvaluationContext = { - paymentInfo, - nonce, - paymentState: PaymentState.InEscrow, - refundStatus: RequestStatus.Pending, - paymentInfoHash, - refundAmount: event.args.amount, - }; - - // Evaluate and optionally auto-submit - const result = await handler(context); - - console.log(`[DECISION] ${result.decision} (confidence: ${result.confidence})`); - console.log(`[REASONING] ${result.reasoning}`); - - if (result.executed) { - console.log(`[ON-CHAIN] Decision submitted: ${result.txHash}`); - - // If approved, execute the refund transfer - if (result.decision === 'approve') { - const { txHash } = await arbiter.executeRefundInEscrow( - paymentInfo, - result.refundAmount // partial refund if specified - ); - console.log(`[REFUND] Executed: ${txHash}`); - } - } else { - console.log('[SKIPPED] Confidence below threshold, requires manual review'); + // Rule-based: auto-approve small amounts + if (context.request && context.request.amount < 5_000_000n) { + return { approve: true, reasoning: 'Small amount auto-approved', confidence: 1.0 } } -}); -// Graceful shutdown -process.on('SIGINT', () => { - unsubscribe(); - process.exit(); -}); + // LLM fallback for larger amounts + const llmResult = await callYourLLM(context) + return { + approve: llmResult.decision === 'approve', + reasoning: llmResult.explanation, + confidence: llmResult.confidence, + } +} ``` -## Evaluation Patterns - -The `evaluationHook` function receives a `CaseEvaluationContext` and returns a `DecisionResult`. Three common patterns: - -- **LLM-based** — Send the structured context to an LLM (GPT-4, Claude, etc.) with a system prompt that outputs JSON `{decision, reasoning, confidence}`. Sanitize inputs to prevent prompt injection. -- **Rule-based** — Apply deterministic rules (amount thresholds, blocklists) for predictable, high-confidence decisions. Best for clear-cut cases. -- **Hybrid** — Apply hard rules first; if no rule matches with high confidence, fall back to LLM evaluation. Deny and flag for manual review when confidence is low. - AI-powered dispute resolution handles financial decisions. Always implement prompt injection protection, input validation, and confidence thresholds before deploying to production. -## Next Steps +## Next steps @@ -216,6 +151,6 @@ AI-powered dispute resolution handles financial decisions. Always implement prom Approve/deny individual cases and execute refunds. - Register your AI arbiter for on-chain discovery. + Register your arbiter for on-chain discovery. diff --git a/sdk/arbiter/batch-operations.mdx b/sdk/arbiter/batch-operations.mdx index 7f32132..e7bfc50 100644 --- a/sdk/arbiter/batch-operations.mdx +++ b/sdk/arbiter/batch-operations.mdx @@ -1,180 +1,107 @@ --- -title: "Batch Operations" -description: "Process multiple refund decisions efficiently with the Arbiter SDK" +title: "Batch operations" +description: "Process multiple refund decisions efficiently as an arbiter" icon: "layer-group" --- -The Arbiter SDK provides batch operations for processing multiple refund requests in a single call. Both `batchApprove` and `batchDeny` accept an array of `{ paymentInfo, nonce }` objects. +The arbiter client can process multiple refund requests by iterating over cases and calling refund/payment methods individually. There is no built-in batch method, but you can build batch workflows on top of the action groups. -Batch items are processed **sequentially**, not atomically. If one item fails mid-batch, all previously processed items will **not** be rolled back. Design your error handling accordingly. +Each refund decision is a separate on-chain transaction. If one fails mid-batch, previously processed items are not rolled back. Design your error handling accordingly. -## Batch Approve +## Batch approve and execute -Approve multiple refund requests in one call: +Fetch pending cases, evaluate each one, then approve or deny: ```typescript -const items = [ - { paymentInfo: paymentInfo1, nonce: 0n }, - { paymentInfo: paymentInfo2, nonce: 0n }, - { paymentInfo: paymentInfo3, nonce: 1n }, -]; +import { createArbiterClient, RefundRequestStatus } from '@x402r/sdk' +import type { PaymentInfo } from '@x402r/sdk' -const results = await arbiter.batchApprove(items); - -for (const { txHash } of results) { - console.log('Approved:', txHash); -} -``` - -## Batch Deny - -Deny multiple refund requests in one call: - -```typescript -const items = [ - { paymentInfo: paymentInfo4, nonce: 0n }, - { paymentInfo: paymentInfo5, nonce: 0n }, -]; - -const results = await arbiter.batchDeny(items); - -for (const { txHash } of results) { - console.log('Denied:', txHash); -} -``` - -## Empty Batch Handling - -Both batch methods safely handle empty arrays and return an empty results array: - -```typescript -const results = await arbiter.batchApprove([]); -console.log(results.length); // 0 -``` - -## Item Format - -Each item in the batch array must include both the `paymentInfo` struct and the `nonce`: - -```typescript -interface BatchItem { - /** The full PaymentInfo struct identifying the payment */ - paymentInfo: PaymentInfo; - /** The record index (nonce) from PaymentIndexRecorder */ - nonce: bigint; -} -``` - - -The `nonce` identifies which specific charge record the refund request targets. For most single-charge payments, this is `0n`. - - -## Example: Triage and Batch Process Pending Cases - -Fetch all pending cases, evaluate each one, then batch approve and deny: - -```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { RequestStatus } from '@x402r/core'; -import type { PaymentInfo, RefundRequestData } from '@x402r/core'; - -async function triageAndProcess( - arbiter: X402rArbiter, - receiverAddress: `0x${string}`, - lookupPaymentInfo: (hash: `0x${string}`) => Promise +async function batchProcess( + arbiter: ReturnType, + paymentInfos: PaymentInfo[], ) { - // Step 1: Fetch pending cases - const { keys, total } = await arbiter.getPendingRefundRequests(0n, 100n, receiverAddress); - console.log(`Processing ${total} pending cases`); - - const toApprove: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> = []; - const toDeny: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> = []; + const approved: string[] = [] + const denied: string[] = [] - // Step 2: Evaluate each case - for (const key of keys) { - const request = await arbiter.getRefundRequestByKey(key); - - if (request.status !== RequestStatus.Pending) { - continue; - } + for (const paymentInfo of paymentInfos) { + const request = await arbiter.refund?.get(paymentInfo) + if (!request || request.status !== RefundRequestStatus.Pending) continue - const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash); - const item = { paymentInfo, nonce: request.nonce }; + const shouldApprove = request.amount < 10_000_000n // Auto-approve < 10 USDC - if (shouldApprove(request)) { - toApprove.push(item); + if (shouldApprove) { + const tx = await arbiter.payment.refundInEscrow(paymentInfo, request.amount) + approved.push(tx) } else { - toDeny.push(item); + const tx = await arbiter.refund?.deny(paymentInfo) + if (tx) denied.push(tx) } } - // Step 3: Batch process decisions - const approveResults = await arbiter.batchApprove(toApprove); - const denyResults = await arbiter.batchDeny(toDeny); - - console.log(`Approved: ${approveResults.length}, Denied: ${denyResults.length}`); - - return { approved: approveResults, denied: denyResults }; -} - -function shouldApprove(request: RefundRequestData): boolean { - // Your decision logic here - return request.amount < BigInt('10000000'); // Auto-approve < 10 USDC + console.log('Approved:', approved.length, 'Denied:', denied.length) + return { approved, denied } } ``` -## Example: Batch Approve with Refund Execution +## Triage from operator requests -After batch approving, execute refunds individually for each approved payment: +Fetch all pending cases for your operator and triage them: ```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import type { PaymentInfo } from '@x402r/core'; - -async function batchApproveAndExecute( - arbiter: X402rArbiter, - items: Array<{ paymentInfo: PaymentInfo; nonce: bigint }> +async function triageOperatorCases( + arbiter: ReturnType, + lookupPaymentInfo: (hash: `0x${string}`) => Promise, ) { - // Step 1: Batch approve all items - const approveResults = await arbiter.batchApprove(items); - console.log(`Approved ${approveResults.length} refund requests`); - - // Step 2: Execute refunds individually - const executeResults: Array<{ txHash: `0x${string}` }> = []; - - for (const { paymentInfo } of items) { - try { - const { txHash } = await arbiter.executeRefundInEscrow(paymentInfo); - executeResults.push({ txHash }); - console.log('Refund executed:', txHash); - } catch (error) { - console.error(`Failed to execute refund for ${paymentInfo.payer}:`, error); + // Step 1: Fetch all refund requests for this operator + const requests = await arbiter.refund?.getOperatorRequests( + arbiter.config.operatorAddress, + 0n, + 100n, + ) + + // Step 2: Filter to pending only + const pending = (requests ?? []).filter( + (r) => r.status === RefundRequestStatus.Pending, + ) + console.log('Pending cases:', pending.length) + + // Step 3: Process each + for (const request of pending) { + const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash) + + // Review evidence if available + const evidenceCount = await arbiter.evidence?.count(paymentInfo) + if (evidenceCount && evidenceCount > 0n) { + const batch = await arbiter.evidence?.getBatch(paymentInfo, 0n, evidenceCount) + console.log('Evidence entries:', batch?.entries.length) } - } - return { - approved: approveResults, - executed: executeResults, - }; + // Make decision + if (request.amount < 10_000_000n) { + await arbiter.payment.refundInEscrow(paymentInfo, request.amount) + } else { + await arbiter.refund?.deny(paymentInfo) + } + } } ``` -## Performance Considerations +## Performance considerations -Each item in a batch results in a separate on-chain transaction. Gas costs scale linearly with the number of items. Plan batch sizes around your RPC provider's rate limits. +Each decision is a separate on-chain transaction. Gas costs scale linearly with the number of items. Plan batch sizes around your RPC provider's rate limits. | Factor | Detail | |--------|--------| -| **Transaction ordering** | Items are processed sequentially to ensure correct nonce ordering. | -| **Gas costs** | Each item is a separate transaction. Batch methods save on SDK overhead, not gas. | +| **Transaction ordering** | Process items sequentially to ensure correct nonce ordering. | +| **Gas costs** | Each item is a separate transaction. Batch processing saves SDK overhead, not gas. | | **Partial failures** | If one transaction fails, previous ones remain on-chain. Handle partial failures in your logic. | | **Rate limiting** | Large batches may hit RPC rate limits. Consider adding delays for 50+ item batches. | -## Next Steps +## Next steps @@ -184,7 +111,7 @@ Each item in a batch results in a separate on-chain transaction. Gas costs scale Watch for new cases in real-time. - Individual approve/deny methods and executeRefundInEscrow. + Individual approve/deny methods and refund execution. Review the complete arbiter setup guide. diff --git a/sdk/arbiter/decision-submission.mdx b/sdk/arbiter/decision-submission.mdx index 696479e..0f2136a 100644 --- a/sdk/arbiter/decision-submission.mdx +++ b/sdk/arbiter/decision-submission.mdx @@ -1,242 +1,198 @@ --- -title: "Decision Submission" -description: "Submit decisions on refund requests and execute refunds with the Arbiter SDK" +title: "Decision submission" +description: "Submit decisions on refund requests and execute refunds as an arbiter" icon: "gavel" --- -The Arbiter SDK provides methods for reviewing refund requests, making decisions, and executing refunds for disputed payments. +The arbiter client provides methods for reviewing refund requests, making decisions, and executing refunds through the `refund`, `payment`, and `evidence` action groups. -## Approve a Refund Request +## Approve a refund (refundInEscrow) -Approve a pending refund request. This updates the on-chain status but does not transfer funds. +To approve and execute a refund in one step, call `payment.refundInEscrow()`. This auto-approves the pending RefundRequest and transfers funds back to the payer. ```typescript -const { txHash } = await arbiter.approveRefundRequest(paymentInfo, 0n); -console.log('Refund approved:', txHash); +const amounts = await arbiter.payment.getAmounts(paymentInfo) +const tx = await arbiter.payment.refundInEscrow(paymentInfo, amounts.refundableAmount) +console.log('Refund approved and executed:', tx) ``` - -Approving a refund request updates the request status to `Approved` but does not transfer funds. You must call `executeRefundInEscrow()` separately to move funds back to the payer. - + +`payment.refundInEscrow()` auto-approves the pending RefundRequest. There is no undo. + -## Deny a Refund Request +## Deny a refund request Deny a pending refund request: ```typescript -const { txHash } = await arbiter.denyRefundRequest(paymentInfo, 0n); -console.log('Refund denied:', txHash); +const tx = await arbiter.refund?.deny(paymentInfo) +console.log('Refund denied:', tx) ``` -## Execute Refund in Escrow +## Refuse to rule -After approving a refund request, execute the actual fund transfer back to the payer: +Decline to rule on a dispute (e.g., conflict of interest): ```typescript -// Full refund (defaults to paymentInfo.maxAmount) -const { txHash } = await arbiter.executeRefundInEscrow(paymentInfo); -console.log('Full refund executed:', txHash); - -// Partial refund -const partialAmount = BigInt('500000'); // 0.5 USDC -const { txHash: partialTx } = await arbiter.executeRefundInEscrow(paymentInfo, partialAmount); -console.log('Partial refund executed:', partialTx); +const tx = await arbiter.refund?.refuse(paymentInfo) +console.log('Declined to rule:', tx) ``` - -When no `amount` is provided, `executeRefundInEscrow` defaults to `paymentInfo.maxAmount`, issuing a full refund. - - -## Check If a Refund Request Exists - -Verify whether a refund request has been submitted for a given payment and nonce: +## Check if a refund request exists ```typescript -const hasRequest = await arbiter.hasRefundRequest(paymentInfo, 0n); +const hasRequest = await arbiter.refund?.has(paymentInfo) if (!hasRequest) { - console.log('No refund request found for this payment'); - return; + console.log('No refund request found for this payment') + return } ``` -## Get Refund Request Data +## Get refund request data Retrieve the full refund request data, including amount and status: ```typescript -import { RequestStatus } from '@x402r/core'; +import { RefundRequestStatus } from '@x402r/sdk' -const request = await arbiter.getRefundRequest(paymentInfo, 0n); +const request = await arbiter.refund?.get(paymentInfo) -console.log('Payment hash:', request.paymentInfoHash); -console.log('Nonce:', request.nonce); -console.log('Refund amount:', request.amount); -console.log('Status:', RequestStatus[request.status]); +console.log('Payment hash:', request?.paymentInfoHash) +console.log('Refund amount:', request?.amount) +console.log('Approved amount:', request?.approvedAmount) +console.log('Status:', request?.status) ``` The `RefundRequestData` type contains: ```typescript interface RefundRequestData { - paymentInfoHash: `0x${string}`; - nonce: bigint; - amount: bigint; - status: RequestStatus; + paymentInfoHash: `0x${string}` + amount: bigint + approvedAmount: bigint + status: RefundRequestStatus } ``` -## Get Refund Request Status - -Query the current status of a specific refund request: +## Get refund request status ```typescript -import { RequestStatus } from '@x402r/core'; +import { RefundRequestStatus } from '@x402r/sdk' -const status = await arbiter.getRefundStatus(paymentInfo, 0n); +const status = await arbiter.refund?.getStatus(paymentInfo) switch (status) { - case RequestStatus.Pending: - console.log('Awaiting decision'); - break; - case RequestStatus.Approved: - console.log('Already approved'); - break; - case RequestStatus.Denied: - console.log('Already denied'); - break; - case RequestStatus.Cancelled: - console.log('Cancelled by payer'); - break; + case RefundRequestStatus.Pending: + console.log('Awaiting decision') + break + case RefundRequestStatus.Approved: + console.log('Already approved') + break + case RefundRequestStatus.Denied: + console.log('Already denied') + break + case RefundRequestStatus.Cancelled: + console.log('Cancelled by payer') + break + case RefundRequestStatus.Refused: + console.log('Arbiter declined to rule') + break } ``` -## Get Pending Refund Requests (Paginated) +## List refund requests (paginated) -Retrieve a paginated list of refund request keys for a receiver. You can optionally filter by receiver address: +Retrieve paginated refund requests for an operator: ```typescript -// Get the first 10 pending requests for a specific receiver -const { keys, total } = await arbiter.getPendingRefundRequests( - 0n, // offset - 10n, // count - '0xReceiverAddress...' // optional: defaults to the arbiter's wallet address -); - -console.log(`${total} total cases, showing first ${keys.length}`); - -// Look up each request by its composite key -for (const key of keys) { - const request = await arbiter.getRefundRequestByKey(key); - console.log(`Amount: ${request.amount}, Status: ${RequestStatus[request.status]}`); +const requests = await arbiter.refund?.getOperatorRequests( + arbiter.config.operatorAddress, + 0n, // offset + 50n, // count +) + +for (const request of requests ?? []) { + console.log('Amount:', request.amount, 'Status:', request.status) } ``` -## Get Refund Request Count - -Get the total number of refund requests for a receiver: +You can also query by payer or receiver: ```typescript -const count = await arbiter.getRefundRequestCount('0xReceiverAddress...'); -console.log(`Total refund requests: ${count}`); - -// Defaults to the arbiter's wallet address if not specified -const myCount = await arbiter.getRefundRequestCount(); -console.log(`My refund requests: ${myCount}`); +const payerRequests = await arbiter.refund?.getPayerRequests(payerAddress, 0n, 10n) +const receiverRequests = await arbiter.refund?.getReceiverRequests(receiverAddress, 0n, 10n) ``` -## Get Refund Request by Composite Key - -Look up a specific refund request using its `keccak256(paymentInfoHash, nonce)` composite key: +## Check if a payment is frozen ```typescript -const request = await arbiter.getRefundRequestByKey(compositeKey); - -console.log('Payment hash:', request.paymentInfoHash); -console.log('Amount:', request.amount); -console.log('Status:', RequestStatus[request.status]); -``` - -## Check If a Payment Is Frozen - -Verify whether a payment is currently frozen by a Freeze condition contract: - -```typescript -const frozen = await arbiter.isFrozen(paymentInfo, freezeAddress); +const frozen = await arbiter.freeze?.isFrozen(paymentInfo) if (frozen) { - console.log('Payment is frozen - dispute in progress'); -} else { - console.log('Payment is not frozen'); + console.log('Payment is frozen, dispute in progress') } ``` -## Complete Decision Workflow +## Complete decision workflow -This example shows the full arbiter workflow: fetching pending cases, reviewing them, making a decision, and executing the refund. +This example shows the full arbiter workflow: fetching pending cases, reviewing evidence, making a decision, and executing the refund. ```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { RequestStatus } from '@x402r/core'; -import type { PaymentInfo } from '@x402r/core'; - -async function processAllPendingCases(arbiter: X402rArbiter, receiverAddress: `0x${string}`) { - // Step 1: Get all pending refund requests - const { keys, total } = await arbiter.getPendingRefundRequests(0n, 50n, receiverAddress); - console.log(`Found ${total} pending cases`); - - for (const key of keys) { - // Step 2: Retrieve request details - const request = await arbiter.getRefundRequestByKey(key); - - // Step 3: Skip if already decided - if (request.status !== RequestStatus.Pending) { - console.log(`Skipping ${request.paymentInfoHash} - already ${RequestStatus[request.status]}`); - continue; +import { createArbiterClient, RefundRequestStatus } from '@x402r/sdk' +import type { PaymentInfo } from '@x402r/sdk' + +async function processCase( + arbiter: ReturnType, + paymentInfo: PaymentInfo, +) { + // Step 1: Check if a refund request exists + const hasRequest = await arbiter.refund?.has(paymentInfo) + if (!hasRequest) return + + // Step 2: Get the request data + const request = await arbiter.refund?.get(paymentInfo) + if (request?.status !== RefundRequestStatus.Pending) return + + // Step 3: Review evidence + const evidenceCount = await arbiter.evidence?.count(paymentInfo) + if (evidenceCount && evidenceCount > 0n) { + const batch = await arbiter.evidence?.getBatch(paymentInfo, 0n, evidenceCount) + for (const entry of batch?.entries ?? []) { + console.log('Evidence CID:', entry.cid, 'from:', entry.submitter) } + } - // Step 4: Apply your decision logic - const shouldApprove = await evaluateCase(request); - - if (shouldApprove) { - // Step 5a: Approve and execute the refund - // NOTE: You need the full PaymentInfo struct to call these methods. - // Retrieve it from your application's database or event logs. - const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash); - - const { txHash: approveTx } = await arbiter.approveRefundRequest(paymentInfo, request.nonce); - console.log(`Approved: ${approveTx}`); - - const { txHash: executeTx } = await arbiter.executeRefundInEscrow(paymentInfo); - console.log(`Refund executed: ${executeTx}`); - } else { - // Step 5b: Deny the refund - const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash); + // Step 4: Make a decision + const shouldApprove = await evaluateCase(request) - const { txHash } = await arbiter.denyRefundRequest(paymentInfo, request.nonce); - console.log(`Denied: ${txHash}`); - } + if (shouldApprove) { + const tx = await arbiter.payment.refundInEscrow(paymentInfo, request!.amount) + console.log('Refund executed:', tx) + } else { + const tx = await arbiter.refund?.deny(paymentInfo) + console.log('Denied:', tx) } } ``` -## Decision Flow Diagram +## Decision flow diagram ```mermaid flowchart TD - A[Get Pending Requests] --> B[getRefundRequestByKey] + A[Check refund.has] --> B[refund.get] B --> C{Status Pending?} - C -->|No| D[Skip - Already Decided] - C -->|Yes| E[Evaluate Case] + C -->|No| D[Skip] + C -->|Yes| E[Review evidence] E --> F{Decision} - F -->|Approve| G[approveRefundRequest] - F -->|Deny| H[denyRefundRequest] - G --> I[executeRefundInEscrow] - I --> J[Funds Returned to Payer] + F -->|Approve| G[payment.refundInEscrow] + F -->|Deny| H[refund.deny] + F -->|Decline| I[refund.refuse] + G --> J[Funds Returned to Payer] H --> K[Merchant Keeps Funds] ``` -## Next Steps +## Next steps diff --git a/sdk/arbiter/quickstart.mdx b/sdk/arbiter/quickstart.mdx index dc24b32..9fd1d12 100644 --- a/sdk/arbiter/quickstart.mdx +++ b/sdk/arbiter/quickstart.mdx @@ -1,56 +1,55 @@ --- title: "Arbiter SDK" -description: "Dispute resolution SDK for reviewing cases and making decisions (experimental)" +description: "Dispute resolution SDK for reviewing cases and making decisions" icon: "rocket" --- - -The Arbiter SDK is experimental. The dispute resolution system design is actively evolving. - - -The `@x402r/arbiter` package provides methods for arbiters to resolve disputes: reviewing refund requests, approving or denying them, and executing refunds. +The `@x402r/sdk` package provides arbiter methods for resolving disputes: reviewing refund requests, approving or denying them, executing refunds, reviewing evidence, and distributing fees. ## Setup ```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { getNetworkConfig } from '@x402r/core'; - -const config = getNetworkConfig('eip155:84532')!; - -const arbiter = new X402rArbiter({ - publicClient, - walletClient, +import { createArbiterClient } from '@x402r/sdk' +import { createPublicClient, createWalletClient, http } from 'viem' +import { baseSepolia } from 'viem/chains' +import { privateKeyToAccount } from 'viem/accounts' + +const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) + +const arbiter = createArbiterClient({ + publicClient: createPublicClient({ chain: baseSepolia, transport: http() }), + walletClient: createWalletClient({ + account, + chain: baseSepolia, + transport: http(), + }), operatorAddress: '0x...', - refundRequestAddress: config.refundRequest, - arbiterRegistryAddress: config.arbiterRegistry, -}); + refundRequestAddress: '0x...', + refundRequestEvidenceAddress: '0x...', + escrowPeriodAddress: '0x...', + freezeAddress: '0x...', +}) ``` -## Available Methods - -The Arbiter SDK currently supports: +## Available action groups -- **`approveRefundRequest()`** — Approve a pending refund request -- **`denyRefundRequest()`** — Deny a pending refund request -- **`executeRefundInEscrow()`** — Execute an approved refund to transfer funds back -- **`getPendingRefundRequests()`** — List refund requests awaiting decision -- **`getRefundRequestByKey()`** — Get details of a specific refund request -- **`registerArbiter()`** — Register in the on-chain ArbiterRegistry -- **`isArbiterRegistered()`** — Check registration status -- **`submitEvidence()`** — Attach evidence (IPFS CID) to a refund request -- **`getEvidence()`** — Retrieve a single evidence entry by index -- **`getAllEvidence()`** — Retrieve all evidence for a refund request +The arbiter client provides these action groups: -## Try It Now +- **`payment`** — `release`, `refundInEscrow`, `refundPostEscrow`, `getAmounts`, `getState` and more +- **`refund`** — `get`, `getStatus`, `has`, `deny`, `refuse`, `getOperatorRequests` and more +- **`evidence`** — `submit`, `get`, `getBatch`, `count` +- **`escrow`** — `isDuringEscrow`, `getAuthorizationTime`, `getDuration` +- **`freeze`** — `freeze`, `unfreeze`, `isFrozen` +- **`operator`** — `getConfig`, `calculateFees`, `distributeFees`, `getAccumulatedProtocolFees` and more +- **`watch`** — `onPayment`, `onRefundRequest`, `onRefundExecuted`, `onFeeDistribution` -The easiest way to try arbiter features is with the **arbiter-cli** example, which provides a command-line interface for all arbiter operations: +## Try it now - - CLI tool for arbiters to review cases, make decisions, and manage registry. + + Approve refunds, review evidence, and distribute fees. -## Next Steps +## Next steps diff --git a/sdk/arbiter/registry.mdx b/sdk/arbiter/registry.mdx index 1855c8d..2507f8b 100644 --- a/sdk/arbiter/registry.mdx +++ b/sdk/arbiter/registry.mdx @@ -1,159 +1,146 @@ --- -title: "Arbiter Registry" -description: "Register, update, and discover arbiters using the on-chain ArbiterRegistry" +title: "Identity and discovery" +description: "Register, discover, and rate arbiters using the ERC-8004 plugin" icon: "clipboard-list" --- -The ArbiterRegistry is an on-chain contract that allows arbiters to register themselves, publish metadata URIs, and be discovered by merchants and clients. +The `@x402r/sdk` includes an ERC-8004 plugin that provides on-chain identity registration, reputation scoring, and service endpoint discovery. Arbiters can register themselves and be discovered by merchants and payers. ## Setup -To use registry methods, provide the `arbiterRegistryAddress` when creating the arbiter instance: +Add the `erc8004Actions` plugin to your client using `.extend()`: ```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { getNetworkConfig } from '@x402r/core'; +import { createArbiterClient, erc8004Actions } from '@x402r/sdk' -const config = getNetworkConfig('eip155:84532')!; - -const arbiter = new X402rArbiter({ +const arbiter = createArbiterClient({ publicClient, walletClient, operatorAddress: '0x...', - refundRequestAddress: config.refundRequest, - arbiterRegistryAddress: config.arbiterRegistry, -}); + refundRequestAddress: '0x...', +}) + +const extended = arbiter.extend(erc8004Actions()) ``` -## Register as an Arbiter +This adds three action groups: `identity`, `reputation`, and `discovery`. + +## Identity actions + +### identity.register -Register your address in the on-chain registry with a URI pointing to your metadata or API endpoint: +Register your address in the on-chain identity registry: ```typescript -const { txHash } = await arbiter.registerArbiter( - 'https://arbiter.example.com/api/disputes' -); -console.log('Registered:', txHash); +const tx = await extended.identity.register('my-arbiter-id') +console.log('Registered:', tx) ``` -The URI can point to: -- An API endpoint for receiving dispute notifications -- A JSON metadata file describing your arbitration services -- An IPFS hash with your arbiter profile +### identity.isRegistered -## Update Your URI - -Change your registered URI: +Check if an address is registered: ```typescript -const { txHash } = await arbiter.updateArbiterUri( - 'https://new-arbiter.example.com/api' -); -console.log('URI updated:', txHash); +const registered = await extended.identity.isRegistered('0xArbiterAddress...') +console.log('Is registered:', registered) ``` -## Deregister +### identity.resolveAgent -Remove yourself from the registry: +Look up an agent's registration data: ```typescript -const { txHash } = await arbiter.deregisterArbiter(); -console.log('Deregistered:', txHash); +const agent = await extended.identity.resolveAgent('0xArbiterAddress...') +console.log('Agent:', agent) ``` -## Query the Registry +### identity.verifyAgentId -### Check if an Address is Registered +Verify that an agent ID matches an address: ```typescript -const isRegistered = await arbiter.isArbiterRegistered('0xArbiterAddress...'); -console.log('Is registered:', isRegistered); +const valid = await extended.identity.verifyAgentId('0xArbiterAddress...', 'my-arbiter-id') +console.log('Valid:', valid) ``` -### Get an Arbiter's URI +## Reputation actions + +### reputation.rate + +Submit a rating (0-100) for another address: ```typescript -const uri = await arbiter.getArbiterUri('0xArbiterAddress...'); -console.log('URI:', uri); // empty string if not registered +const tx = await extended.reputation.rate('0xTargetAddress...', 85) +console.log('Rating submitted:', tx) ``` -### Get Total Arbiter Count +### reputation.getSummary + +Get the reputation summary for an address: ```typescript -const count = await arbiter.getArbiterCount(); -console.log('Total arbiters:', count); +const summary = await extended.reputation.getSummary('0xArbiterAddress...') +console.log('Reputation:', summary) ``` -### List Arbiters (Paginated) +### reputation.giveFeedback -```typescript -const { arbiters, uris, total } = await arbiter.listArbiters(0n, 10n); -console.log(`Showing ${arbiters.length} of ${total} arbiters`); +Submit raw feedback with custom tags: -for (let i = 0; i < arbiters.length; i++) { - console.log(`${arbiters[i]}: ${uris[i]}`); -} +```typescript +const tx = await extended.reputation.giveFeedback({ + target: '0xTargetAddress...', + score: 90n, + tag1: 'starred', + tag2: 'x402', +}) +console.log('Feedback submitted:', tx) ``` -## Method Reference +## Discovery actions -| Method | Parameters | Returns | Description | -|--------|-----------|---------|-------------| -| `registerArbiter` | `uri: string` | `{ txHash }` | Register with a URI | -| `updateArbiterUri` | `newUri: string` | `{ txHash }` | Update registered URI | -| `deregisterArbiter` | (none) | `{ txHash }` | Remove from registry | -| `getArbiterUri` | `arbiter: Address` | `string` | Get arbiter's URI | -| `isArbiterRegistered` | `arbiter: Address` | `boolean` | Check registration | -| `getArbiterCount` | (none) | `bigint` | Total registered count | -| `listArbiters` | `offset: bigint, count: bigint` | `ArbiterList` | Paginated list | +### discovery.resolveServiceEndpoint -## ArbiterList Type +Look up a service endpoint for an address: ```typescript -interface ArbiterList { - arbiters: readonly `0x${string}`[]; // Arbiter addresses - uris: readonly string[]; // Corresponding URIs - total: bigint; // Total registered count -} +const endpoint = await extended.discovery.resolveServiceEndpoint('0xArbiterAddress...') +console.log('Service endpoint:', endpoint) ``` -## Complete Example +## Complete example ```typescript -import { X402rArbiter } from '@x402r/arbiter'; -import { getNetworkConfig } from '@x402r/core'; - -async function main() { - const config = getNetworkConfig('eip155:84532')!; - - const arbiter = new X402rArbiter({ - publicClient, - walletClient, - operatorAddress: '0x...', - arbiterRegistryAddress: config.arbiterRegistry, - }); - - // Register - await arbiter.registerArbiter('https://my-arbiter.example.com/api'); - - // Verify - const isRegistered = await arbiter.isArbiterRegistered( - walletClient.account!.address - ); - console.log('Registered:', isRegistered); - - // Browse all arbiters - const { arbiters, uris, total } = await arbiter.listArbiters(0n, 100n); - console.log(`${total} arbiters registered:`); - for (let i = 0; i < arbiters.length; i++) { - console.log(` ${arbiters[i]} → ${uris[i]}`); - } -} - -main().catch(console.error); +import { createArbiterClient, erc8004Actions } from '@x402r/sdk' +import { createPublicClient, createWalletClient, http } from 'viem' +import { baseSepolia } from 'viem/chains' +import { privateKeyToAccount } from 'viem/accounts' + +const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) + +const arbiter = createArbiterClient({ + publicClient: createPublicClient({ chain: baseSepolia, transport: http() }), + walletClient: createWalletClient({ + account, + chain: baseSepolia, + transport: http(), + }), + operatorAddress: '0x...', +}).extend(erc8004Actions()) + +// Register +await arbiter.identity.register('my-dispute-resolver') + +// Verify +const isRegistered = await arbiter.identity.isRegistered(account.address) +console.log('Registered:', isRegistered) + +// Check reputation +const summary = await arbiter.reputation.getSummary(account.address) +console.log('Reputation:', summary) ``` -## Next Steps +## Next steps diff --git a/sdk/arbiter/subscriptions.mdx b/sdk/arbiter/subscriptions.mdx index e2436ae..2fbe99e 100644 --- a/sdk/arbiter/subscriptions.mdx +++ b/sdk/arbiter/subscriptions.mdx @@ -1,120 +1,86 @@ --- -title: "Arbiter Events" +title: "Arbiter events" description: "Subscribe to dispute events and build real-time arbiter dashboards" icon: "bell" --- -The Arbiter SDK provides three subscription methods for monitoring dispute activity in real-time. Each returns an object with an `unsubscribe` function you call to stop watching. +The arbiter client provides real-time event subscriptions through the `watch` action group. Each method returns an unsubscribe function for cleanup. -## Watch New Cases +## watch.onRefundRequest -Subscribe to `RefundRequested` events -- these are new refund requests that need your attention: +Watch for refund request lifecycle events on the RefundRequest contract. This fires for new requests, status updates, and cancellations. ```typescript -import type { RefundRequestEventLog } from '@x402r/core'; - -const { unsubscribe } = arbiter.watchNewCases((event: RefundRequestEventLog) => { - console.log('New refund request!'); - console.log('Payment hash:', event.args.paymentInfoHash); - console.log('Payer:', event.args.payer); - console.log('Receiver:', event.args.receiver); - console.log('Amount:', event.args.amount); - console.log('Nonce:', event.args.nonce); - console.log('Block:', event.blockNumber); -}); +const unwatch = arbiter.watch.onRefundRequest((logs) => { + for (const log of logs) { + console.log('Refund event:', log.eventName) + } +}) // Later: stop watching -unsubscribe(); +unwatch() ``` -## Watch Decisions + +This is a no-op if `refundRequestAddress` was not provided in the client config. + + +## watch.onPayment -Subscribe to `RefundRequestStatusUpdated` events -- these fire when a refund request is approved or denied: +Watch for payment lifecycle events: `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted`. ```typescript -import type { RefundRequestEventLog } from '@x402r/core'; - -const { unsubscribe } = arbiter.watchDecisions((event: RefundRequestEventLog) => { - console.log('Decision made!'); - console.log('Payment hash:', event.args.paymentInfoHash); - console.log('New status:', event.args.status); - console.log('Transaction:', event.transactionHash); -}); +const unwatch = arbiter.watch.onPayment((logs) => { + for (const log of logs) { + console.log('Payment event:', log.eventName) + } +}) + +unwatch() ``` -## Watch Freeze Events +## watch.onRefundExecuted -Subscribe to `PaymentFrozen` and `PaymentUnfrozen` events from a Freeze condition contract: +Watch for refund execution events: `RefundInEscrowExecuted` and `RefundPostEscrowExecuted`. ```typescript -import type { FreezeEventLog } from '@x402r/core'; - -const freezeAddress = '0xFreezeContractAddress...' as `0x${string}`; - -const { unsubscribe } = arbiter.watchFreezeEvents( - freezeAddress, - (event: FreezeEventLog) => { - if (event.eventName === 'PaymentFrozen') { - console.log('Payment frozen:', event.args.paymentInfoHash); - console.log('Frozen by:', event.args.caller); - } else if (event.eventName === 'PaymentUnfrozen') { - console.log('Payment unfrozen:', event.args.paymentInfoHash); - } +const unwatch = arbiter.watch.onRefundExecuted((logs) => { + for (const log of logs) { + console.log('Refund executed:', log.eventName) } -); -``` - -## Event Type Reference +}) -| Method | Contract Event | Fires When | -|--------|---------------|------------| -| `watchNewCases` | `RefundRequested` | A payer submits a new refund request | -| `watchDecisions` | `RefundRequestStatusUpdated` | An arbiter or receiver approves/denies a request | -| `watchFreezeEvents` | `PaymentFrozen` / `PaymentUnfrozen` | A payment is frozen or unfrozen | +unwatch() +``` -## Event Log Types +## watch.onFeeDistribution -Both `watchNewCases` and `watchDecisions` emit `RefundRequestEventLog` events: +Watch for `FeesDistributed` events on the PaymentOperator contract. ```typescript -interface RefundRequestEventLog { - eventName: 'RefundRequested' | 'RefundRequestStatusUpdated' | 'RefundRequestCancelled'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - nonce?: bigint; - status?: number; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} +const unwatch = arbiter.watch.onFeeDistribution((logs) => { + for (const log of logs) { + console.log('Fees distributed:', log) + } +}) + +unwatch() ``` -The `watchFreezeEvents` method emits `FreezeEventLog` events: +## Event types reference -```typescript -interface FreezeEventLog { - eventName: 'PaymentFrozen' | 'PaymentUnfrozen'; - args: { - paymentInfoHash?: `0x${string}`; - caller?: `0x${string}`; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} -``` +| Method | Events Watched | Contract | +|--------|---------------|----------| +| `watch.onRefundRequest` | All RefundRequest ABI events | RefundRequest | +| `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | +| `watch.onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | +| `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | -All subscription methods use viem's `watchContractEvent` under the hood. For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). +For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). -## Next Steps +## Next steps @@ -127,6 +93,6 @@ All subscription methods use viem's `watchContractEvent` under the hood. For rel Process multiple queued cases efficiently. - See how clients subscribe to the same events. + See how payers subscribe to the same events. diff --git a/sdk/client/escrow-management.mdx b/sdk/client/escrow-management.mdx index 5f86fd3..c5da9a7 100644 --- a/sdk/client/escrow-management.mdx +++ b/sdk/client/escrow-management.mdx @@ -1,33 +1,22 @@ --- -title: "Escrow Management" -description: "Manage escrow periods and freeze payments with the Client SDK" +title: "Escrow management" +description: "Manage escrow periods and freeze payments with the payer client" icon: "lock" --- -The Client SDK provides methods to interact with the escrow system, including freezing payments during disputes and querying escrow period timing. All methods documented on this page are **fully functional**. +The payer client provides methods to interact with the escrow system through the `freeze` and `escrow` action groups. These let you freeze payments during disputes and query escrow period timing. -## Freeze Operations +## Freeze operations -Freezing a payment pauses the escrow timer, preventing the merchant from releasing funds while a dispute is being resolved. Freeze operations interact with the `Freeze` contract. +Freezing a payment prevents the merchant from releasing funds while a dispute is being resolved. The `freeze` group requires `freezeAddress` in the client config. -### freezePayment +### freeze.freeze -Freeze a payment to pause the escrow timer. Only the payer can freeze a payment. +Freeze a payment to block the merchant from releasing. Only the payer can freeze a payment. ```typescript -const freezeAddress = '0x...'; // Freeze contract address - -const { txHash } = await client.freezePayment(paymentInfo, freezeAddress); -console.log(`Payment frozen: ${txHash}`); -``` - -#### Signature - -```typescript -freezePayment( - paymentInfo: PaymentInfo, - freezeAddress: `0x${string}` -): Promise<{ txHash: `0x${string}` }> +const tx = await client.freeze?.freeze(paymentInfo) +console.log('Payment frozen:', tx) ``` @@ -37,104 +26,68 @@ Freezing is useful when: - The merchant is unresponsive to your refund request -### unfreezePayment +### freeze.unfreeze Unfreeze a previously frozen payment. The receiver (merchant) or arbiter can unfreeze a payment once the dispute is resolved. ```typescript -const { txHash } = await client.unfreezePayment(paymentInfo, freezeAddress); -console.log(`Payment unfrozen: ${txHash}`); +const tx = await client.freeze?.unfreeze(paymentInfo) +console.log('Payment unfrozen:', tx) ``` -#### Signature - -```typescript -unfreezePayment( - paymentInfo: PaymentInfo, - freezeAddress: `0x${string}` -): Promise<{ txHash: `0x${string}` }> -``` - -### isFrozen +### freeze.isFrozen Check whether a payment is currently frozen. ```typescript -const frozen = await client.isFrozen(paymentInfo, freezeAddress); +const frozen = await client.freeze?.isFrozen(paymentInfo) if (frozen) { - console.log('Payment is frozen - escrow timer paused'); + console.log('Payment is frozen') } else { - console.log('Payment is not frozen - escrow timer running'); + console.log('Payment is not frozen') } ``` -#### Signature +## Escrow period operations -```typescript -isFrozen( - paymentInfo: PaymentInfo, - freezeAddress: `0x${string}` -): Promise -``` - -## Escrow Period Operations - -These methods interact with the `EscrowPeriod` contract to query timing information about a payment's escrow window. +These methods query timing information about a payment's escrow window. The `escrow` group requires `escrowPeriodAddress` in the client config. -### getAuthorizationTime +### escrow.getAuthorizationTime Get the timestamp (in seconds) when a payment was authorized on-chain. This is the starting point of the escrow period. ```typescript -const escrowPeriodAddress = '0x...'; // EscrowPeriod contract address - -const authTime = await client.getAuthorizationTime(paymentInfo, escrowPeriodAddress); -const authDate = new Date(Number(authTime) * 1000); - -console.log(`Payment authorized at: ${authDate.toISOString()}`); -``` +const authTime = await client.escrow?.getAuthorizationTime(paymentInfo) +const authDate = new Date(Number(authTime) * 1000) -#### Signature - -```typescript -getAuthorizationTime( - paymentInfo: PaymentInfo, - escrowPeriodAddress: `0x${string}` -): Promise +console.log('Payment authorized at:', authDate.toISOString()) ``` -### isDuringEscrowPeriod +### escrow.isDuringEscrow -Check whether a payment is still within its escrow period. Returns `true` if the escrow period has not yet passed, meaning the payment can still be refunded. +Check whether a payment is still within its escrow period. Returns `true` if the escrow period is still active (refund window is open). ```typescript -const duringEscrow = await client.isDuringEscrowPeriod( - paymentInfo, - escrowPeriodAddress -); +const duringEscrow = await client.escrow?.isDuringEscrow(paymentInfo) if (duringEscrow) { - console.log('Still in escrow period - refund is possible'); + console.log('Still in escrow period, refund is possible') } else { - console.log('Escrow period has passed - funds can be fully released'); + console.log('Escrow period has passed, funds can be fully released') } ``` -#### Signature +### escrow.getDuration + +Get the configured escrow period duration in seconds. ```typescript -isDuringEscrowPeriod( - paymentInfo: PaymentInfo, - escrowPeriodAddress: `0x${string}` -): Promise +const duration = await client.escrow?.getDuration() +console.log('Escrow period:', duration, 'seconds') ``` - -The method name is `isDuringEscrowPeriod`, **not** `isEscrowPeriodPassed`. It returns `true` when the escrow period is still **active** (refund window is open), and `false` when it has passed. - - -## Understanding Escrow Timing +## Understanding escrow timing | Condition | Escrow Timer | Can Request Refund | Can Release | |-----------|--------------|-------------------|-------------| @@ -143,63 +96,49 @@ The method name is `isDuringEscrowPeriod`, **not** `isEscrowPeriodPassed`. It re | Escrow period passed (unfrozen) | Stopped | No | Full amount | -The escrow period length is configured at the contract level when the `EscrowPeriod` condition is deployed. Common values are 7 days, 14 days, or 30 days. You can calculate the remaining time from `getAuthorizationTime` and the configured period. +The escrow period length is configured at the contract level when the `EscrowPeriod` condition is deployed. Common values are 7 days, 14 days, or 30 days. You can calculate the remaining time from `escrow.getAuthorizationTime()` and `escrow.getDuration()`. -## Example: Freeze and Request Refund +## Example: freeze and request refund A common pattern is to freeze a payment before submitting a refund request, ensuring the merchant cannot release funds while the request is pending. ```typescript -import { X402rClient } from '@x402r/client'; -import type { PaymentInfo } from '@x402r/core'; +import { createPayerClient } from '@x402r/sdk' +import type { PaymentInfo } from '@x402r/sdk' async function freezeAndRequestRefund( - client: X402rClient, + client: ReturnType, paymentInfo: PaymentInfo, - freezeAddress: `0x${string}`, refundAmount: bigint ) { // Step 1: Check if already frozen - const alreadyFrozen = await client.isFrozen(paymentInfo, freezeAddress); + const alreadyFrozen = await client.freeze?.isFrozen(paymentInfo) if (!alreadyFrozen) { - // Freeze the payment first - const { txHash: freezeTx } = await client.freezePayment( - paymentInfo, - freezeAddress - ); - console.log(`Payment frozen: ${freezeTx}`); + const tx = await client.freeze?.freeze(paymentInfo) + console.log('Payment frozen:', tx) } // Step 2: Check if refund request already exists - const nonce = 0n; - const hasRequest = await client.hasRefundRequest(paymentInfo, nonce); + const hasRequest = await client.refund?.has(paymentInfo) if (!hasRequest) { - // Submit the refund request - const { txHash: refundTx } = await client.requestRefund( - paymentInfo, - refundAmount, - nonce - ); - console.log(`Refund requested: ${refundTx}`); + const tx = await client.refund?.request(paymentInfo, refundAmount) + console.log('Refund requested:', tx) } // Step 3: Watch for resolution - const { unsubscribe } = client.watchFreezeEvents( - freezeAddress, - (event) => { - if (event.eventName === 'PaymentUnfrozen') { - console.log('Payment was unfrozen - dispute may be resolved'); - unsubscribe(); - } + const unwatch = client.watch.onRefundRequest((logs) => { + for (const log of logs) { + console.log('Refund event:', log.eventName) } - ); + unwatch() + }) } ``` -## Freeze / Unfreeze Flow +## Freeze / unfreeze flow ```mermaid sequenceDiagram @@ -208,19 +147,19 @@ sequenceDiagram participant M as Merchant / Arbiter Note over F: Escrow timer running - P->>F: freezePayment() + P->>F: freeze.freeze() Note over F: Timer paused alt Dispute resolved favorably - M->>F: unfreezePayment() + M->>F: freeze.unfreeze() Note over F: Timer resumes else Payer cancels dispute - P->>F: unfreezePayment() + P->>F: freeze.unfreeze() Note over F: Timer resumes end ``` -## Next Steps +## Next steps @@ -230,9 +169,9 @@ sequenceDiagram Request refunds while payment is in escrow. - Planned query methods and current workarounds. + Query payment state and amounts. - Full setup guide for the Client SDK. + Full setup guide for the payer client. diff --git a/sdk/client/payment-queries.mdx b/sdk/client/payment-queries.mdx index 38d3103..f79975d 100644 --- a/sdk/client/payment-queries.mdx +++ b/sdk/client/payment-queries.mdx @@ -1,108 +1,96 @@ --- -title: "Payment Queries" -description: "Query payment states and details with the Client SDK" +title: "Payment queries" +description: "Query payment states, amounts, and history with the payer client" icon: "magnifying-glass" --- -The Client SDK provides five methods for querying payment state and history. These read directly from the escrow contract and on-chain event logs. +The payer client provides methods for querying payment state and history across the `payment`, `query`, and `operator` action groups. -## getPaymentState +## payment.getState -Derive the lifecycle state of a payment from the escrow contract (amounts and expiry). +Derive the lifecycle state of a payment from the operator contract. ```typescript -import { PaymentState } from '@x402r/core'; +const state = await client.payment.getState(paymentInfo) -const state = await client.getPaymentState(paymentInfo); - -// PaymentState enum: +// Returns a PaymentState number: // 0 = NonExistent - Payment has never been authorized // 1 = InEscrow - Funds locked, capturableAmount > 0 -// 2 = Released - Funds released to receiver, may still be refundable -// 3 = Settled - No funds in escrow or refundable -// 4 = Expired - Authorization expired, payer can reclaim -``` - -```typescript -getPaymentState(paymentInfo: PaymentInfo): Promise +// 2 = Released - Funds released to receiver +// 3 = Settled - Payment fully settled +// 4 = Expired - Authorization expired ``` -## paymentExists +## payment.getAmounts -Check whether a payment has been collected by reading the escrow's `hasCollectedPayment` flag. +Query the current capturable and refundable amounts for a payment. ```typescript -const exists = await client.paymentExists(paymentInfoHash); -if (exists) { - console.log('Payment found'); -} -``` +const amounts = await client.payment.getAmounts(paymentInfo) -```typescript -paymentExists(paymentInfoHash: `0x${string}`): Promise +console.log('Has collected:', amounts.hasCollectedPayment) +console.log('Capturable:', amounts.capturableAmount) +console.log('Refundable:', amounts.refundableAmount) ``` -## isInEscrow +## query.getPayerPayments -Check if a payment currently has capturable funds in escrow. +List all payments where a given address is the payer. Requires `paymentIndexRecorderAddress` in the client config. ```typescript -const inEscrow = await client.isInEscrow(paymentInfoHash); -if (inEscrow) { - console.log('Payment has funds in escrow'); +const payments = await client.query?.getPayerPayments(payerAddress) + +for (const payment of payments ?? []) { + console.log('Payment info:', payment) } ``` -```typescript -isInEscrow(paymentInfoHash: `0x${string}`): Promise -``` + +The `query` group uses a tiered resolver: it checks the in-memory store first, then the on-chain recorder, then falls back to event log scanning. Pass `eventFromBlock` in the client config to limit the scan range. + -## getPaymentDetails +## query.getReceiverPayments -Retrieve the full `PaymentInfo` struct by scanning `AuthorizationCreated` events for the given hash. +List all payments where a given address is the receiver. ```typescript -const details = await client.getPaymentDetails(paymentInfoHash); - -console.log('Payer:', details.payer); -console.log('Receiver:', details.receiver); -console.log('Amount:', details.maxAmount); +const payments = await client.query?.getReceiverPayments(receiverAddress) ``` +## query.getPayment + +Look up a single payment by its hash. + ```typescript -getPaymentDetails( - paymentInfoHash: `0x${string}`, - fromBlock?: bigint -): Promise +const payment = await client.query?.getPayment(paymentInfoHash) ``` - -This method scans event logs. Pass `fromBlock` to limit the scan range if your RPC limits `eth_getLogs` responses (Base Sepolia typically caps at 10,000 blocks). - - -## getPayerPayments +## operator.getConfig -List all payments where the connected wallet is the payer, by scanning `AuthorizationCreated` events. +Retrieve all slot addresses from the PaymentOperator contract. ```typescript -const payments = await client.getPayerPayments(); +const config = await client.operator.getConfig() -for (const { hash, paymentInfo } of payments) { - console.log(`Payment ${hash}: ${paymentInfo.maxAmount}`); -} +console.log('Release condition:', config.releaseCondition) +console.log('Fee calculator:', config.feeCalculator) +console.log('Fee recipient:', config.feeRecipient) ``` +## operator.calculateFees + +Calculate the fee breakdown for a given payment amount. + ```typescript -getPayerPayments( - fromBlock?: bigint -): Promise> -``` +const fees = await client.operator.calculateFees(paymentInfo, 1_000_000n) - -Like `getPaymentDetails`, this scans event logs. Pass `fromBlock` to limit the range for large histories. - +console.log('Operator fee:', fees.operatorFee) +console.log('Protocol fee:', fees.protocolFee) +console.log('Total fee:', fees.totalFee) +console.log('Net amount:', fees.netAmount) +``` -## Next Steps +## Next steps @@ -112,7 +100,7 @@ Like `getPaymentDetails`, this scans event logs. Pass `fromBlock` to limit the r Freeze payments and query escrow periods. - Full setup guide for the Client SDK. + Full setup guide for the payer client. Known constraints including event log scanning limits. diff --git a/sdk/client/quickstart.mdx b/sdk/client/quickstart.mdx index f451a5f..038e55c 100644 --- a/sdk/client/quickstart.mdx +++ b/sdk/client/quickstart.mdx @@ -1,60 +1,60 @@ --- title: "Client SDK" -description: "Payer-side SDK for refunds, freezes, and escrow management (experimental)" +description: "Payer-side SDK for refunds, freezes, and escrow management" icon: "rocket" --- - -The Client SDK is experimental. APIs will change as the refund and dispute system design evolves. - - -The `@x402r/client` package provides payer-side methods for interacting with X402r payments: requesting refunds, freezing payments, and querying escrow state. +The `@x402r/sdk` package provides payer-side methods for interacting with x402r payments: requesting refunds, freezing payments, submitting evidence, and querying escrow state. ## Setup ```typescript -import { X402rClient } from '@x402r/client'; -import { getNetworkConfig } from '@x402r/core'; +import { createPayerClient } from '@x402r/sdk' +import { createPublicClient, createWalletClient, http } from 'viem' +import { baseSepolia } from 'viem/chains' +import { privateKeyToAccount } from 'viem/accounts' -const networkConfig = getNetworkConfig('eip155:84532')!; +const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) -const client = new X402rClient({ - publicClient, - walletClient, - operatorAddress: '0x...', // Your PaymentOperator address - refundRequestAddress: networkConfig.refundRequest, - escrowAddress: networkConfig.authCaptureEscrow, -}); +const client = createPayerClient({ + publicClient: createPublicClient({ chain: baseSepolia, transport: http() }), + walletClient: createWalletClient({ + account, + chain: baseSepolia, + transport: http(), + }), + operatorAddress: '0x...', + refundRequestAddress: '0x...', + refundRequestEvidenceAddress: '0x...', + escrowPeriodAddress: '0x...', + freezeAddress: '0x...', +}) ``` -## Available Methods +## Available action groups + +The payer client provides these action groups: -The Client SDK currently supports: +- **`payment`** — `getAmounts`, `getState`, `authorize`, `refundInEscrow` and more +- **`escrow`** — `isDuringEscrow`, `getAuthorizationTime`, `getDuration` +- **`refund`** — `request`, `cancel`, `get`, `getStatus`, `has`, `getByKey`, `getPayerRequests` and more +- **`evidence`** — `submit`, `get`, `getBatch`, `count` +- **`freeze`** — `freeze`, `unfreeze`, `isFrozen` +- **`watch`** — `onPayment`, `onRefundRequest`, `onRefundExecuted`, `onFeeDistribution` +- **`operator`** — `getConfig`, `getFeeAddresses`, `calculateFees` and more +- **`query`** — `getPayerPayments`, `getReceiverPayments`, `getPayment` (requires `paymentIndexRecorderAddress`) -- **`requestRefund()`** — Submit a refund request for a payment in escrow -- **`getRefundStatus()`** — Check the status of a refund request -- **`freezePayment()`** — Freeze a payment to prevent release during a dispute -- **`isFrozen()`** — Check if a payment is frozen -- **`isDuringEscrowPeriod()`** — Check if a payment is still in its escrow window -- **`getAuthorizationTime()`** — Get when a payment was authorized -- **`getPaymentState()`** — Derive the lifecycle state of a payment -- **`paymentExists()`** — Check if a payment has been authorized -- **`isInEscrow()`** — Check if a payment has capturable funds -- **`getPaymentDetails()`** — Retrieve full PaymentInfo from event logs -- **`getPayerPayments()`** — List all payments for the connected wallet -- **`submitEvidence()`** — Attach evidence (IPFS CID) to a refund request -- **`getEvidence()`** — Retrieve a single evidence entry by index -- **`getAllEvidence()`** — Retrieve all evidence for a refund request +Optional groups (`escrow`, `refund`, `evidence`, `freeze`, `query`) are `undefined` if you do not pass the corresponding contract address. Use optional chaining when calling them. -## Try It Now +## Try it now -The easiest way to try client features is with the **client-cli** example, which provides a command-line interface for all client operations: +The easiest way to try payer features is with the runnable examples in the SDK repo: - - CLI tool for payers to `pay`, `preview-fee`, request refunds, freeze payments, and check status. + + Request refunds, submit evidence, and freeze payments. -## Next Steps +## Next steps diff --git a/sdk/client/refund-operations.mdx b/sdk/client/refund-operations.mdx index 7acd7f3..8ad8630 100644 --- a/sdk/client/refund-operations.mdx +++ b/sdk/client/refund-operations.mdx @@ -1,39 +1,34 @@ --- -title: "Refund Operations" -description: "Request and manage refunds with the Client SDK - submit, cancel, and track refund requests" +title: "Refund operations" +description: "Request and manage refunds with the payer client" icon: "rotate-left" --- -The Client SDK provides complete refund management capabilities for payers. All refund methods interact directly with the `RefundRequest` contract on-chain. +The payer client provides complete refund management through the `refund` action group. All methods interact directly with the `RefundRequest` contract on-chain. -**About the `nonce` parameter:** Every refund method requires a `nonce: bigint` parameter. This is the record index from the `PaymentIndexRecorder` and identifies which charge within a payment you are requesting a refund for. For the first (and most common) charge, use `0n`. +The `refund` group requires `refundRequestAddress` in the client config. Without it, `client.refund` is `undefined`. -## requestRefund +## refund.request Submit a refund request for a payment that is in escrow. The request goes on-chain and is visible to the merchant and any assigned arbiter. ```typescript -const { txHash } = await client.requestRefund( - paymentInfo, - BigInt('1000000'), // amount to refund (e.g., 1 USDC with 6 decimals) - 0n // nonce: first charge -); - -console.log(`Refund requested: ${txHash}`); +const tx = await client.refund?.request(paymentInfo, 1_000_000n) // 1 USDC +console.log('Refund requested:', tx) ``` -## cancelRefundRequest +## refund.cancel Cancel a pending refund request that you submitted. Only the original requester (payer) can cancel, and only while the request status is `Pending`. ```typescript -const { txHash } = await client.cancelRefundRequest(paymentInfo, 0n); -console.log(`Refund request cancelled: ${txHash}`); +const tx = await client.refund?.cancel(paymentInfo) +console.log('Refund request cancelled:', tx) ``` -## Query Refund State +## Query refund state These methods read on-chain state for refund requests. None require a wallet client. @@ -41,66 +36,91 @@ These methods read on-chain state for refund requests. None require a wallet cli ```typescript // Check if a refund request exists -const hasRequest = await client.hasRefundRequest(paymentInfo, 0n); +const hasRequest = await client.refund?.has(paymentInfo) // Get just the status -const status = await client.getRefundStatus(paymentInfo, 0n); -// Returns: RequestStatus.Pending | Approved | Denied | Cancelled +const status = await client.refund?.getStatus(paymentInfo) +// Returns: 0 = Pending, 1 = Approved, 2 = Denied, 3 = Cancelled, 4 = Refused ``` ### Get full refund request data ```typescript -// By paymentInfo + nonce -const request = await client.getRefundRequest(paymentInfo, 0n); -console.log(request.amount, request.status); +// By paymentInfo +const request = await client.refund?.get(paymentInfo) +console.log(request?.amount, request?.status) -// By composite key (from getMyRefundRequests) -const request2 = await client.getRefundRequestByKey(compositeKey); +// By hash (from list methods) +const request2 = await client.refund?.getByKey(paymentInfoHash) ``` -### List your refund requests +### List refund requests ```typescript -// Get total count -const count = await client.getMyRefundRequestCount(); +// Get payer's refund requests (paginated) +const payerRequests = await client.refund?.getPayerRequests( + payerAddress, + 0n, // offset + 10n, // count +) + +// Get receiver's refund requests +const receiverRequests = await client.refund?.getReceiverRequests( + receiverAddress, + 0n, + 10n, +) + +// Get all requests for an operator +const operatorRequests = await client.refund?.getOperatorRequests( + operatorAddress, + 0n, + 10n, +) +``` -// Get paginated keys, then fetch details -const { keys, total } = await client.getMyRefundRequests(0n, 10n); +### Track cancellations -for (const key of keys) { - const request = await client.getRefundRequestByKey(key); - console.log(`Amount: ${request.amount}, Status: ${request.status}`); -} +```typescript +const cancelCount = await client.refund?.getCancelCount(paymentInfo) +const cancelledAmount = await client.refund?.getCancelledAmount(paymentInfo, 0n) ``` -## Refund Request Lifecycle +## Refund request lifecycle ```mermaid stateDiagram-v2 - [*] --> Pending: requestRefund() + [*] --> Pending: refund.request() Pending --> Approved: Merchant/Arbiter approves Pending --> Denied: Merchant/Arbiter denies - Pending --> Cancelled: cancelRefundRequest() + Pending --> Refused: Arbiter declines to rule + Pending --> Cancelled: refund.cancel() Approved --> [*]: Funds returned Denied --> [*] + Refused --> [*] Cancelled --> [*] ``` -## Method Reference +## Method reference | Method | Parameters | Returns | |--------|-----------|---------| -| `requestRefund` | `paymentInfo, amount, nonce` | `{ txHash }` | -| `cancelRefundRequest` | `paymentInfo, nonce` | `{ txHash }` | -| `hasRefundRequest` | `paymentInfo, nonce` | `boolean` | -| `getRefundStatus` | `paymentInfo, nonce` | `RequestStatus` | -| `getRefundRequest` | `paymentInfo, nonce` | `RefundRequestData` | -| `getRefundRequestByKey` | `compositeKey` | `RefundRequestData` | -| `getMyRefundRequests` | `offset, count` | `{ keys, total }` | -| `getMyRefundRequestCount` | none | `bigint` | - -## Next Steps +| `refund.request` | `paymentInfo, amount` | `Hash` | +| `refund.cancel` | `paymentInfo` | `Hash` | +| `refund.deny` | `paymentInfo` | `Hash` | +| `refund.refuse` | `paymentInfo` | `Hash` | +| `refund.has` | `paymentInfo` | `boolean` | +| `refund.getStatus` | `paymentInfo` | `RefundRequestStatus` | +| `refund.get` | `paymentInfo` | `RefundRequestData` | +| `refund.getByKey` | `paymentInfoHash` | `RefundRequestData` | +| `refund.getStoredPaymentInfo` | `paymentInfoHash` | `PaymentInfo` | +| `refund.getPayerRequests` | `payer, offset, count` | `RefundRequestData[]` | +| `refund.getReceiverRequests` | `receiver, offset, count` | `RefundRequestData[]` | +| `refund.getOperatorRequests` | `operator, offset, count` | `RefundRequestData[]` | +| `refund.getCancelCount` | `paymentInfo` | `bigint` | +| `refund.getCancelledAmount` | `paymentInfo, cancelIndex` | `bigint` | + +## Next steps @@ -113,6 +133,6 @@ stateDiagram-v2 Query payment state and details. - Full setup guide for the Client SDK. + Full setup guide for the payer client. diff --git a/sdk/client/subscriptions.mdx b/sdk/client/subscriptions.mdx index cac10ce..b32878b 100644 --- a/sdk/client/subscriptions.mdx +++ b/sdk/client/subscriptions.mdx @@ -1,225 +1,97 @@ --- -title: "Client Events" -description: "Subscribe to real-time payment, refund, and freeze events as a payer" +title: "Client events" +description: "Subscribe to real-time payment, refund, and fee events" icon: "bell" --- -The Client SDK provides methods to subscribe to blockchain events in real-time using viem's `watchContractEvent` under the hood. All subscription methods return an object with an `unsubscribe` function that you should call when you no longer need the watcher. +The `watch` action group provides real-time event subscriptions using viem's `watchContractEvent` under the hood. Each method returns an unsubscribe function you should call when you no longer need the watcher. -## watchPaymentState +The `watch` group is always available on the client (no optional address required). -Watch for state changes on a specific payment. This subscribes to `ReleaseExecuted`, `RefundInEscrowExecuted`, and `RefundPostEscrowExecuted` events on the `PaymentOperator` contract. +## watch.onPayment -```typescript -const { unsubscribe } = client.watchPaymentState( - paymentInfoHash, - (event) => { - console.log(`Payment event: ${event.eventName}`); +Watch for payment lifecycle events: `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted` on the PaymentOperator contract. - switch (event.eventName) { +```typescript +const unwatch = client.watch.onPayment((logs) => { + for (const log of logs) { + console.log('Payment event:', log.eventName) + + switch (log.eventName) { + case 'AuthorizationCreated': + console.log('New payment authorized') + break + case 'ChargeExecuted': + console.log('Payment charged') + break case 'ReleaseExecuted': - console.log('Funds released to merchant'); - console.log('Amount:', event.args.amount); - break; - case 'RefundInEscrowExecuted': - console.log('Funds refunded from escrow'); - break; - case 'RefundPostEscrowExecuted': - console.log('Funds refunded after escrow period'); - break; + console.log('Funds released to merchant') + break } } -); +}) // Stop watching when done -unsubscribe(); -``` - -### Signature - -```typescript -watchPaymentState( - paymentInfoHash: `0x${string}`, - callback: (event: PaymentOperatorEventLog) => void -): { unsubscribe: () => void } -``` - -### PaymentOperatorEventLog Type - -```typescript -interface PaymentOperatorEventLog { - eventName: - | 'ReleaseExecuted' - | 'RefundInEscrowExecuted' - | 'RefundPostEscrowExecuted' - | 'AuthorizationCreated' - | 'ChargeExecuted'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} +unwatch() ``` -## watchRefundRequests +## watch.onRefundRequest -Watch for refund request lifecycle events. This subscribes to `RefundRequested`, `RefundRequestStatusUpdated`, and `RefundRequestCancelled` events on the `RefundRequest` contract. +Watch for refund request lifecycle events on the RefundRequest contract. This is a no-op if `refundRequestAddress` was not provided in the client config. ```typescript -const { unsubscribe } = client.watchRefundRequests((event) => { - switch (event.eventName) { - case 'RefundRequested': - console.log('New refund request submitted'); - console.log('Payment:', event.args.paymentInfoHash); - console.log('Amount:', event.args.amount); - break; - case 'RefundRequestStatusUpdated': - console.log('Refund status changed:', event.args.status); - // 1 = Approved, 2 = Denied - break; - case 'RefundRequestCancelled': - console.log('Refund request cancelled'); - break; +const unwatch = client.watch.onRefundRequest((logs) => { + for (const log of logs) { + console.log('Refund event:', log.eventName) } -}); +}) // Stop watching when done -unsubscribe(); -``` - -### Signature - -```typescript -watchRefundRequests( - callback: (event: RefundRequestEventLog) => void -): { unsubscribe: () => void } -``` - - -Requires `refundRequestAddress` to be configured on the client. Throws an error if not set. - - -### RefundRequestEventLog Type - -```typescript -interface RefundRequestEventLog { - eventName: - | 'RefundRequested' - | 'RefundRequestStatusUpdated' - | 'RefundRequestCancelled'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - nonce?: bigint; - status?: number; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} +unwatch() ``` -## watchMyPayments +## watch.onRefundExecuted -Watch for new payment authorizations where the connected wallet is the payer. This subscribes to `AuthorizationCreated` events on the `PaymentOperator` contract, filtered by the wallet's address. +Watch for refund execution events: `RefundInEscrowExecuted` and `RefundPostEscrowExecuted` on the PaymentOperator contract. ```typescript -const { unsubscribe } = client.watchMyPayments((event) => { - console.log('New payment authorized!'); - console.log('Event:', event.eventName); // 'AuthorizationCreated' - console.log('Hash:', event.args.paymentInfoHash); - console.log('Receiver:', event.args.receiver); - console.log('Amount:', event.args.amount); -}); - -// Stop watching when done -unsubscribe(); -``` - -### Signature +const unwatch = client.watch.onRefundExecuted((logs) => { + for (const log of logs) { + console.log('Refund executed:', log.eventName) + } +}) -```typescript -watchMyPayments( - callback: (event: PaymentOperatorEventLog) => void -): { unsubscribe: () => void } +unwatch() ``` - -Requires a `walletClient` with an account to be configured, since the events are filtered by the payer address. - +## watch.onFeeDistribution -## watchFreezeEvents - -Watch for freeze and unfreeze events on a specific `Freeze` contract. This subscribes to `PaymentFrozen` and `PaymentUnfrozen` events. +Watch for `FeesDistributed` events on the PaymentOperator contract. ```typescript -const freezeAddress = '0x...'; // Freeze contract address - -const { unsubscribe } = client.watchFreezeEvents( - freezeAddress, - (event) => { - if (event.eventName === 'PaymentFrozen') { - console.log('Payment frozen:', event.args.paymentInfoHash); - console.log('Frozen by:', event.args.caller); - } else if (event.eventName === 'PaymentUnfrozen') { - console.log('Payment unfrozen:', event.args.paymentInfoHash); - console.log('Unfrozen by:', event.args.caller); - } +const unwatch = client.watch.onFeeDistribution((logs) => { + for (const log of logs) { + console.log('Fees distributed:', log) } -); +}) -// Stop watching when done -unsubscribe(); -``` - -### Signature - -```typescript -watchFreezeEvents( - freezeAddress: `0x${string}`, - callback: (event: FreezeEventLog) => void -): { unsubscribe: () => void } -``` - -### FreezeEventLog Type - -```typescript -interface FreezeEventLog { - eventName: 'PaymentFrozen' | 'PaymentUnfrozen'; - args: { - paymentInfoHash?: `0x${string}`; - caller?: `0x${string}`; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} +unwatch() ``` -## Event Types Reference +## Event types reference -| Method | Events Watched | Contract | Use Case | -|--------|---------------|----------|----------| -| `watchPaymentState` | `ReleaseExecuted`, `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | Track a single payment's lifecycle | -| `watchRefundRequests` | `RefundRequested`, `RefundRequestStatusUpdated`, `RefundRequestCancelled` | RefundRequest | Monitor refund request workflow | -| `watchMyPayments` | `AuthorizationCreated` (filtered by payer) | PaymentOperator | Track new payments for your wallet | -| `watchFreezeEvents` | `PaymentFrozen`, `PaymentUnfrozen` | Freeze | Monitor dispute freeze activity | +| Method | Events Watched | Contract | +|--------|---------------|----------| +| `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | +| `watch.onRefundRequest` | All RefundRequest ABI events | RefundRequest | +| `watch.onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | +| `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | -All subscription methods use viem's `watchContractEvent` under the hood. For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). +For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). -## Next Steps +## Next steps @@ -231,7 +103,7 @@ All subscription methods use viem's `watchContractEvent` under the hood. For rel Freeze payments and check escrow timing. - - Learn about the merchant side of X402r. + + Learn about the merchant side of x402r. diff --git a/sdk/concepts.mdx b/sdk/concepts.mdx index 3b66c61..fe681e7 100644 --- a/sdk/concepts.mdx +++ b/sdk/concepts.mdx @@ -52,7 +52,7 @@ interface PaymentInfo { ``` -Use `computePaymentInfoHash()` from `@x402r/core` to compute the unique hash of a payment. Use `parsePaymentInfo()` to deserialize a JSON PaymentInfo back into the typed struct. +Use `computePaymentInfoHash()` from `@x402r/sdk` to compute the unique hash of a payment. ## Escrow Period @@ -64,15 +64,22 @@ The **EscrowPeriod** contract tracks when a payment was authorized and enforces - **After the period** — merchants can release funds to themselves ```typescript -import { X402rClient } from '@x402r/client'; +import { createPayerClient } from '@x402r/sdk' + +const client = createPayerClient({ + publicClient, + walletClient, + operatorAddress: '0x...', + escrowPeriodAddress: '0x...', +}) // Check when payment was authorized -const authTime = await client.getAuthorizationTime(paymentInfo, escrowPeriodAddress); +const authTime = await client.escrow?.getAuthorizationTime(paymentInfo) // Check if still within escrow period -const inEscrow = await client.isDuringEscrowPeriod(paymentInfo, escrowPeriodAddress); +const inEscrow = await client.escrow?.isDuringEscrow(paymentInfo) if (!inEscrow) { - console.log('Escrow period has passed - funds can be released'); + console.log('Escrow period has passed, funds can be released') } ``` @@ -81,13 +88,14 @@ if (!inEscrow) { When a payer wants a refund, they create a refund request that goes through approval: ```typescript -import { RequestStatus } from '@x402r/core'; - -// RequestStatus values: -RequestStatus.Pending // 0 - Awaiting decision -RequestStatus.Approved // 1 - Approved by merchant/arbiter -RequestStatus.Denied // 2 - Denied by merchant/arbiter -RequestStatus.Cancelled // 3 - Cancelled by payer +import { RefundRequestStatus } from '@x402r/sdk' + +// RefundRequestStatus values: +RefundRequestStatus.Pending // 0 - Awaiting decision +RefundRequestStatus.Approved // 1 - Approved by merchant/arbiter +RefundRequestStatus.Denied // 2 - Denied by merchant/arbiter +RefundRequestStatus.Cancelled // 3 - Cancelled by payer +RefundRequestStatus.Refused // 4 - Arbiter declined to rule ``` ### Refund Flow @@ -131,13 +139,13 @@ The **Freeze** contract allows payers to freeze a payment during the escrow peri ```typescript // Payer freezes payment (requires payer authorization) -await client.freezePayment(paymentInfo, freezeAddress); +await client.freeze?.freeze(paymentInfo) // Merchant unfreezes payment (requires receiver authorization) -await merchant.unfreezePayment(paymentInfo, freezeAddress); +await merchant.freeze?.unfreeze(paymentInfo) // Check frozen status -const frozen = await client.isFrozen(paymentInfo, freezeAddress); +const frozen = await client.freeze?.isFrozen(paymentInfo) ``` diff --git a/sdk/create-client.mdx b/sdk/create-client.mdx index 5df2a78..36c1f50 100644 --- a/sdk/create-client.mdx +++ b/sdk/create-client.mdx @@ -52,6 +52,7 @@ Type narrowing is a DX convenience, not a security boundary. On-chain [condition | `walletClient` | `WalletClient` | No | Required for writes. Role presets throw without it. | | `operatorAddress` | `Address` | Yes | Your deployed PaymentOperator | | `chainId` | `number` | No | Auto-detected from `publicClient.chain` | +| `network` | `string` | No | EIP-155 network ID (e.g., `'eip155:84532'`). Alternative to `chainId`. | | `escrowPeriodAddress` | `Address` | No | Activates `escrow` group | | `refundRequestAddress` | `Address` | No | Activates `refund` group | | `refundRequestEvidenceAddress` | `Address` | No | Activates `evidence` group (requires `refundRequestAddress`) | @@ -68,7 +69,7 @@ Type narrowing is a DX convenience, not a security boundary. On-chain [condition | `operator` | 8 | Always available | | `watch` | 4 | Always available | | `escrow` | 3 | `escrowPeriodAddress` | -| `refund` | 15 | `refundRequestAddress` | +| `refund` | 14 | `refundRequestAddress` | | `evidence` | 4 | `refundRequestEvidenceAddress` | | `freeze` | 3 | `freezeAddress` | | `query` | 3 | `paymentIndexRecorderAddress` | diff --git a/sdk/limitations.mdx b/sdk/limitations.mdx index 7e3a23c..66b272d 100644 --- a/sdk/limitations.mdx +++ b/sdk/limitations.mdx @@ -8,16 +8,18 @@ The SDK provides full coverage of core payment flows including authorization, re ## API Constraints -### EIP-155 Network Identifiers +### Chain configuration -Network configuration requires EIP-155 format strings, not chain ID numbers: +Use `getChainConfig()` from `@x402r/sdk` with a numeric chain ID: ```typescript -// Correct -const config = getNetworkConfig('eip155:84532'); +import { getChainConfig } from '@x402r/sdk' -// Incorrect - will return undefined -const config = getNetworkConfig(84532); +// Correct - numeric chain ID +const config = getChainConfig(84532) + +// Incorrect - EIP-155 strings are not accepted +// const config = getChainConfig('eip155:84532') ``` ### PaymentInfo Must Be Complete @@ -26,10 +28,10 @@ All SDK methods require a complete `PaymentInfo` object. You cannot query by has ```typescript // Works - full PaymentInfo -const status = await client.getRefundStatus(paymentInfo, 0n); +const status = await client.refund?.getStatus(paymentInfo) -// Not supported - hash-only queries require the full struct -// const state = await client.getPaymentStateByHash(hash); +// Some methods accept a hash directly +const request = await client.refund?.getByKey(paymentInfoHash) ``` ### Event Log Scanning Limits @@ -37,9 +39,16 @@ const status = await client.getRefundStatus(paymentInfo, 0n); `getPayerPayments()`, `getReceiverPayments()`, and `getPaymentDetails()` scan `AuthorizationCreated` events using `eth_getLogs`. Base Sepolia RPCs typically limit responses to 10,000 blocks. Pass a `fromBlock` parameter for large ranges: ```typescript -// Scan only recent blocks to avoid RPC limits -const payments = await client.getPayerPayments(recentBlockNumber); -const details = await client.getPaymentDetails(hash, recentBlockNumber); +// Pass eventFromBlock when creating the client to limit scan range +const client = createPayerClient({ + publicClient, + walletClient, + operatorAddress: '0x...', + paymentIndexRecorderAddress: '0x...', + eventFromBlock: recentBlockNumber, +}) + +const payments = await client.query?.getPayerPayments(payerAddress) ``` ### No Express/Hono Middleware diff --git a/sdk/merchant/payment-operations.mdx b/sdk/merchant/payment-operations.mdx index e5bdd20..996614c 100644 --- a/sdk/merchant/payment-operations.mdx +++ b/sdk/merchant/payment-operations.mdx @@ -1,256 +1,184 @@ --- -title: "Payment Operations" -description: "Release funds, charge payments, process refunds, and query escrow state with the Merchant SDK" +title: "Payment operations" +description: "Release funds, charge payments, process refunds, and query escrow state" icon: "coins" --- -The `X402rMerchant` class provides methods for managing the full payment lifecycle: releasing escrowed funds, charging directly for subscriptions, processing refunds, and querying operator configuration. +The merchant client provides methods for managing the full payment lifecycle through the `payment` and `operator` action groups: releasing escrowed funds, charging directly for subscriptions, processing refunds, and querying operator configuration. -## Payment Operations +## Payment operations -### Release Funds from Escrow +### payment.release -Use `release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required** and specifies the exact amount to release in token units. +Transfer escrowed funds to the receiver (merchant). The `amount` parameter is required. ```typescript -import { X402rMerchant } from '@x402r/merchant'; - // Release 10 USDC (6 decimals) from escrow -const { txHash } = await merchant.release(paymentInfo, BigInt('10000000')); -console.log('Released:', txHash); +const tx = await merchant.payment.release(paymentInfo, 10_000_000n) +console.log('Released:', tx) ``` -For partial releases, specify a smaller amount. The remaining funds stay in escrow and can be released or refunded later. +For partial releases, specify a smaller amount. The remaining funds stay in escrow. ```typescript // Release 3 USDC of a 10 USDC escrow -const { txHash } = await merchant.release(paymentInfo, BigInt('3000000')); -console.log('Partial release:', txHash); +const tx = await merchant.payment.release(paymentInfo, 3_000_000n) // Check what remains -const { capturableAmount } = await merchant.getPaymentAmounts(paymentInfo); -console.log('Remaining in escrow:', capturableAmount); // 7000000n +const amounts = await merchant.payment.getAmounts(paymentInfo) +console.log('Remaining in escrow:', amounts.capturableAmount) // 7000000n ``` -The `amount` parameter is always required. There is no default "release all" behavior. Always query `getPaymentAmounts()` first to determine the available capturable amount. +Always query `payment.getAmounts()` first to determine the available capturable amount. -### Refund While in Escrow +### payment.refundInEscrow -Use `refundInEscrow()` to return escrowed funds to the payer before release. The `amount` parameter is **required**. +Return escrowed funds to the payer before release. ```typescript // Full refund of 10 USDC -const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('10000000')); -console.log('Refunded from escrow:', txHash); +const tx = await merchant.payment.refundInEscrow(paymentInfo, 10_000_000n) +console.log('Refunded from escrow:', tx) ``` ```typescript // Partial refund: return 2 USDC, keep 8 USDC in escrow -const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('2000000')); -console.log('Partial refund:', txHash); +const tx = await merchant.payment.refundInEscrow(paymentInfo, 2_000_000n) ``` -### Charge Directly +### payment.charge -Use `charge()` for non-escrow flows such as subscriptions or session-based payments. This pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`). +For non-escrow flows such as subscriptions or session-based payments. Pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`). ```typescript -const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...'; -const collectorData: `0x${string}` = '0xSignatureOrCalldata...'; - -const { txHash } = await merchant.charge( +const tx = await merchant.payment.charge( paymentInfo, - BigInt('5000000'), // 5 USDC - tokenCollectorAddress, // token collector contract - collectorData // authorization data (e.g., ERC-3009 signature) -); -console.log('Charged:', txHash); + 5_000_000n, // 5 USDC + '0xTokenCollector...' as `0x${string}`, // token collector contract + '0xSignatureData...' as `0x${string}`, // authorization data +) +console.log('Charged:', tx) ``` -The `charge()` method is designed for recurring payments and session-based billing where funds are not pre-escrowed. The token collector contract handles the actual token transfer. +The `charge()` method is designed for recurring payments and session-based billing where funds are not pre-escrowed. -### Refund After Release (Post-Escrow) +### payment.refundPostEscrow -Use `refundPostEscrow()` to refund funds that have already been released to the receiver. This requires a token collector to source the refund from the merchant's balance. +Refund funds that have already been released. Requires a token collector to source the refund from the merchant's balance. ```typescript -const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...'; -const collectorData: `0x${string}` = '0xSignatureOrCalldata...'; - -const { txHash } = await merchant.refundPostEscrow( +const tx = await merchant.payment.refundPostEscrow( paymentInfo, - BigInt('5000000'), // 5 USDC to refund - tokenCollectorAddress, // token collector that sources the refund - collectorData // authorization data -); -console.log('Post-escrow refund:', txHash); + 5_000_000n, // 5 USDC to refund + '0xTokenCollector...' as `0x${string}`, // sources the refund + '0xSignatureData...' as `0x${string}`, // authorization data +) +console.log('Post-escrow refund:', tx) ``` Post-escrow refunds require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer. -## Query Methods +### payment.approvePostEscrowRefund -### Get Payment Amounts - -Use `getPaymentAmounts()` to query the current capturable and refundable amounts for a payment. This method reads directly from the escrow contract. +Approve a post-escrow refund allowance. This sets how much the refund budget contract can pull from the merchant. ```typescript -const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo); - -console.log('Capturable:', capturableAmount); // Funds available to release -console.log('Refundable:', refundableAmount); // Funds available to refund - -if (capturableAmount > 0n) { - // Release available funds - const { txHash } = await merchant.release(paymentInfo, capturableAmount); - console.log('Released all capturable funds:', txHash); -} +const tx = await merchant.payment.approvePostEscrowRefund(paymentInfo, 5_000_000n) +console.log('Allowance approved:', tx) ``` - -`getPaymentAmounts()` requires the `escrowAddress` to be configured when creating the `X402rMerchant` instance. - +### payment.getPostEscrowRefundAllowance -### Get Operator Configuration - -Use `getOperatorConfig()` to retrieve all 14 immutable slot addresses from the PaymentOperator contract. This includes the escrow address, fee configuration, all 5 condition slots, and all 5 recorder slots. +Check the current post-escrow refund allowance. ```typescript -const config = await merchant.getOperatorConfig(); - -// Core state -console.log('Escrow:', config.escrow); -console.log('Fee recipient:', config.feeRecipient); -console.log('Fee calculator:', config.feeCalculator); -console.log('Protocol fee config:', config.protocolFeeConfig); - -// Condition slots (address(0) = always allow) -console.log('Authorize condition:', config.authorizeCondition); -console.log('Charge condition:', config.chargeCondition); -console.log('Release condition:', config.releaseCondition); -console.log('Refund in-escrow condition:', config.refundInEscrowCondition); -console.log('Refund post-escrow condition:', config.refundPostEscrowCondition); - -// Recorder slots (address(0) = no-op) -console.log('Authorize recorder:', config.authorizeRecorder); -console.log('Charge recorder:', config.chargeRecorder); -console.log('Release recorder:', config.releaseRecorder); -console.log('Refund in-escrow recorder:', config.refundInEscrowRecorder); -console.log('Refund post-escrow recorder:', config.refundPostEscrowRecorder); +const allowance = await merchant.payment.getPostEscrowRefundAllowance(paymentInfo) +console.log('Allowance:', allowance) ``` -### Get Fee Structure +## Query methods + +### payment.getAmounts -Use `getFeeStructure()` to retrieve the fee-related addresses for the operator. This is a lighter alternative to `getOperatorConfig()` when you only need fee information. +Query the current capturable and refundable amounts for a payment. ```typescript -const fees = await merchant.getFeeStructure(); +const amounts = await merchant.payment.getAmounts(paymentInfo) -console.log('Fee calculator:', fees.feeCalculator); -console.log('Protocol fee config:', fees.protocolFeeConfig); -console.log('Fee recipient:', fees.feeRecipient); +console.log('Has collected:', amounts.hasCollectedPayment) +console.log('Capturable:', amounts.capturableAmount) +console.log('Refundable:', amounts.refundableAmount) ``` -The returned `FeeStructure` contains three fields: - -| Field | Type | Description | -|-------|------|-------------| -| `feeCalculator` | `0x${string}` | Contract that computes fee amounts | -| `protocolFeeConfig` | `0x${string}` | Protocol-level fee configuration | -| `feeRecipient` | `0x${string}` | Address that receives the operator's fee share | +### payment.getState -### Get Release Conditions - -Use `getReleaseConditions()` to check which condition contract governs release operations. A zero address means releases are always allowed. +Derive the lifecycle state of a payment from the operator contract. ```typescript -const releaseCondition = await merchant.getReleaseConditions(); - -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; -if (releaseCondition === ZERO_ADDRESS) { - console.log('No release conditions configured - releases always allowed'); -} else { - console.log('Release condition contract:', releaseCondition); -} +const state = await merchant.payment.getState(paymentInfo) +// 0 = NonExistent, 1 = InEscrow, 2 = Released, 3 = Settled, 4 = Expired ``` -### Get Payment State +### operator.getConfig -Use `getPaymentState()` to derive the lifecycle state of a payment from the escrow contract. +Retrieve all slot addresses from the PaymentOperator contract, including conditions and recorders. ```typescript -import { PaymentState } from '@x402r/core'; +const config = await merchant.operator.getConfig() -const state = await merchant.getPaymentState(paymentInfo); -// PaymentState: NonExistent, InEscrow, Released, Settled, or Expired +console.log('Fee recipient:', config.feeRecipient) +console.log('Fee calculator:', config.feeCalculator) +console.log('Release condition:', config.releaseCondition) ``` -```typescript -getPaymentState(paymentInfo: PaymentInfo): Promise -``` - -### Get Receiver Payments +### operator.getFeeAddresses -Use `getReceiverPayments()` to list all payments where the connected wallet is the receiver, by scanning `AuthorizationCreated` events. +Get just the fee-related addresses. ```typescript -const payments = await merchant.getReceiverPayments(); +const fees = await merchant.operator.getFeeAddresses() -for (const { hash, paymentInfo } of payments) { - console.log(`Payment ${hash}: ${paymentInfo.maxAmount}`); -} -``` - -```typescript -getReceiverPayments( - fromBlock?: bigint -): Promise> +console.log('Fee calculator:', fees.feeCalculator) +console.log('Protocol fee config:', fees.protocolFeeConfig) +console.log('Fee recipient:', fees.feeRecipient) ``` - -This method scans event logs. Pass `fromBlock` to limit the scan range if your RPC limits `eth_getLogs` responses (Base Sepolia typically caps at 10,000 blocks). - - -### Get Payment Details +### operator.calculateFees -Use `getPaymentDetails()` to retrieve the full `PaymentInfo` struct by scanning `AuthorizationCreated` events for a given hash. +Calculate the full fee breakdown for a payment amount. ```typescript -const details = await merchant.getPaymentDetails(paymentInfoHash); -console.log('Payer:', details.payer); -console.log('Amount:', details.maxAmount); -``` +const fees = await merchant.operator.calculateFees(paymentInfo, 1_000_000n) -```typescript -getPaymentDetails( - paymentInfoHash: `0x${string}`, - fromBlock?: bigint -): Promise +console.log('Operator fee:', fees.operatorFee) +console.log('Protocol fee:', fees.protocolFee) +console.log('Total fee:', fees.totalFee) +console.log('Net amount:', fees.netAmount) ``` -## Release vs Refund Decision Flow +## Release vs refund decision flow ```mermaid flowchart TD - A[Payment in Escrow] --> B{Check getPaymentAmounts} + A[Payment in Escrow] --> B{Check payment.getAmounts} B --> C{capturableAmount > 0?} C -->|Yes| D{Has refund request?} C -->|No| E[Nothing to release] D -->|No| F[Safe to release] D -->|Yes| G{Approve refund?} - F --> H["release(paymentInfo, amount)"] - G -->|Yes| I["refundInEscrow(paymentInfo, amount)"] + F --> H["payment.release(paymentInfo, amount)"] + G -->|Yes| I["payment.refundInEscrow(paymentInfo, amount)"] G -->|No| J[Deny request, then release] J --> H ``` -## Next Steps +## Next steps diff --git a/sdk/merchant/quickstart.mdx b/sdk/merchant/quickstart.mdx index 84aa102..712b9d0 100644 --- a/sdk/merchant/quickstart.mdx +++ b/sdk/merchant/quickstart.mdx @@ -1,232 +1,212 @@ --- title: "Merchant SDK" -description: "Release funds, charge payments, process refunds, and query escrow state with @x402r/merchant" +description: "Release funds, charge payments, process refunds, and query escrow state" icon: "rocket" --- -The `@x402r/merchant` package provides everything merchants need for the post-payment lifecycle: releasing escrowed funds, charging directly, processing refunds, and querying operator state. +The `@x402r/sdk` package provides everything merchants need for the post-payment lifecycle: releasing escrowed funds, charging directly, processing refunds, and querying operator state. -**Looking for server setup?** The [Merchant Server Quickstart](/sdk/merchant/getting-started) shows how to accept escrow payments via Express middleware. This page covers the `X402rMerchant` class for managing payments after they arrive. +**Looking for server setup?** The [Merchant Server Quickstart](/sdk/merchant/getting-started) shows how to accept escrow payments via Express middleware. This page covers the `createMerchantClient` factory for managing payments after they arrive. ## Installation -```bash -npm install @x402r/merchant @x402r/helpers @x402r/core viem + +```bash npm +npm install @x402r/sdk viem ``` +```bash pnpm +pnpm add @x402r/sdk viem +``` +```bash bun +bun add @x402r/sdk viem +``` + ## Setup -Create viem clients as described in [Installation](/sdk/overview), then: - ```typescript -import { X402rMerchant } from '@x402r/merchant'; -import { getNetworkConfig } from '@x402r/core'; +import { createMerchantClient } from '@x402r/sdk' +import { createPublicClient, createWalletClient, http } from 'viem' +import { baseSepolia } from 'viem/chains' +import { privateKeyToAccount } from 'viem/accounts' -const config = getNetworkConfig('eip155:84532')!; +const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) -const merchant = new X402rMerchant({ - publicClient, - walletClient, - operatorAddress: '0x...', // Your PaymentOperator address - escrowAddress: config.authCaptureEscrow, - refundRequestAddress: config.refundRequest, -}); +const merchant = createMerchantClient({ + publicClient: createPublicClient({ chain: baseSepolia, transport: http() }), + walletClient: createWalletClient({ + account, + chain: baseSepolia, + transport: http(), + }), + operatorAddress: '0x...', + escrowPeriodAddress: '0x...', + refundRequestAddress: '0x...', + refundRequestEvidenceAddress: '0x...', + freezeAddress: '0x...', +}) ``` -## Release Funds from Escrow +## Release funds from escrow -Use `release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is **required** and specifies the exact amount to release in token units. +Use `payment.release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is required. ```typescript // Release 10 USDC (6 decimals) from escrow -const { txHash } = await merchant.release(paymentInfo, BigInt('10000000')); -console.log('Released:', txHash); +const tx = await merchant.payment.release(paymentInfo, 10_000_000n) +console.log('Released:', tx) ``` -For partial releases, specify a smaller amount. The remaining funds stay in escrow and can be released or refunded later. +For partial releases, specify a smaller amount. The remaining funds stay in escrow. ```typescript // Release 3 USDC of a 10 USDC escrow -const { txHash } = await merchant.release(paymentInfo, BigInt('3000000')); -console.log('Partial release:', txHash); +const tx = await merchant.payment.release(paymentInfo, 3_000_000n) +console.log('Partial release:', tx) // Check what remains -const { capturableAmount } = await merchant.getPaymentAmounts(paymentInfo); -console.log('Remaining in escrow:', capturableAmount); // 7000000n +const amounts = await merchant.payment.getAmounts(paymentInfo) +console.log('Remaining in escrow:', amounts.capturableAmount) // 7000000n ``` -The `amount` parameter is always required. There is no default "release all" behavior. Always query `getPaymentAmounts()` first to determine the available capturable amount. +Always query `payment.getAmounts()` first to determine the available capturable amount. -## Refund While in Escrow +## Refund while in escrow -Use `refundInEscrow()` to return escrowed funds to the payer before release. The `amount` parameter is **required**. +Use `payment.refundInEscrow()` to return escrowed funds to the payer before release. ```typescript // Full refund of 10 USDC -const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('10000000')); -console.log('Refunded from escrow:', txHash); +const tx = await merchant.payment.refundInEscrow(paymentInfo, 10_000_000n) +console.log('Refunded from escrow:', tx) ``` ```typescript // Partial refund: return 2 USDC, keep 8 USDC in escrow -const { txHash } = await merchant.refundInEscrow(paymentInfo, BigInt('2000000')); -console.log('Partial refund:', txHash); +const tx = await merchant.payment.refundInEscrow(paymentInfo, 2_000_000n) +console.log('Partial refund:', tx) ``` -## Charge Directly +## Charge directly -Use `charge()` for non-escrow flows such as subscriptions or session-based payments. This pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`). +Use `payment.charge()` for non-escrow flows such as subscriptions or session-based payments. This pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`). ```typescript -const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...'; -const collectorData: `0x${string}` = '0xSignatureOrCalldata...'; - -const { txHash } = await merchant.charge( +const tx = await merchant.payment.charge( paymentInfo, - BigInt('5000000'), // 5 USDC - tokenCollectorAddress, // token collector contract - collectorData // authorization data (e.g., ERC-3009 signature) -); -console.log('Charged:', txHash); + 5_000_000n, // 5 USDC + '0xTokenCollector...' as `0x${string}`, // token collector contract + '0xSignatureData...' as `0x${string}`, // authorization data +) +console.log('Charged:', tx) ``` The `charge()` method is designed for recurring payments and session-based billing where funds are not pre-escrowed. The token collector contract handles the actual token transfer. -## Refund After Release (Post-Escrow) +## Refund after release (post-escrow) -Use `refundPostEscrow()` to refund funds that have already been released to the receiver. This requires a token collector to source the refund from the merchant's balance. +Use `payment.refundPostEscrow()` to refund funds that have already been released. This requires a token collector to source the refund from the merchant's balance. ```typescript -const tokenCollectorAddress: `0x${string}` = '0xTokenCollector...'; -const collectorData: `0x${string}` = '0xSignatureOrCalldata...'; - -const { txHash } = await merchant.refundPostEscrow( +const tx = await merchant.payment.refundPostEscrow( paymentInfo, - BigInt('5000000'), // 5 USDC to refund - tokenCollectorAddress, // token collector that sources the refund - collectorData // authorization data -); -console.log('Post-escrow refund:', txHash); + 5_000_000n, // 5 USDC to refund + '0xTokenCollector...' as `0x${string}`, // sources the refund + '0xSignatureData...' as `0x${string}`, // authorization data +) +console.log('Post-escrow refund:', tx) ``` Post-escrow refunds require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer. -## Query Methods +## Query methods -### Get Payment Amounts +### payment.getAmounts -Use `getPaymentAmounts()` to query the current capturable and refundable amounts for a payment. +Query the current capturable and refundable amounts for a payment. ```typescript -const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo); +const amounts = await merchant.payment.getAmounts(paymentInfo) -console.log('Capturable:', capturableAmount); // Funds available to release -console.log('Refundable:', refundableAmount); // Funds available to refund +console.log('Capturable:', amounts.capturableAmount) +console.log('Refundable:', amounts.refundableAmount) -if (capturableAmount > 0n) { - const { txHash } = await merchant.release(paymentInfo, capturableAmount); - console.log('Released all capturable funds:', txHash); +if (amounts.capturableAmount > 0n) { + await merchant.payment.release(paymentInfo, amounts.capturableAmount) } ``` - -`getPaymentAmounts()` requires the `escrowAddress` to be configured when creating the `X402rMerchant` instance. - - -### Get Payment State +### payment.getState -Use `getPaymentState()` to derive the lifecycle state of a payment from the escrow contract. +Derive the lifecycle state of a payment from the operator contract. ```typescript -import { PaymentState } from '@x402r/core'; - -const state = await merchant.getPaymentState(paymentInfo); -// PaymentState: NonExistent, InEscrow, Released, Settled, or Expired +const state = await merchant.payment.getState(paymentInfo) +// 0 = NonExistent, 1 = InEscrow, 2 = Released, 3 = Settled, 4 = Expired ``` -### Get Receiver Payments +### operator.getConfig -Use `getReceiverPayments()` to list all payments where the connected wallet is the receiver. +Retrieve all slot addresses from the PaymentOperator contract. ```typescript -const payments = await merchant.getReceiverPayments(); - -for (const { hash, paymentInfo } of payments) { - console.log(`Payment ${hash}: ${paymentInfo.maxAmount}`); -} -``` - - -This method scans event logs. Pass `fromBlock` to limit the scan range if your RPC limits `eth_getLogs` responses (Base Sepolia typically caps at 10,000 blocks). - - -### Get Payment Details - -Use `getPaymentDetails()` to retrieve the full `PaymentInfo` struct by scanning `AuthorizationCreated` events for a given hash. +const config = await merchant.operator.getConfig() -```typescript -const details = await merchant.getPaymentDetails(paymentInfoHash); -console.log('Payer:', details.payer); -console.log('Amount:', details.maxAmount); +console.log('Fee recipient:', config.feeRecipient) +console.log('Fee calculator:', config.feeCalculator) +console.log('Release condition:', config.releaseCondition) ``` -### Get Operator Configuration +### operator.getFeeAddresses -Use `getOperatorConfig()` to retrieve all 14 immutable slot addresses from the PaymentOperator contract. +Get just the fee-related addresses. ```typescript -const config = await merchant.getOperatorConfig(); +const fees = await merchant.operator.getFeeAddresses() -console.log('Escrow:', config.escrow); -console.log('Fee recipient:', config.feeRecipient); -console.log('Fee calculator:', config.feeCalculator); -console.log('Release condition:', config.releaseCondition); +console.log('Fee calculator:', fees.feeCalculator) +console.log('Protocol fee config:', fees.protocolFeeConfig) +console.log('Fee recipient:', fees.feeRecipient) ``` -### Get Fee Structure +### operator.calculateFees -Use `getFeeStructure()` for just the fee-related addresses — a lighter alternative to `getOperatorConfig()`. +Calculate the full fee breakdown for a payment amount. ```typescript -const fees = await merchant.getFeeStructure(); - -console.log('Fee calculator:', fees.feeCalculator); -console.log('Protocol fee config:', fees.protocolFeeConfig); -console.log('Fee recipient:', fees.feeRecipient); -``` +const fees = await merchant.operator.calculateFees(paymentInfo, 1_000_000n) -### Get Release Conditions - -```typescript -const releaseCondition = await merchant.getReleaseConditions(); -// address(0) means releases are always allowed +console.log('Operator fee:', fees.operatorFee) +console.log('Protocol fee:', fees.protocolFee) +console.log('Total fee:', fees.totalFee) +console.log('Net amount:', fees.netAmount) ``` -## Release vs Refund Decision Flow +## Release vs refund decision flow ```mermaid flowchart TD - A[Payment in Escrow] --> B{Check getPaymentAmounts} + A[Payment in Escrow] --> B{Check payment.getAmounts} B --> C{capturableAmount > 0?} C -->|Yes| D{Has refund request?} C -->|No| E[Nothing to release] D -->|No| F[Safe to release] D -->|Yes| G{Approve refund?} - F --> H["release(paymentInfo, amount)"] - G -->|Yes| I["refundInEscrow(paymentInfo, amount)"] + F --> H["payment.release(paymentInfo, amount)"] + G -->|Yes| I["payment.refundInEscrow(paymentInfo, amount)"] G -->|No| J[Deny request, then release] J --> H ``` -## Next Steps +## Next steps diff --git a/sdk/merchant/refund-handling.mdx b/sdk/merchant/refund-handling.mdx index 02e8b31..7e9fd41 100644 --- a/sdk/merchant/refund-handling.mdx +++ b/sdk/merchant/refund-handling.mdx @@ -1,313 +1,256 @@ --- -title: "Refund Handling" -description: "Process, approve, deny, and manage refund requests with the Merchant SDK" +title: "Refund handling" +description: "Process, approve, deny, and manage refund requests as a merchant" icon: "rotate-left" --- -The `X402rMerchant` class provides a complete set of methods for handling refund requests from payers. Every refund-related method requires a `nonce: bigint` parameter that identifies which specific charge the refund targets. +The merchant client provides refund management through the `refund` and `freeze` action groups. The `refund` group requires `refundRequestAddress` in the client config. - -The `nonce` parameter corresponds to the record index from the `PaymentIndexRecorder`. For the first charge against a payment, the nonce is `0n`. Each subsequent charge increments the nonce. - +## Refund request queries -## Refund Request Queries +### refund.has -### Check If a Refund Request Exists - -Use `hasRefundRequest()` to check whether a payer has submitted a refund request for a specific payment and nonce. +Check whether a payer has submitted a refund request for a specific payment. ```typescript -const hasRequest = await merchant.hasRefundRequest(paymentInfo, 0n); +const hasRequest = await merchant.refund?.has(paymentInfo) if (hasRequest) { - console.log('Refund request exists for this payment'); -} else { - console.log('No refund request submitted'); + console.log('Refund request exists for this payment') } ``` -### Get Refund Request Status +### refund.getStatus -Use `getRefundStatus()` to retrieve the current status of a refund request. Returns a `RequestStatus` enum value. +Retrieve the current status of a refund request. ```typescript -import { RequestStatus } from '@x402r/core'; +import { RefundRequestStatus } from '@x402r/sdk' -const status = await merchant.getRefundStatus(paymentInfo, 0n); +const status = await merchant.refund?.getStatus(paymentInfo) switch (status) { - case RequestStatus.Pending: - console.log('Awaiting your decision'); - break; - case RequestStatus.Approved: - console.log('You approved this refund'); - break; - case RequestStatus.Denied: - console.log('You denied this refund'); - break; - case RequestStatus.Cancelled: - console.log('Payer cancelled the request'); - break; + case RefundRequestStatus.Pending: + console.log('Awaiting your decision') + break + case RefundRequestStatus.Approved: + console.log('You approved this refund') + break + case RefundRequestStatus.Denied: + console.log('You denied this refund') + break + case RefundRequestStatus.Cancelled: + console.log('Payer cancelled the request') + break + case RefundRequestStatus.Refused: + console.log('Arbiter declined to rule') + break } ``` -### Get Full Refund Request Data +### refund.get -Use `getRefundRequest()` to retrieve the complete refund request data, including the amount and status. +Retrieve the complete refund request data, including the amount and status. ```typescript -import type { RefundRequestData } from '@x402r/core'; - -const request: RefundRequestData = await merchant.getRefundRequest(paymentInfo, 0n); +const request = await merchant.refund?.get(paymentInfo) -console.log('Payment hash:', request.paymentInfoHash); -console.log('Nonce:', request.nonce); -console.log('Requested amount:', request.amount); -console.log('Status:', request.status); +console.log('Payment hash:', request?.paymentInfoHash) +console.log('Requested amount:', request?.amount) +console.log('Approved amount:', request?.approvedAmount) +console.log('Status:', request?.status) ``` -The `RefundRequestData` type contains: - -| Field | Type | Description | -|-------|------|-------------| -| `paymentInfoHash` | `0x${string}` | Hash of the PaymentInfo struct | -| `nonce` | `bigint` | Record index this refund targets | -| `amount` | `bigint` | Amount requested for refund (uint120) | -| `status` | `RequestStatus` | Current status (Pending, Approved, Denied, Cancelled) | - -### Get Refund Request by Composite Key +### refund.getByKey -Use `getRefundRequestByKey()` to look up a refund request directly by its composite key (the `keccak256(paymentInfoHash, nonce)` value returned from paginated queries). +Look up a refund request directly by its payment info hash. ```typescript -const request = await merchant.getRefundRequestByKey(compositeKey); - -console.log('Amount:', request.amount); -console.log('Status:', request.status); +const request = await merchant.refund?.getByKey(paymentInfoHash) +console.log('Amount:', request?.amount) +console.log('Status:', request?.status) ``` -## Paginated Refund Request Listing - -### Get Pending Refund Requests - -Use `getPendingRefundRequests()` to retrieve paginated refund request keys for the current receiver address. This method uses the wallet address associated with your `X402rMerchant` instance. - -```typescript -// Get the first 10 refund request keys -const { keys, total } = await merchant.getPendingRefundRequests(0n, 10n); - -console.log(`Showing ${keys.length} of ${total} total refund requests`); +## Paginated refund request listing -// Look up each request by its composite key -for (const key of keys) { - const request = await merchant.getRefundRequestByKey(key); - console.log(`Key: ${key}`); - console.log(` Amount: ${request.amount}`); - console.log(` Status: ${request.status}`); -} -``` +### refund.getReceiverRequests -For pagination, adjust the `offset` and `count` parameters: +Retrieve paginated refund request data for a receiver address. ```typescript -// Page through all refund requests, 20 at a time -const pageSize = 20n; -let offset = 0n; -let hasMore = true; - -while (hasMore) { - const { keys, total } = await merchant.getPendingRefundRequests(offset, pageSize); - - for (const key of keys) { - const request = await merchant.getRefundRequestByKey(key); - // Process each request... - } - - offset += pageSize; - hasMore = offset < total; +const requests = await merchant.refund?.getReceiverRequests( + receiverAddress, + 0n, // offset + 10n, // count +) + +for (const request of requests ?? []) { + console.log('Amount:', request.amount, 'Status:', request.status) } ``` -### Get Refund Request Count +### refund.getOperatorRequests -Use `getRefundRequestCount()` to get the total number of refund requests targeting the current receiver. +Retrieve paginated refund requests across all payments on an operator. ```typescript -const count = await merchant.getRefundRequestCount(); -console.log(`Total refund requests: ${count}`); - -if (count > 0n) { - const { keys } = await merchant.getPendingRefundRequests(0n, count); - console.log(`Retrieved all ${keys.length} request keys`); -} +const requests = await merchant.refund?.getOperatorRequests( + merchant.config.operatorAddress, + 0n, + 10n, +) ``` -## Refund Request Actions +## Refund request actions -### Approve a Refund Request +### refund.deny -Use `approveRefundRequest()` to approve a pending refund request. This changes the request status to `Approved`. +Deny a pending refund request. This changes the request status to `Denied`. ```typescript -const { txHash } = await merchant.approveRefundRequest(paymentInfo, 0n); -console.log('Refund approved:', txHash); +const tx = await merchant.refund?.deny(paymentInfo) +console.log('Refund denied:', tx) ``` - -Approving a refund request changes its status but does **not** transfer funds. You must also call `refundInEscrow()` or `refundPostEscrow()` to execute the actual token transfer. - + +If you deny a request, the payer may escalate to an arbiter for dispute resolution. Consider providing a reason off-chain to reduce escalation risk. + -### Deny a Refund Request +### payment.refundInEscrow -Use `denyRefundRequest()` to deny a pending refund request. This changes the request status to `Denied`. +To approve and execute a refund, call `payment.refundInEscrow()`. This both auto-approves the pending RefundRequest and transfers funds back to the payer. ```typescript -const { txHash } = await merchant.denyRefundRequest(paymentInfo, 0n); -console.log('Refund denied:', txHash); +const tx = await merchant.payment.refundInEscrow(paymentInfo, request!.amount) +console.log('Refund approved and executed:', tx) ``` - -If you deny a request, the payer may escalate to an arbiter for dispute resolution. Consider providing a reason off-chain to reduce escalation risk. - + +`refundInEscrow()` auto-approves the pending RefundRequest. There is no undo. + -## Freeze Management +## Freeze management -### Check If a Payment Is Frozen +### freeze.isFrozen -Use `isFrozen()` to check whether a payment has been frozen by the payer or an arbiter. Frozen payments cannot be released until unfrozen. +Check whether a payment has been frozen by the payer. Frozen payments cannot be released until unfrozen. ```typescript -const freezeAddress: `0x${string}` = '0xFreezeContract...'; - -const frozen = await merchant.isFrozen(paymentInfo, freezeAddress); +const frozen = await merchant.freeze?.isFrozen(paymentInfo) if (frozen) { - console.log('Payment is frozen - cannot release until unfrozen'); -} else { - console.log('Payment is not frozen'); + console.log('Payment is frozen, cannot release until unfrozen') } ``` -### Unfreeze a Payment +### freeze.unfreeze -Use `unfreezePayment()` to remove a freeze on a payment. Only the receiver (merchant) or an authorized party can unfreeze. +Remove a freeze on a payment. Only the receiver (merchant) or an authorized party can unfreeze. ```typescript -const freezeAddress: `0x${string}` = '0xFreezeContract...'; - -const { txHash } = await merchant.unfreezePayment(paymentInfo, freezeAddress); -console.log('Payment unfrozen:', txHash); +const tx = await merchant.freeze?.unfreeze(paymentInfo) +console.log('Payment unfrozen:', tx) ``` -## Complete Refund Workflow +## Complete refund workflow Here is a full workflow showing how to detect a refund request, review it, make a decision, and execute the refund if approved. ```typescript -import { createPublicClient, createWalletClient, http } from 'viem'; -import { baseSepolia } from 'viem/chains'; -import { privateKeyToAccount } from 'viem/accounts'; -import { X402rMerchant } from '@x402r/merchant'; -import { getNetworkConfig, RequestStatus } from '@x402r/core'; +import { createMerchantClient, RefundRequestStatus } from '@x402r/sdk' +import type { PaymentInfo } from '@x402r/sdk' async function handleRefundWorkflow( - merchant: X402rMerchant, + merchant: ReturnType, paymentInfo: PaymentInfo, - nonce: bigint ) { // Step 1: Check if a refund request exists - const hasRequest = await merchant.hasRefundRequest(paymentInfo, nonce); + const hasRequest = await merchant.refund?.has(paymentInfo) if (!hasRequest) { - console.log('No refund request for this payment/nonce'); - return; + console.log('No refund request for this payment') + return } // Step 2: Get the full request data - const request = await merchant.getRefundRequest(paymentInfo, nonce); - console.log(`Refund request: ${request.amount} tokens, status: ${request.status}`); + const request = await merchant.refund?.get(paymentInfo) + console.log('Refund request:', request?.amount, 'status:', request?.status) // Step 3: Only process pending requests - if (request.status !== RequestStatus.Pending) { - console.log('Request already processed'); - return; + if (request?.status !== RefundRequestStatus.Pending) { + console.log('Request already processed') + return } // Step 4: Check if the payment is frozen - const freezeAddress: `0x${string}` = '0xFreezeContract...'; - const frozen = await merchant.isFrozen(paymentInfo, freezeAddress); + const frozen = await merchant.freeze?.isFrozen(paymentInfo) if (frozen) { - console.log('Payment is frozen - resolve dispute before processing refund'); - return; + console.log('Payment is frozen, resolve dispute first') + return } // Step 5: Check available amounts - const { capturableAmount, refundableAmount } = await merchant.getPaymentAmounts(paymentInfo); - console.log(`Available to refund: ${refundableAmount}`); + const amounts = await merchant.payment.getAmounts(paymentInfo) + console.log('Available to refund:', amounts.refundableAmount) // Step 6: Make a decision - const shouldApprove = request.amount <= refundableAmount; + const shouldApprove = request.amount <= amounts.refundableAmount if (shouldApprove) { - // Approve the request - const { txHash: approveTx } = await merchant.approveRefundRequest(paymentInfo, nonce); - console.log('Approved:', approveTx); - - // Execute the refund from escrow - const { txHash: refundTx } = await merchant.refundInEscrow(paymentInfo, request.amount); - console.log('Refund executed:', refundTx); + // Execute the refund (auto-approves the request) + const tx = await merchant.payment.refundInEscrow(paymentInfo, request.amount) + console.log('Refund executed:', tx) } else { // Deny the request - const { txHash: denyTx } = await merchant.denyRefundRequest(paymentInfo, nonce); - console.log('Denied:', denyTx); + const tx = await merchant.refund?.deny(paymentInfo) + console.log('Denied:', tx) } } ``` -## Refund Request Lifecycle +## Refund request lifecycle ```mermaid sequenceDiagram - participant P as Payer (Client SDK) + participant P as Payer participant R as RefundRequest Contract - participant M as Merchant (Merchant SDK) + participant M as Merchant participant O as PaymentOperator - P->>R: requestRefund(paymentInfo, amount, nonce) + P->>R: refund.request(paymentInfo, amount) R-->>M: RefundRequested event - M->>R: hasRefundRequest(paymentInfo, nonce) + M->>R: refund.has(paymentInfo) R-->>M: true - M->>R: getRefundRequest(paymentInfo, nonce) + M->>R: refund.get(paymentInfo) R-->>M: RefundRequestData M->>M: Review request (policy check) alt Approve - M->>R: approveRefundRequest(paymentInfo, nonce) - M->>O: refundInEscrow(paymentInfo, amount) + M->>O: payment.refundInEscrow(paymentInfo, amount) O->>P: Funds returned to payer else Deny - M->>R: denyRefundRequest(paymentInfo, nonce) + M->>R: refund.deny(paymentInfo) Note over P: Payer may escalate to arbiter end ``` -## Method Reference - -| Method | Parameters | Returns | Description | -|--------|-----------|---------|-------------| -| `hasRefundRequest` | `paymentInfo, nonce: bigint` | `Promise` | Check if refund request exists | -| `getRefundStatus` | `paymentInfo, nonce: bigint` | `Promise` | Get request status | -| `getRefundRequest` | `paymentInfo, nonce: bigint` | `Promise` | Get full request data | -| `approveRefundRequest` | `paymentInfo, nonce: bigint` | `Promise<{ txHash }>` | Approve a pending request | -| `denyRefundRequest` | `paymentInfo, nonce: bigint` | `Promise<{ txHash }>` | Deny a pending request | -| `getPendingRefundRequests` | `offset: bigint, count: bigint` | `Promise<{ keys, total }>` | Paginated request keys | -| `getRefundRequestCount` | _(none)_ | `Promise` | Total requests for receiver | -| `getRefundRequestByKey` | `compositeKey: hex` | `Promise` | Look up by composite key | -| `unfreezePayment` | `paymentInfo, freezeAddress: hex` | `Promise<{ txHash }>` | Remove payment freeze | -| `isFrozen` | `paymentInfo, freezeAddress: hex` | `Promise` | Check if payment is frozen | - -## Next Steps +## Method reference + +| Method | Parameters | Returns | +|--------|-----------|---------| +| `refund.has` | `paymentInfo` | `boolean` | +| `refund.getStatus` | `paymentInfo` | `RefundRequestStatus` | +| `refund.get` | `paymentInfo` | `RefundRequestData` | +| `refund.getByKey` | `paymentInfoHash` | `RefundRequestData` | +| `refund.deny` | `paymentInfo` | `Hash` | +| `refund.refuse` | `paymentInfo` | `Hash` | +| `refund.getReceiverRequests` | `receiver, offset, count` | `RefundRequestData[]` | +| `refund.getOperatorRequests` | `operator, offset, count` | `RefundRequestData[]` | +| `freeze.isFrozen` | `paymentInfo` | `boolean` | +| `freeze.unfreeze` | `paymentInfo` | `Hash` | + +## Next steps diff --git a/sdk/merchant/subscriptions.mdx b/sdk/merchant/subscriptions.mdx index 813b31d..4d09b38 100644 --- a/sdk/merchant/subscriptions.mdx +++ b/sdk/merchant/subscriptions.mdx @@ -1,202 +1,133 @@ --- -title: "Merchant Events" -description: "Subscribe to real-time refund, release, and freeze events with the Merchant SDK" +title: "Merchant events" +description: "Subscribe to real-time refund, release, and fee events" icon: "bell" --- -The `X402rMerchant` class provides three subscription methods for watching blockchain events in real-time. Each returns an object with an `unsubscribe` function for cleanup. +The merchant client provides real-time event subscriptions through the `watch` action group. Each method returns an unsubscribe function for cleanup. -## Watch Refund Requests +## watch.onPayment -Use `watchRefundRequests()` to subscribe to `RefundRequested` events emitted by the RefundRequest contract. The callback receives a `RefundRequestEventLog` object for each event. +Watch for payment lifecycle events: `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted` on the PaymentOperator contract. ```typescript -const { unsubscribe } = merchant.watchRefundRequests((event) => { - console.log('Event:', event.eventName); - console.log('Payment hash:', event.args.paymentInfoHash); - console.log('Payer:', event.args.payer); - console.log('Receiver:', event.args.receiver); - console.log('Amount:', event.args.amount); - console.log('Nonce:', event.args.nonce); - console.log('Block:', event.blockNumber); - console.log('Tx hash:', event.transactionHash); -}); +const unwatch = merchant.watch.onPayment((logs) => { + for (const log of logs) { + console.log('Payment event:', log.eventName) + } +}) // Later: stop watching -unsubscribe(); -``` - -The `RefundRequestEventLog` type has the following shape: - -```typescript -interface RefundRequestEventLog { - eventName: 'RefundRequested' | 'RefundRequestStatusUpdated' | 'RefundRequestCancelled'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - nonce?: bigint; - status?: number; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} +unwatch() ``` - -`watchRefundRequests()` requires the `refundRequestAddress` to be configured when creating the `X402rMerchant` instance. - - -### Example: Auto-respond to Small Refund Requests +### Example: revenue tracking ```typescript -import { X402rMerchant } from '@x402r/merchant'; -import { RequestStatus } from '@x402r/core'; - -const AUTO_APPROVE_THRESHOLD = BigInt('5000000'); // 5 USDC - -const { unsubscribe } = merchant.watchRefundRequests(async (event) => { - const amount = event.args.amount; - const paymentHash = event.args.paymentInfoHash; - - console.log(`New refund request: ${paymentHash}, amount: ${amount}`); - - if (amount && amount < AUTO_APPROVE_THRESHOLD) { - console.log('Auto-approving small refund request'); - // You would look up the paymentInfo from your database - // and call merchant.approveRefundRequest(paymentInfo, nonce) - } else { - console.log('Queuing for manual review'); +let totalReleased = 0n + +const unwatch = merchant.watch.onPayment((logs) => { + for (const log of logs) { + if (log.eventName === 'ReleaseExecuted') { + const amount = log.args?.amount ?? 0n + totalReleased += amount + console.log('Release: +', amount, 'Total:', totalReleased) + } } -}); +}) ``` -## Watch Releases +## watch.onRefundRequest -Use `watchReleases()` to subscribe to `ReleaseExecuted` events emitted by the PaymentOperator contract. The callback receives a `PaymentOperatorEventLog` object for each event. +Watch for refund request lifecycle events on the RefundRequest contract. This is a no-op if `refundRequestAddress` was not provided in the client config. ```typescript -const { unsubscribe } = merchant.watchReleases((event) => { - console.log('Release executed!'); - console.log('Payment hash:', event.args.paymentInfoHash); - console.log('Amount:', event.args.amount); - console.log('Payer:', event.args.payer); - console.log('Receiver:', event.args.receiver); - console.log('Block:', event.blockNumber); - console.log('Tx hash:', event.transactionHash); -}); +const unwatch = merchant.watch.onRefundRequest((logs) => { + for (const log of logs) { + console.log('Refund event:', log.eventName) + } +}) -// Later: stop watching -unsubscribe(); +unwatch() ``` -The `PaymentOperatorEventLog` type has the following shape: +### Example: auto-respond to small refund requests ```typescript -interface PaymentOperatorEventLog { - eventName: 'ReleaseExecuted' | 'RefundInEscrowExecuted' | 'RefundPostEscrowExecuted' - | 'AuthorizationCreated' | 'ChargeExecuted'; - args: { - paymentInfoHash?: `0x${string}`; - payer?: `0x${string}`; - receiver?: `0x${string}`; - amount?: bigint; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} -``` - -### Example: Revenue Tracking +import { RefundRequestStatus } from '@x402r/sdk' -```typescript -let totalReleased = 0n; +const AUTO_APPROVE_THRESHOLD = 5_000_000n // 5 USDC -const { unsubscribe } = merchant.watchReleases((event) => { - const amount = event.args.amount ?? 0n; - totalReleased += amount; +const unwatch = merchant.watch.onRefundRequest(async (logs) => { + for (const log of logs) { + const amount = log.args?.amount + console.log('New refund request, amount:', amount) - console.log(`Release: +${amount} tokens`); - console.log(`Total released: ${totalReleased}`); -}); + if (amount && amount < AUTO_APPROVE_THRESHOLD) { + console.log('Auto-approving small refund request') + // Look up the paymentInfo from your database and call + // merchant.payment.refundInEscrow(paymentInfo, amount) + } else { + console.log('Queuing for manual review') + } + } +}) ``` -## Watch Freeze Events +## watch.onRefundExecuted -Use `watchFreezeEvents()` to subscribe to `PaymentFrozen` and `PaymentUnfrozen` events from a specific Freeze contract. You must provide the Freeze contract address as the first argument. +Watch for refund execution events: `RefundInEscrowExecuted` and `RefundPostEscrowExecuted`. ```typescript -const freezeAddress: `0x${string}` = '0xFreezeContract...'; - -const { unsubscribe } = merchant.watchFreezeEvents( - freezeAddress, - (event) => { - if (event.eventName === 'PaymentFrozen') { - console.log('Payment FROZEN:', event.args.paymentInfoHash); - console.log('Frozen by:', event.args.caller); - // Alert: a dispute may be in progress - } else if (event.eventName === 'PaymentUnfrozen') { - console.log('Payment UNFROZEN:', event.args.paymentInfoHash); - console.log('Unfrozen by:', event.args.caller); - // The payment can now be released - } +const unwatch = merchant.watch.onRefundExecuted((logs) => { + for (const log of logs) { + console.log('Refund executed:', log.eventName) } -); +}) -// Later: stop watching -unsubscribe(); +unwatch() ``` -The `FreezeEventLog` type has the following shape: +## watch.onFeeDistribution + +Watch for `FeesDistributed` events on the PaymentOperator contract. ```typescript -interface FreezeEventLog { - eventName: 'PaymentFrozen' | 'PaymentUnfrozen'; - args: { - paymentInfoHash?: `0x${string}`; - caller?: `0x${string}`; - }; - address: `0x${string}`; - blockNumber: bigint; - transactionHash: `0x${string}`; - logIndex: number; -} -``` +const unwatch = merchant.watch.onFeeDistribution((logs) => { + for (const log of logs) { + console.log('Fees distributed:', log) + } +}) - -The `freezeAddress` parameter is the address of the Freeze condition contract, not the PaymentOperator. You can retrieve it from your operator config via `merchant.getOperatorConfig()`. - +unwatch() +``` -## Event Types Reference +## Event types reference -| Method | Event Name | Callback Type | Use Case | -|--------|-----------|---------------|----------| -| `watchRefundRequests` | `RefundRequested` | `RefundRequestEventLog` | Detect incoming refund requests | -| `watchReleases` | `ReleaseExecuted` | `PaymentOperatorEventLog` | Track revenue and release confirmations | -| `watchFreezeEvents` | `PaymentFrozen` / `PaymentUnfrozen` | `FreezeEventLog` | Monitor dispute-related freezes | +| Method | Events Watched | Contract | +|--------|---------------|----------| +| `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | +| `watch.onRefundRequest` | All RefundRequest ABI events | RefundRequest | +| `watch.onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | +| `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | -All subscription methods use viem's `watchContractEvent` under the hood. For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). +For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). -## Next Steps +## Next steps Release funds, charge, and query escrow state. - + Learn about dispute resolution from the arbiter perspective. Process refund requests with approve/deny workflows. - See how clients subscribe to the same events. + See how payers subscribe to the same events. From ae81cc5f61b127eb38015d197c98af9906124ef2 Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:26:47 +0000 Subject: [PATCH 11/37] Add ERC-8004 extraction helpers and identity.check() docs Generated-By: mintlify-agent --- sdk/create-client.mdx | 72 +++++++++++++++++- sdk/helpers/erc8004.mdx | 159 ++++++++++++++++++++++++++++++++++++++++ sdk/overview.mdx | 2 +- 3 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 sdk/helpers/erc8004.mdx diff --git a/sdk/create-client.mdx b/sdk/create-client.mdx index 5df2a78..dda911c 100644 --- a/sdk/create-client.mdx +++ b/sdk/create-client.mdx @@ -96,7 +96,73 @@ const extended = client.extend( const payments = await extended.query.getPayerPayments(payerAddress) ``` -## Next Steps +### ERC-8004 plugin + +The `erc8004Actions` plugin adds `identity`, `reputation`, and `discovery` action groups for on-chain agent identity and reputation: + +```typescript +import { createX402r, erc8004Actions } from '@x402r/sdk' + +const client = createX402r({ publicClient, walletClient, operatorAddress: '0x...' }) + +const extended = client.extend(erc8004Actions()) + +// Identity +await extended.identity.register('https://my-agent.example.com') +await extended.identity.verifyAgentId(42n, '0xAgentAddress...') +await extended.identity.resolveAgent(42n) +await extended.identity.isRegistered('0xAgentAddress...') + +// Verify + reputation in one call +const result = await extended.identity.check(42n, '0xAgentAddress...', [ + '0xReviewer1...', + '0xReviewer2...', +]) +console.log('Verified:', result.verified) +console.log('Reputation:', result.reputation) // ReputationSummary | null + +// Reputation +await extended.reputation.rate(42n, 85) +await extended.reputation.getSummary(42n, ['0xReviewer...']) + +// Discovery +await extended.discovery.resolveServiceEndpoint(42n, 'arbiter') +``` + +#### `identity.check()` + +Verifies an agent's on-chain identity and optionally fetches their reputation summary in a single call. Both the verification and reputation lookup run in parallel. + +```typescript +const { verified, reputation } = await extended.identity.check( + agentId, + agentAddress, + reviewerAddresses, // optional +) +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `agentId` | `bigint` | The agent's on-chain ID | +| `address` | `Address` | The address claiming to own the agent ID | +| `reviewers` | `readonly Address[]` | Optional list of reviewer addresses for reputation lookup | + +Returns `CheckAgentResult`: + +```typescript +interface CheckAgentResult { + verified: boolean + reputation: ReputationSummary | null +} +``` + +When `reviewers` is omitted or empty, `reputation` is `null` and only on-chain verification runs. + + +For standalone helpers that extract identity data from x402 extension responses without a client instance, see [ERC-8004 helpers](/sdk/helpers/erc8004). + + +## Next steps @@ -105,7 +171,7 @@ const payments = await extended.query.getPayerPayments(payerAddress) Get the addresses for your client config. - - Conditions and recorders that control on-chain access. + + Standalone extraction helpers for arbiter identity and reputation data. diff --git a/sdk/helpers/erc8004.mdx b/sdk/helpers/erc8004.mdx new file mode 100644 index 0000000..59be3b6 --- /dev/null +++ b/sdk/helpers/erc8004.mdx @@ -0,0 +1,159 @@ +--- +title: "ERC-8004 helpers" +description: "Extract arbiter identity and agent registrations from x402 extension responses" +--- + +The `@x402r/sdk` package exports three helper functions for working with ERC-8004 identity data from x402 payment extensions. These are standalone functions that do not require a client instance. + +## `extractArbiterIdentity()` + +Extracts the arbiter's `agentId` and `address` from a raw attestation extension response. The attestation extension passes through untyped data, so this helper validates the shape before returning a typed result. + +Returns `undefined` if the arbiter has not registered or if the attestation extension is absent. + +```typescript +import { extractArbiterIdentity } from '@x402r/sdk' + +const identity = extractArbiterIdentity( + paymentRequired.extensions?.attestation?.info?.identity +) + +if (identity) { + console.log('Agent ID:', identity.agentId) // bigint + console.log('Address:', identity.address) // 0x-prefixed address +} +``` + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `attestationInfo` | `unknown` | Raw attestation identity object from `extensions.attestation.info.identity` | + +### Return type + +Returns `ArbiterIdentity | undefined`: + +```typescript +interface ArbiterIdentity { + agentId: bigint + address: Address +} +``` + +The `agentId` field is coerced to `bigint` from any of: `bigint`, integer `number`, or numeric `string` (covers JSON deserialization). Non-integer numbers and non-numeric strings return `undefined`. + +## `extractReputationRegistrations()` + +Parses agent registrations from the upstream x402 reputation extension (`extensions["reputation"].info.registrations`). Agents can have multiple registrations across chains (EVM + Solana). Each registration has a CAIP-10 `agentRegistry` and a numeric `agentId`. + +Returns an empty array if the reputation extension is absent or malformed. + + +The upstream x402 reputation extension is not yet merged. The shape of `extensions["reputation"]` may change. See [x402-foundation/x402#931](https://github.com/x402-foundation/x402/issues/931). + + +```typescript +import { extractReputationRegistrations } from '@x402r/sdk' + +const registrations = extractReputationRegistrations( + paymentRequired.extensions +) + +for (const reg of registrations) { + console.log('Agent ID:', reg.agentId) // bigint + console.log('Registry:', reg.agentRegistry) // CAIP-10 string +} +``` + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `extensions` | `Record \| undefined` | Extensions object from `PaymentRequired` or `PaymentPayload` | + +### Return type + +Returns `AgentRegistration[]`: + +```typescript +interface AgentRegistration { + agentId: bigint + /** CAIP-10 identity registry address (e.g. "eip155:8453:0x8004..." or "solana:5eykt4...:satiRkx...") */ + agentRegistry: string +} +``` + +Entries with non-numeric `agentId` values (such as Solana base58 mint addresses) or empty `agentRegistry` strings are silently skipped. + +## `fetchArbiterIdentity()` + +Fetches the arbiter's identity by POSTing to the `/attest/identity` endpoint. You can use this at merchant startup to verify the arbiter before serving customers. + +Returns `undefined` if the arbiter does not include an `agentId`. Network errors (DNS failures, connection refused) propagate as thrown exceptions. + +```typescript +import { fetchArbiterIdentity } from '@x402r/sdk' + +const identity = await fetchArbiterIdentity('https://arbiter.example.com') + +if (identity) { + console.log('Arbiter agent:', identity.agentId, identity.address) +} else { + console.warn('Arbiter is not registered with an agentId') +} +``` + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `arbiterUrl` | `string` | Base URL of the arbiter service (trailing slash is handled) | + +### Return type + +Returns `Promise`. See `extractArbiterIdentity()` for the `ArbiterIdentity` type. + +## Complete example + +A merchant service that verifies the arbiter at startup and checks agent registrations on each payment: + +```typescript +import { + fetchArbiterIdentity, + extractReputationRegistrations, +} from '@x402r/sdk' + +// At startup: verify the arbiter +const arbiterIdentity = await fetchArbiterIdentity( + process.env.ARBITER_URL! +) +if (!arbiterIdentity) { + throw new Error('Arbiter is not registered, refusing to start') +} +console.log('Arbiter verified:', arbiterIdentity.agentId) + +// On each payment: check reputation registrations +function handlePayment(paymentRequired: { extensions?: Record }) { + const registrations = extractReputationRegistrations( + paymentRequired.extensions + ) + if (registrations.length === 0) { + console.log('No reputation registrations found') + } + for (const reg of registrations) { + console.log(`Agent ${reg.agentId} on ${reg.agentRegistry}`) + } +} +``` + +## Next steps + + + + Add escrow config to x402 payment options. + + + Client factory with identity.check() for combined verification. + + diff --git a/sdk/overview.mdx b/sdk/overview.mdx index 4b43e59..9049c73 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -32,7 +32,7 @@ bun add @x402r/sdk ``` -`@x402r/sdk` is the only package most developers need. It includes role-scoped client factories, 8 action groups (payment, escrow, refund, evidence, freeze, query, operator, watch), and an `.extend()` plugin system. +`@x402r/sdk` is the only package most developers need. It includes role-scoped client factories, 8 action groups (payment, escrow, refund, evidence, freeze, query, operator, watch), an `.extend()` plugin system, and [ERC-8004 helpers](/sdk/helpers/erc8004) for extracting agent identity and reputation data from x402 extension responses. For low-level access to contract ABIs and deploy utilities: From 08908a42b3a737abd3f3b09276ded695c49e13a3 Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:09:34 +0000 Subject: [PATCH 12/37] Fix broken internal links in cursor AI tools guide Generated-By: mintlify-agent --- ai-tools/cursor.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ai-tools/cursor.mdx b/ai-tools/cursor.mdx index fbb7761..061d618 100644 --- a/ai-tools/cursor.mdx +++ b/ai-tools/cursor.mdx @@ -233,17 +233,17 @@ Example of accordion groups: Example of cards and card groups: - + Complete walkthrough from installation to your first API call in under 10 minutes. - - Learn how to authenticate requests using API keys or JWT tokens. + + Learn how to set up and configure the SDK client for payments. - - Understand rate limits and best practices for high-volume usage. + + Understand current limitations and best practices for usage. From e4eca66daf6f2703af3c871851a8a11045d1a080 Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 00:58:40 +0000 Subject: [PATCH 13/37] Add @x402r/cli documentation page Generated-By: mintlify-agent --- docs.json | 1 + sdk/cli.mdx | 204 +++++++++++++++++++++++++++++++++++++++++++++++ sdk/overview.mdx | 19 ++++- 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 sdk/cli.mdx diff --git a/docs.json b/docs.json index d4c2b4a..7587d43 100644 --- a/docs.json +++ b/docs.json @@ -112,6 +112,7 @@ { "group": "Reference", "pages": [ + "sdk/cli", "sdk/create-client", "sdk/examples" ] diff --git a/sdk/cli.mdx b/sdk/cli.mdx new file mode 100644 index 0000000..faaa55e --- /dev/null +++ b/sdk/cli.mdx @@ -0,0 +1,204 @@ +--- +title: "CLI" +description: "One-shot command-line tool for paying x402 endpoints. Wallet-agnostic with zero provider dependencies." +icon: "terminal" +--- + +`@x402r/cli` makes a single x402 payment from the command line. You point it at a URL, provide a signer, and get back the response body plus a settlement transaction hash. + +The CLI carries zero provider SDK dependencies. Raw private keys, JSON-RPC signers (Privy, Turnkey, Fireblocks, Safe), and custom signer modules all work through the same interface. + +### Install + + +```bash npm +npx @x402r/cli pay [options] +``` +```bash pnpm +pnpm dlx @x402r/cli pay [options] +``` +```bash bun +bunx @x402r/cli pay [options] +``` + + +No project install required. Pin the version (e.g. `@x402r/cli@0.2.0`) for reproducible agent workflows. + +### Usage + +```bash +x402r pay [signer flags] [--chain ] [--rpc ] [--max-amount N] [--json] +``` + +If the URL does not return HTTP 402, the CLI short-circuits and prints the response body with exit code 0. No payment is made. + +### Signer configuration + +Exactly one signer source must be configured. CLI flags take precedence over environment variables. If zero or multiple sources are detected, the CLI exits with code 6. + +| Source | Flag | Env var | +|--------|------|---------| +| Raw private key | `--key 0x...` | `PRIVATE_KEY` | +| Remote JSON-RPC | `--signer-url ` and `--signer-address 0x...` | `SIGNER_URL` and `SIGNER_ADDRESS` | +| Custom module | `--signer-module ` | `SIGNER_MODULE` | + +Environment variable names are unprefixed to match Foundry, Hardhat, and x402-reference conventions. + +### Request options + +| Flag | Description | +|------|-------------| +| `--chain ` | Select a specific `accepts[]` entry when the merchant offers multiple chains. Required when there are multiple options. | +| `--rpc ` | Override the RPC URL for on-chain reads. Required for chain IDs not in `viem/chains`. | +| `--max-amount ` | Refuse to pay more than `n` atomic token units. Exits with code 3 if the price exceeds this. | +| `--json` | Emit a single JSON envelope to stdout instead of plain text. | + +### Exit codes + +| Code | Meaning | +|------|---------| +| 0 | Success | +| 1 | Network error | +| 2 | Malformed 402 response or unusable `accepts[]` | +| 3 | Price exceeds `--max-amount` | +| 4 | Signature rejected | +| 5 | Settlement failed (merchant error after payment, or facilitator error) | +| 6 | Signer resolution failed (none, multiple, or partially configured) | + +### Examples + +#### Raw private key + +```bash +PRIVATE_KEY=0xabc123... npx @x402r/cli pay https://api.example.com/paid-endpoint +``` + +#### JSON-RPC signer + +Any endpoint that speaks `eth_signTypedData_v4` works: Privy wallet RPC, Turnkey, Fireblocks, Safe, a local `cast wallet` endpoint, or a hardware wallet behind an RPC bridge. + +```bash +npx @x402r/cli pay https://api.example.com/paid-endpoint \ + --signer-url https://signer.example/rpc \ + --signer-address 0xYourAddress... +``` + +#### Custom module (Privy) + +```javascript privy-signer.js +import { PrivyClient } from "@privy-io/server-auth"; +import { createViemAccount } from "@privy-io/server-auth/viem"; + +export default async function () { + const privy = new PrivyClient( + process.env.PRIVY_APP_ID, + process.env.PRIVY_APP_SECRET + ); + return createViemAccount({ + walletId: process.env.PRIVY_WALLET_ID, + address: process.env.PRIVY_WALLET_ADDRESS, + privy, + }); +} +``` + +```bash +npx @x402r/cli pay https://api.example.com/paid-endpoint \ + --signer-module ./privy-signer.js +``` + +#### Custom module (Coinbase CDP) + +```javascript cdp-signer.js +import { CdpClient } from "@coinbase/cdp-sdk"; +import { toAccount } from "viem/accounts"; + +export default async function () { + const cdp = new CdpClient(); + const acct = await cdp.evm.getOrCreateAccount({ + name: process.env.CDP_ACCOUNT_NAME, + }); + return toAccount(acct); +} +``` + +```bash +npx @x402r/cli pay https://api.example.com/paid-endpoint \ + --signer-module ./cdp-signer.js +``` + +### JSON output + +With `--json`, the CLI writes a single JSON envelope to stdout: + +```json +{ + "body": "", + "status": 200, + "tx": "0x...", + "elapsedMs": 1234, + "signer": { "kind": "key", "address": "0x..." } +} +``` + +The `signer` field is omitted when the URL returned a non-402 response (no payment was made). + +### Signer module contract + +A custom signer module must default-export a factory function with the signature `() => Promise`. The returned object must be a viem `Account` with at least `address` and `signTypedData`. The CLI only needs typed-data signatures, transaction broadcasting is handled by the facilitator. + +### Programmatic usage + +You can also use the `pay` function and `resolveSigner` directly from TypeScript: + +```typescript +import { pay } from "@x402r/cli"; +import type { PayResult } from "@x402r/cli"; + +const result: PayResult = await pay({ + url: "https://api.example.com/paid-endpoint", + key: process.env.PRIVATE_KEY, + json: true, +}); + +console.log(result.body); +console.log(result.tx); +``` + + +The programmatic API uses the same `PayFlags` interface as the CLI binary. All options (chain, rpc, maxAmount, signer flags) are available. + + +### Exports + +The `@x402r/cli` package exports: + +| Export | Type | Description | +|--------|------|-------------| +| `pay` | function | Execute a one-shot payment against a URL | +| `resolveSigner` | function | Resolve a signer from flags and environment variables | +| `CliError` | class | Base error class with typed exit codes | +| `NetworkError` | class | Exit code 1 | +| `Malformed402Error` | class | Exit code 2 | +| `MaxAmountExceededError` | class | Exit code 3 | +| `SignatureRejectedError` | class | Exit code 4 | +| `SettlementError` | class | Exit code 5 | +| `SignerResolutionError` | class | Exit code 6 | + +### Supported chains + +The CLI auto-detects the chain from the 402 response's `accepts[].network` field. Any EVM chain known to `viem/chains` works out of the box (Base, Base Sepolia, Ethereum, Arbitrum, Optimism, and others). For unknown chain IDs, pass `--rpc ` to provide an RPC endpoint. + +## Next steps + + + + Use the SDK programmatically for richer payer workflows. + + + Accept payments and manage escrow releases. + + + Runnable examples for every SDK operation. + + diff --git a/sdk/overview.mdx b/sdk/overview.mdx index 4b43e59..80c91e4 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -62,9 +62,23 @@ bun add @x402r/helpers ``` +For one-shot payments from the command line (no project install required): + + +```bash npm +npx @x402r/cli pay [options] +``` +```bash pnpm +pnpm dlx @x402r/cli pay [options] +``` +```bash bun +bunx @x402r/cli pay [options] +``` + + ### Guides - + Deploy an operator, accept a payment, release funds from escrow. @@ -74,6 +88,9 @@ bun add @x402r/helpers Review disputes, approve or deny refunds, distribute fees. + + One-shot payments from the command line or scripts. + ### Supported Chains From f806fec4c52622943a4a769a284615a234f1e221 Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 23:59:10 +0000 Subject: [PATCH 14/37] Document commerce-payments v1 addresses, remove SKALE Generated-By: mintlify-agent --- sdk/overview.mdx | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/sdk/overview.mdx b/sdk/overview.mdx index 4b43e59..b14f292 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -94,4 +94,41 @@ All contracts are deployed to identical addresses on every chain via CREATE3. On | Avalanche C-Chain | `43114` | `0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E` | | Monad | `143` | `0x754704Bc059F8C67012fEd69BC8A327a5aafb603` | | Linea | `59144` | `0x176211869cA2b568f2A7D4EE941E073a821EE1ff` | -| SKALE Base | `1187947933` | `0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20` | + + +SKALE Base (`1187947933`) was removed from the registry. Its Shanghai EVM is incompatible with the Cancun-locked canonical bytecode used by the commerce-payments v1 primitives. + + +### commerce-payments v1 primitives + +The `base/commerce-payments@v1.0.0` primitives are redeployed at canonical CREATE2 addresses via CreateX permissionless salts. Each contract has the same address on every supported chain. + +| Contract | Address | +|----------|---------| +| `AuthCaptureEscrow` | `0xF8211868187974a7Fb9d99b8fFB171AD70665Dc6` | +| `ERC3009PaymentCollector` | `0x7561DC178D9aD5bc5fb103C01f448A510d2A36D0` | +| `Permit2PaymentCollector` | `0xD8490609d2da0ee626b0e676941b225cbc1A8C08` | + +Salt namespace: `commerce-payments::v1::`. + +Import the addresses from `@x402r/core`: + +```ts +import { + commercePaymentsAddresses, + commercePaymentsAuthCaptureEscrow, + commercePaymentsErc3009PaymentCollector, + commercePaymentsPermit2PaymentCollector, +} from '@x402r/core'; + +// Convenience bundle +commercePaymentsAddresses.authCaptureEscrow; +commercePaymentsAddresses.erc3009PaymentCollector; +commercePaymentsAddresses.permit2PaymentCollector; +``` + +The primitives are live on Base, Optimism, Arbitrum One, Polygon, Celo, Avalanche C-Chain, Linea, Base Sepolia, Ethereum Sepolia, and Arbitrum Sepolia. Ethereum mainnet and Monad are listed in the registry pending an imminent deploy. + + +The legacy CREATE3 exports (`authCaptureEscrow`, `tokenCollector`, `factories`, `conditions`, `recorders`) remain unchanged. They will be retired in the SDK v2 migration that ships the x402r-authored contracts at CREATE2 addresses. + From 5e6850c683aa24b42780253b984299b00248a427 Mon Sep 17 00:00:00 2001 From: A1igator Date: Mon, 11 May 2026 19:13:07 -0700 Subject: [PATCH 15/37] fix(docs): remove stale nav entry for deleted refundable.mdx PR #21 deleted sdk/helpers/refundable.mdx but the V2 rewrite's docs.json nav was merged with theirs strategy and ended up keeping a reference to the deleted page. Remove the dangling entry. --- docs.json | 1 - 1 file changed, 1 deletion(-) diff --git a/docs.json b/docs.json index 8713453..bda342a 100644 --- a/docs.json +++ b/docs.json @@ -97,7 +97,6 @@ "group": "Marketplace", "pages": [ "sdk/merchant/getting-started", - "sdk/helpers/refundable", "sdk/helpers/forward-to-arbiter", "sdk/merchant/quickstart", "sdk/merchant/refund-handling" From 3c7ff2ce99729ee8fda2ebf7b4eb2223bd1f07d8 Mon Sep 17 00:00:00 2001 From: A1igator Date: Mon, 11 May 2026 19:33:06 -0700 Subject: [PATCH 16/37] docs: align scheme docs with canonical commerce-payments + authCapture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The base commerce-payments fork has been dropped — x402r now uses the canonical contracts directly at their universal CREATE2 addresses. The scheme has also been renamed from `escrow` to `authCapture` with a new wire format (autoCapture flag for two-phase vs single-shot) and added Permit2 support alongside ERC-3009. - x402-integration/escrow-scheme.mdx: rewrite as authCapture spec — new extra fields (captureAuthorizer, captureDeadline, refundDeadline, feeRecipient, autoCapture, assetTransferMethod), Permit2 payload variant, updated verification + settlement logic, refreshed error codes, payer-agnostic PaymentInfo nonce derivation - contracts/overview.mdx, contracts/periphery/auth-capture-escrow.mdx, contracts/audits.mdx: drop fork framing and partialVoid() references; clarify that audited canonical contracts are used as-is - roadmap.mdx: drop partialVoid mention and stale spec-PR references - x402-integration/{overview,comparison}.mdx: update card titles / cross-references to authCapture Route kept stable at /x402-integration/escrow-scheme to avoid breaking inbound links. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/audits.mdx | 15 +- contracts/overview.mdx | 11 +- contracts/periphery/auth-capture-escrow.mdx | 19 +- roadmap.mdx | 12 +- x402-integration/comparison.mdx | 2 +- x402-integration/escrow-scheme.mdx | 336 ++++++++++---------- x402-integration/overview.mdx | 6 +- 7 files changed, 193 insertions(+), 208 deletions(-) diff --git a/contracts/audits.mdx b/contracts/audits.mdx index 004648b..557d36f 100644 --- a/contracts/audits.mdx +++ b/contracts/audits.mdx @@ -6,11 +6,11 @@ icon: "shield-halved" ## Audit Status -x402r is built on top of a **fork** of Base's [commerce-payments](https://github.com/base/commerce-payments) protocol. The original commerce-payments contracts have been professionally audited. Our fork adds `partialVoid()` to the escrow contract (for partial refunds during escrow), and all x402r-specific contracts built on top of the fork are **not yet audited**. +x402r is built on top of the canonical [commerce-payments](https://github.com/base/commerce-payments) protocol from Base. The commerce-payments contracts are professionally audited and are used directly at their universal CREATE2 addresses (no fork). The x402r-specific contracts built on top of them are **not yet audited**. ### What's Audited (Upstream) -The original commerce-payments `AuthCaptureEscrow` contract and its supporting infrastructure (TokenCollectors, TokenStore, Permit2 integration) were audited by: +The commerce-payments `AuthCaptureEscrow` contract and its supporting infrastructure (TokenCollectors, TokenStore, Permit2 integration) were audited by: - **Spearbit** (2 audits) - **Coinbase Protocol Security** (3 audits) @@ -21,7 +21,6 @@ These audits cover the core escrow lifecycle: authorize, capture, void, reclaim, | Component | Status | Risk | |-----------|--------|------| -| `partialVoid()` on AuthCaptureEscrow | **Unaudited** (fork addition) | Follows same patterns as audited `void()`, but adds partial amount logic | | PaymentOperator | **Unaudited** | Core operator with condition/recorder dispatch and fee system | | PaymentOperatorFactory | **Unaudited** | CREATE2 deterministic deployment | | ProtocolFeeConfig | **Unaudited** | Timelocked fee governance | @@ -36,7 +35,6 @@ These audits cover the core escrow lifecycle: authorize, capture, void, reclaim, ### What This Means - The audited escrow layer provides a strong foundation — fund custody, token transfers, and payment state transitions have been professionally reviewed -- `partialVoid()` is a minimal addition (~20 lines) that mirrors the audited `void()` function with an additional amount parameter and balance check - The condition/recorder plugin system is stateless or minimal-state by design, reducing attack surface @@ -58,11 +56,10 @@ Even without a formal audit, the x402r contracts follow established security pat We plan to pursue third-party audits as the contract architecture and use cases stabilize. Priority order: -1. **`partialVoid()`** — our only change to the audited commerce-payments escrow, and the foundation everything else depends on -2. **PaymentOperator** — condition dispatch, fee calculation, fee locking, distribution -3. **Plugin system** — conditions, recorders, combinators, and their factories -4. **EscrowPeriod + Freeze** — time-lock enforcement and freeze state management -5. **RefundRequest** — request lifecycle and access control +1. **PaymentOperator** — condition dispatch, fee calculation, fee locking, distribution +2. **Plugin system** — conditions, recorders, combinators, and their factories +3. **EscrowPeriod + Freeze** — time-lock enforcement and freeze state management +4. **RefundRequest** — request lifecycle and access control We'll publish audit reports publicly once completed. diff --git a/contracts/overview.mdx b/contracts/overview.mdx index 2d773c4..2104ef8 100644 --- a/contracts/overview.mdx +++ b/contracts/overview.mdx @@ -6,7 +6,7 @@ icon: "circle-info" ## What is x402r? -x402r is a smart contract extension for HTTP-native refundable payments. It builds on [commerce-payments](https://github.com/BackTrackCo/commerce-payments) to add dispute resolution, escrow periods, and flexible refund capabilities. +x402r is a smart contract extension for HTTP-native refundable payments. It builds on the canonical [Commerce Payments Protocol](https://github.com/base/commerce-payments) to add dispute resolution, escrow periods, and flexible refund capabilities. ## Architecture Layers @@ -14,11 +14,7 @@ See the [system architecture diagrams](https://github.com/BackTrackCo/x402r-cont ## Commerce Payments (Base Layer) -[Commerce Payments](https://github.com/BackTrackCo/commerce-payments) provides the foundational payment infrastructure. - - -x402r uses a [fork of commerce-payments](https://github.com/BackTrackCo/commerce-payments) that adds **partial void** support for handling partially completed orders and partial refunds. - +The audited [Commerce Payments Protocol](https://github.com/base/commerce-payments) provides the foundational payment infrastructure. x402r uses the canonical contracts directly (no fork) at their universal CREATE2 addresses. ### AuthCaptureEscrow @@ -28,8 +24,7 @@ Core escrow contract for holding ERC-20 tokens during payments. - Authorization-based deposits (no direct transfers) - Payment state machine: `NonExistent` → `InEscrow` → `Released` → `Settled` - Void/reclaim for failed authorizations -- **Partial void support** (x402r addition) - Refund partial amounts during escrow -- Operator-based access control +- CaptureAuthorizer-based access control **Key Methods:** ```solidity diff --git a/contracts/periphery/auth-capture-escrow.mdx b/contracts/periphery/auth-capture-escrow.mdx index aaf32bc..95361b7 100644 --- a/contracts/periphery/auth-capture-escrow.mdx +++ b/contracts/periphery/auth-capture-escrow.mdx @@ -4,11 +4,7 @@ description: "AuthCaptureEscrow and ERC3009PaymentCollector — the base layer f icon: "vault" --- -x402r builds on the [Commerce Payments Protocol](https://github.com/base/commerce-payments). Two contracts from this stack form the base layer: **AuthCaptureEscrow** (holds funds) and **ERC3009PaymentCollector** (collects funds via signed authorizations). - - -x402r uses a [fork of commerce-payments](https://github.com/BackTrackCo/commerce-payments) that adds **partial void** support for handling partially completed orders and partial refunds. - +x402r builds on the canonical [Commerce Payments Protocol](https://github.com/base/commerce-payments) (no fork). Two contracts from this stack form the base layer: **AuthCaptureEscrow** (holds funds) and **ERC3009PaymentCollector** (collects funds via signed authorizations). Both are deployed at universal CREATE2 addresses. ## AuthCaptureEscrow @@ -108,19 +104,6 @@ function reclaim( **Requires:** Receiver has approved escrow for `amount` -#### partialVoid() - -Returns partial amount to payer (x402r addition). - -```solidity -function partialVoid( - bytes32 paymentId, - uint256 amount -) external onlyOperator -``` - -**Use case:** Partial refunds for partially fulfilled orders - ### Security Features - **Operator whitelist** - Only registered operators can manage payments diff --git a/roadmap.mdx b/roadmap.mdx index 29575ba..8fceec6 100644 --- a/roadmap.mdx +++ b/roadmap.mdx @@ -15,19 +15,17 @@ icon: "map" ### Protocol V2 (Completed) -- Switched from proxy pattern to escrow scheme (commerce-payments) +- Switched from proxy pattern to authCapture scheme on the canonical [Commerce Payments Protocol](https://github.com/base/commerce-payments) - PaymentOperator with pluggable conditions and recorders -- Partial refund support (partial void) - EscrowPeriod and Freeze contracts -- Factory pattern with unified CREATE3 deterministic deployment +- Factory pattern with unified CREATE2 deterministic deployment - RefundRequestEvidence for on-chain evidence submission -- Deployed across 11 chains with unified CREATE3 addresses (same address everywhere) +- Deployed across supported chains with unified addresses (same address everywhere) -### Escrow Scheme Spec (In Progress) +### authCapture Scheme Spec (In Progress) -- [Proposal submitted to coinbase/x402 (Issue #1011)](https://github.com/coinbase/x402/issues/1011) -- [Spec PR submitted (PR #1425)](https://github.com/coinbase/x402/pull/1425) - Reference implementation: [x402r-scheme](https://github.com/BackTrackCo/x402r-scheme) +- See the [authCapture Scheme Specification](/x402-integration/escrow-scheme) for the wire format and verification logic ### Developer Experience (In Progress) diff --git a/x402-integration/comparison.mdx b/x402-integration/comparison.mdx index 41da4ae..02cf8a4 100644 --- a/x402-integration/comparison.mdx +++ b/x402-integration/comparison.mdx @@ -358,7 +358,7 @@ flowchart TD Learn about x402 and why escrow matters. - + Complete technical specification. diff --git a/x402-integration/escrow-scheme.mdx b/x402-integration/escrow-scheme.mdx index 0428249..9117757 100644 --- a/x402-integration/escrow-scheme.mdx +++ b/x402-integration/escrow-scheme.mdx @@ -1,27 +1,25 @@ --- -title: "Escrow Scheme Specification" -description: "Technical specification for the x402 escrow payment scheme" +title: "authCapture Scheme Specification" +description: "Technical specification for the x402 authCapture payment scheme" icon: "file-contract" --- ## Overview -The **escrow scheme** for x402 v2 uses the [Commerce Payments Protocol](https://github.com/base/commerce-payments) contract stack to enable secure, conditional fund handling. The client signs a single ERC-3009 authorization. The facilitator submits it to an operator, which handles token collection, escrow locking, and fee distribution in one transaction. +The **`authCapture` scheme** for x402 v2 uses the audited [Commerce Payments Protocol](https://github.com/base/commerce-payments) (`AuthCaptureEscrow` + token collectors) directly — no fork. The client signs a single signature (ERC-3009 or Permit2). The facilitator submits it, either locking funds in escrow for later capture (two-phase) or sending them directly to the receiver with refund capability (single-shot). - -This spec is based on the [escrow scheme proposal (Issue #1011)](https://github.com/coinbase/x402/issues/1011) and the [spec PR (#1425)](https://github.com/coinbase/x402/pull/1425). It uses audited on-chain escrow contracts from the Commerce Payments Protocol. - +Unlike `exact`, which has no built-in mechanism for returning funds, `authCapture` supports returning funds to the client through void, refund, and reclaim. -## Settlement Methods +## Settlement Paths -The scheme supports two settlement paths: +The scheme supports two settlement paths, selected via `extra.autoCapture`: -| Method | Behavior | -|:-------|:---------| -| `authorize` (default) | Funds held in escrow. Can be captured, voided, reclaimed, or refunded. | -| `charge` | Funds sent directly to receiver. Refundable post-settlement. | +| `autoCapture` | Behavior | +|:---|:---| +| `false` (default) | Two-phase. Funds held in escrow. CaptureAuthorizer can capture, void, or refund. Client can reclaim if the capture deadline passes. | +| `true` | Single-shot. Funds sent directly to the receiver. CaptureAuthorizer can refund post-settlement. | -### Authorize (Default) +### Two-phase (`autoCapture: false`, default) ``` AUTHORIZE -> RESOURCE DELIVERED -> CAPTURE / VOID -> (REFUND) @@ -29,7 +27,7 @@ AUTHORIZE -> RESOURCE DELIVERED -> CAPTURE / VOID -> (REFUND) - Client authorization is submitted — funds locked in escrow via `operator.authorize()`. The operator calls the token collector to execute `receiveWithAuthorization` with the client's ERC-3009 signature, then routes funds into the escrow contract. + Client authorization is submitted — funds locked in escrow via `AuthCaptureEscrow.authorize()`. The token collector executes the client's signature (ERC-3009 `receiveWithAuthorization` or Permit2 `permitTransferFrom`) to pull tokens into escrow. @@ -37,19 +35,19 @@ AUTHORIZE -> RESOURCE DELIVERED -> CAPTURE / VOID -> (REFUND) - The operator can capture (release funds to receiver via `operator.release()`) or void (return escrowed funds to client). Capture conditions are configurable per operator (time-locked, arbiter-approved, etc.). + The captureAuthorizer can capture (release funds to the receiver) or void (return escrowed funds to the client). Capture conditions are policy-defined per captureAuthorizer (time-locked, arbiter-approved, etc.). - If `authorizationExpiry` passes without capture, the client can reclaim funds directly from escrow without operator approval. + If `captureDeadline` passes without capture, the client can reclaim funds directly from the escrow without captureAuthorizer involvement. - After capture, the operator can refund within the `refundExpiry` window via `operator.refundPostEscrow()`. + After capture, the captureAuthorizer can refund within the `refundDeadline` window. -### Charge +### Single-shot (`autoCapture: true`) ``` CHARGE -> RESOURCE DELIVERED -> (REFUND) @@ -57,7 +55,7 @@ CHARGE -> RESOURCE DELIVERED -> (REFUND) - Client authorization is submitted — funds sent directly to receiver via `operator.charge()`. No escrow hold. + Client authorization is submitted — funds sent directly to receiver via `AuthCaptureEscrow.charge()`. No escrow hold. @@ -65,7 +63,7 @@ CHARGE -> RESOURCE DELIVERED -> (REFUND) - The operator can refund within the `refundExpiry` window via `operator.refundPostEscrow()`. + The captureAuthorizer can refund within the `refundDeadline` window. @@ -88,64 +86,63 @@ sequenceDiagram Note over Client,Receiver: No recourse after payment - Payment is final ``` -### Escrow Payment (Deferred Settlement) +### authCapture (Two-phase) ```mermaid sequenceDiagram participant Client participant Server participant Facilitator - participant Operator - participant Escrow + participant Escrow as AuthCaptureEscrow participant Receiver Client->>Server: GET /resource Server-->>Client: 402 PaymentRequired - Note over Client: Signs ERC-3009 authorization + Note over Client: Signs ERC-3009 or Permit2 Client->>Server: PaymentPayload with signature Server->>Facilitator: verify + settle - Facilitator->>Operator: authorize(paymentInfo, amount, tokenCollector, signature) - Operator->>Escrow: Lock funds in escrow + Facilitator->>Escrow: authorize(paymentInfo, amount, tokenCollector, signature) + Escrow->>Escrow: Lock funds Facilitator-->>Server: Settlement confirmed Server-->>Client: 200 OK + resource - Note over Operator,Escrow: Later: operator releases based on conditions + Note over Facilitator,Escrow: Later: captureAuthorizer acts based on policy alt Successful completion - Operator->>Escrow: release(paymentInfo, amount) + Facilitator->>Escrow: capture(paymentInfo, amount, feeBps, feeReceiver) Escrow->>Receiver: Transfer funds (minus fees) - else Void (full refund in escrow) - Operator->>Escrow: void(paymentInfo) + else Void (full return from escrow) + Facilitator->>Escrow: void(paymentInfo) Escrow->>Client: Return to payer - else Authorization expired - Client->>Escrow: reclaim() - Escrow->>Client: Return to payer (no operator needed) + else Capture deadline passed + Client->>Escrow: reclaim(paymentInfo) + Escrow->>Client: Return to payer (no captureAuthorizer needed) end ``` ### Key Differences -| Aspect | Exact | Escrow | -|--------|-------|--------| -| **Settlement** | Immediate on request | Deferred until conditions met | -| **Payer Protection** | None (payment final) | Refundable until capture | +| Aspect | Exact | authCapture | +|---|---|---| +| **Settlement** | Immediate on request | Via escrow (two-phase) or direct with refund (single-shot) | +| **Payer Protection** | None (payment final) | Refundable in both paths | | **Resource Delivery** | After payment clears | Immediately after authorization | -| **Recourse** | No recourse | Reclaim after expiry, refund via operator | +| **Recourse** | No recourse | Reclaim after capture deadline, refund via captureAuthorizer | | **Fee System** | None | Configurable (min/max bounds, client-signed) | | **Use Case** | Trusted, low-value, instant | High-value, variable cost, disputes | -## Operator Flexibility +## CaptureAuthorizer -The **operator** is the key abstraction. Different implementations enable different payment patterns: +The **captureAuthorizer** is the address authorized to authorize/capture/void/refund/charge a payment. The escrow contract gates those operations on `msg.sender`. 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.). -| Use Case | Operator Behavior | -|----------|-------------------| -| Session billing | Track usage off-chain, capture periodically | -| Time-locked escrow | Release after period expires | -| Dispute resolution | Arbiter decides release vs refund | -| Immediate (exact-like) | Use `charge()` for instant settlement | -| Streaming payments | Time-proportional captures | +| Use Case | CaptureAuthorizer | +|---|---| +| Session billing | EOA that tracks usage off-chain, captures periodically | +| Time-locked escrow | Contract that releases after a period expires | +| Dispute resolution | Arbiter contract that decides release vs refund | +| Immediate (exact-like) | Facilitator with `autoCapture: true` for instant settlement | +| Streaming payments | Contract that performs time-proportional captures | ## Message Format @@ -157,7 +154,7 @@ Server sends this to request payment: { "x402Version": 2, "accepts": [{ - "scheme": "escrow", + "scheme": "authCapture", "network": "eip155:8453", "amount": "1000000", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", @@ -166,21 +163,24 @@ Server sends this to request payment: "extra": { "name": "USDC", "version": "2", - "escrowAddress": "0xe050bB89eD43BB02d71343063824614A7fb80B77", - "operatorAddress": "0xOperatorAddress", - "tokenCollector": "0xcE66Ab399EDA513BD12760b6427C87D6602344a7", - "settlementMethod": "authorize", + "captureAuthorizer": "0xCaptureAuthorizerAddress", + "captureDeadline": 1740758554, + "refundDeadline": 1741276954, "minFeeBps": 0, "maxFeeBps": 1000, - "feeReceiver": "0xOperatorAddress" + "feeRecipient": "0xFeeRecipientAddress", + "autoCapture": false, + "assetTransferMethod": "eip3009" } }] } ``` -### PaymentPayload (Client Response) +A server MAY list multiple `accepts[]` entries with different `assetTransferMethod` values so clients can pick the method matching their token approvals. + +### PaymentPayload — EIP-3009 (default) -Client sends this with signed authorization: +Client sends this with a signed ERC-3009 authorization: ```json { @@ -190,7 +190,7 @@ Client sends this with signed authorization: "method": "GET" }, "accepted": { - "scheme": "escrow", + "scheme": "authCapture", "network": "eip155:8453", "amount": "1000000", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", @@ -201,83 +201,100 @@ Client sends this with signed authorization: "payload": { "authorization": { "from": "0xPayerAddress", - "to": "0xcE66Ab399EDA513BD12760b6427C87D6602344a7", + "to": "0xEIP3009TokenCollectorAddress", "value": "1000000", "validAfter": "0", - "validBefore": "1740672154", + "validBefore": "1740675754", "nonce": "0xf374...3480" }, "signature": "0x2d6a...571c", - "paymentInfo": { - "operator": "0xOperatorAddress", - "receiver": "0xReceiverAddress", - "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", - "maxAmount": "1000000", - "preApprovalExpiry": 1740672154, - "authorizationExpiry": 4294967295, - "refundExpiry": 281474976710655, - "minFeeBps": 0, - "maxFeeBps": 1000, - "feeReceiver": "0xOperatorAddress", - "salt": "0x0000...0001" - } + "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc" } } ``` +### PaymentPayload — Permit2 + +When `extra.assetTransferMethod === "permit2"`, the client signs a Permit2 `PermitTransferFrom`: + +```json +{ + "x402Version": 2, + "resource": { "url": "https://api.example.com/resource", "method": "GET" }, + "accepted": { "scheme": "authCapture", "...": "..." }, + "payload": { + "permit2Authorization": { + "from": "0xPayerAddress", + "permitted": { + "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amount": "1000000" + }, + "spender": "0xPermit2TokenCollectorAddress", + "nonce": "11021048692073456...", + "deadline": "1740675754" + }, + "signature": "0x2d6a...571c", + "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc" + } +} +``` + +The merchant address is bound through the deterministic nonce (no witness struct). + ## Field Reference ### Required Extra Fields | Field | Type | Description | -|-------|------|-------------| -| `name` | string | EIP-712 domain name for the token (e.g., `"USDC"`) | -| `version` | string | EIP-712 domain version (e.g., `"2"`) | -| `escrowAddress` | address | AuthCaptureEscrow contract address on the specified network | -| `operatorAddress` | address | Operator contract address (stored in PaymentInfo.operator) | -| `tokenCollector` | address | ERC-3009 token collector contract address | +|---|---|---| +| `name` | string | EIP-712 token-domain name (e.g., `"USDC"`). Used for ERC-3009 signing only. | +| `version` | string | EIP-712 token-domain version (e.g., `"2"`). | +| `captureAuthorizer` | address | Address authorized to authorize/capture/void/refund/charge. Committed on-chain as `PaymentInfo.operator`. | +| `captureDeadline` | uint48 | Absolute Unix seconds — capture must occur before this. Encoded as `authorizationExpiry`. | +| `refundDeadline` | uint48 | Absolute Unix seconds — refunds allowed until this. Encoded as `refundExpiry`. | +| `feeRecipient` | address | Fee recipient. Set to `address(0)` to let the captureAuthorizer specify any non-zero recipient at capture/charge time. | +| `minFeeBps` | uint16 | Minimum fee in basis points the captureAuthorizer must take. `0` = no minimum. | +| `maxFeeBps` | uint16 | Maximum fee in basis points the captureAuthorizer can take. | ### Optional Extra Fields | Field | Type | Description | Default | -|-------|------|-------------|---------| -| `settlementMethod` | `"authorize"` \| `"charge"` | Settlement path | `"authorize"` | -| `minFeeBps` | uint16 | Minimum fee in basis points | `0` | -| `maxFeeBps` | uint16 | Maximum fee in basis points | `0` | -| `feeReceiver` | address | Address receiving fees | `address(0)` (flexible) | -| `preApprovalExpirySeconds` | uint48 | ERC-3009 signature validity / pre-approval deadline (seconds from now) | `type(uint48).max` | -| `authorizationExpirySeconds` | uint48 | Deadline for capturing escrowed funds (seconds from now) | `type(uint48).max` | -| `refundExpirySeconds` | uint48 | Deadline for refund requests (seconds from now) | `type(uint48).max` | +|---|---|---|---| +| `autoCapture` | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). | `false` | +| `assetTransferMethod` | `"eip3009"` \| `"permit2"` | Which token collector to use. | `"eip3009"` | -**Fee Configuration:** Fees are enforced on-chain in the PaymentInfo struct. The operator contract cannot charge more than `maxFeeBps` or less than `minFeeBps`. If `feeReceiver` is set, the actual fee recipient at capture/charge must match. +**Fee Configuration:** Fees are enforced on-chain in the `PaymentInfo` struct. The escrow rejects captures/charges that fall outside `[minFeeBps, maxFeeBps]`. If `feeRecipient` is non-zero, the actual fee recipient at capture/charge must match. ## Nonce Derivation -The ERC-3009 nonce is deterministically derived from the payment parameters: +The signature nonce is the payer-agnostic `PaymentInfo` hash. Payer is zeroed; everything else is the values that will appear on-chain. ``` -nonce = keccak256(abi.encode(chainId, escrowAddress, paymentInfoHash)) +paymentInfoHash = keccak256(abi.encode(PAYMENT_INFO_TYPEHASH, paymentInfoWithZeroPayer)) +nonce = keccak256(abi.encode(chainId, AUTH_CAPTURE_ESCROW_ADDRESS, paymentInfoHash)) ``` -This ties the off-chain signature to the specific chain, escrow contract, and payment terms — preventing cross-chain or cross-contract replay. The nonce is consumed on-chain at settlement. +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` contains `authorization`, `signature`, and `paymentInfo` fields -2. **Scheme match** — Verify `scheme === "escrow"` -3. **Network match** — Verify network format is `eip155:` and matches between requirements and payload -4. **Extra validation** — Verify `extra` contains required fields (`escrowAddress`, `operatorAddress`, `tokenCollector`) -5. **Time window** — Verify `validBefore > now + 6s` (not expired) and `validAfter <= now` (active) -6. **ERC-3009 signature** — Recover signer from EIP-712 typed data (`ReceiveWithAuthorization` primary type) and verify matches `authorization.from` -7. **Amount** — Verify `authorization.value === requirements.amount` -8. **Recipient match** — Verify `authorization.to === extra.tokenCollector` -9. **Token match** — Verify `paymentInfo.token === requirements.asset` -10. **Receiver match** — Verify `paymentInfo.receiver === requirements.payTo` -11. **Simulate** — Call `operator.authorize(...)` or `operator.charge(...)` via `eth_call` to verify success +1. **Type guard** — Payload matches `Eip3009Payload` or `Permit2Payload` (includes `signature` and `salt`). +2. **Scheme match** — `requirements.scheme === "authCapture"` and `payload.accepted.scheme === "authCapture"`. +3. **Network match** — `payload.accepted.network === requirements.network` and format is `eip155:`. +4. **Extra validation** — All required `extra` fields present. +5. **Method routing** — `extra.assetTransferMethod` (default `"eip3009"`) matches the payload shape. +6. **Deadline ordering** — `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and the payload's `validBefore` (EIP-3009) or `deadline` (Permit2) `<= captureDeadline`. +7. **Time window** — `validBefore` / `deadline > now + 6s` (not expired) and `validAfter <= now` (active, EIP-3009 only). +8. **Spender / collector match** — `authorization.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `permit2Authorization.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). +9. **Token match** — `permit2Authorization.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 amount matches `requirements.amount`. +12. **Nonce match** — Reconstruct `PaymentInfo` from extra + salt + payer + requirements; recompute the 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). +13. **Simulate** — Call `AuthCaptureEscrow.authorize(...)` or `.charge(...)` via `eth_call` to verify success. ### EIP-6492 Support @@ -285,38 +302,32 @@ For smart wallet clients, the signature may be EIP-6492 wrapped (containing depl ## Settlement Logic -Settlement is performed by the facilitator calling the operator: - -1. **Re-verify** the payload (catch expired/invalid payloads before spending gas) -2. **Determine function** — `settlementMethod === "charge" ? "charge" : "authorize"` -3. **Call operator** — `operator.(paymentInfo, amount, tokenCollector, collectorData)` -4. **Wait for receipt** — Confirm transaction success (60s timeout) -5. **Return result** — Transaction hash, network, and payer address - -The operator handles: - -- Calling the token collector to execute `receiveWithAuthorization` with the client's ERC-3009 signature -- Routing funds to escrow (authorize) or directly to receiver (charge) -- Validating fee bounds against the client-signed `PaymentInfo` +1. **Re-verify** the payload (catch 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** — `AuthCaptureEscrow.(paymentInfo, amount, tokenCollector, collectorData)`. +6. **Wait for receipt** — 60s timeout. +7. **Return result** — tx hash, network, payer. ## PaymentInfo Struct -This is the on-chain Solidity struct. The `payer` field is not included in the JSON payload — it is derived from `authorization.from` at settlement time. +This is the on-chain Solidity struct. The `payer` field is not included in the JSON payload — it is derived from the signature recovery at settlement time. Wire-format `extra` uses spec-level field names; the on-chain struct keeps canonical names so the EIP-712 typehash matches the AuthCaptureEscrow contract byte-for-byte. ```solidity struct PaymentInfo { - address operator; // Operator address - address payer; // Derived from authorization.from (not in payload) - address receiver; // Fund recipient (payTo) - address token; // ERC-20 token address - uint120 maxAmount; // Maximum authorized amount - uint48 preApprovalExpiry; // ERC-3009 validBefore / pre-approval deadline - uint48 authorizationExpiry;// Capture deadline (authorize path only) - uint48 refundExpiry; // Refund request deadline - uint16 minFeeBps; // Minimum acceptable fee (basis points) - uint16 maxFeeBps; // Maximum acceptable fee (basis points) - address feeReceiver; // Fee recipient (address(0) = flexible) - uint256 salt; // Client-provided entropy + 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) } ``` @@ -324,11 +335,11 @@ struct PaymentInfo { The contract enforces: `preApprovalExpiry <= authorizationExpiry <= refundExpiry` -| Expiry | Enforced At | Effect | -|:-------|:------------|:-------| -| `preApprovalExpiry` | `authorize()` / `charge()` | Blocks settlement after this time | -| `authorizationExpiry` | `capture()` | Blocks capture; enables `reclaim()` | -| `refundExpiry` | `refund()` | Blocks refund requests | +| 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 | ## Safety Guarantees @@ -336,7 +347,7 @@ The escrow contract enforces invariants on-chain: - Settlement amount is capped by client-signed `maxAmount`. Attempting to exceed the limit reverts the transaction. + Settlement amount is capped by client-signed `maxAmount`. Attempting to exceed the limit reverts. @@ -344,16 +355,16 @@ The escrow contract enforces invariants on-chain: - After `authorizationExpiry`, payer can reclaim escrowed funds directly without operator approval. + After `captureDeadline`, the payer can reclaim escrowed funds directly without captureAuthorizer approval. - Min/max fee bounds in PaymentInfo are client-signed and enforced on-chain. The operator must respect these limits. + Min/max fee bounds in `PaymentInfo` are client-signed and enforced on-chain. The captureAuthorizer must respect these limits. -**Operator Trust Required:** The operator contract controls when and how much to release. Choose operators carefully and understand their release conditions. See [Operators](/contracts/overview#payment-operator) for details. +**CaptureAuthorizer Trust Required:** The captureAuthorizer controls when and how much to release. Choose carefully and understand the release policy. See [Operators](/contracts/overview#payment-operator) for examples. ## Error Codes @@ -361,32 +372,36 @@ The escrow contract enforces invariants on-chain: ### Verification Errors | Error Code | Description | -|:-----------|:------------| -| `invalid_payload_format` | Payload missing `authorization`, `signature`, or `paymentInfo` | -| `unsupported_scheme` | Scheme is not `escrow` | -| `network_mismatch` | Payload network does not match requirements | -| `invalid_network` | Network format is not `eip155:` | -| `invalid_escrow_extra` | Missing required extra fields (`escrowAddress`, `operatorAddress`, `tokenCollector`) | -| `authorization_expired` | `validBefore <= now + 6s` | -| `authorization_not_yet_valid` | `validAfter > now` | -| `invalid_escrow_signature` | ERC-3009 signature verification failed | -| `amount_mismatch` | `authorization.value !== requirements.amount` | -| `token_collector_mismatch` | `authorization.to !== extra.tokenCollector` | -| `token_mismatch` | `paymentInfo.token !== requirements.asset` | -| `receiver_mismatch` | `paymentInfo.receiver !== requirements.payTo` | -| `insufficient_balance` | Payer balance is less than required amount | -| `simulation_failed` | Settlement simulation via `eth_call` failed | +|---|---| +| `invalid_payload_format` | Payload doesn't match `Eip3009Payload` or `Permit2Payload`. | +| `unsupported_scheme` | Scheme is not `authCapture`. | +| `network_mismatch` | Payload network doesn't match requirements. | +| `invalid_network` | Network format is not `eip155:`. | +| `invalid_authCapture_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_authCapture_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. | ### Settlement Errors | Error Code | Description | -|:-----------|:------------| -| `verification_failed` | Re-verification before settlement failed | -| `transaction_reverted` | On-chain transaction reverted after confirmation | +|---|---| +| `verification_failed` | Re-verification before settlement failed. | +| `transaction_reverted` | On-chain transaction reverted after confirmation. | ## vs Exact Scheme -The `escrow` scheme adds an authorization step before settlement. For simple immediate payments where trust is not a concern, the `exact` scheme remains more efficient. +The `authCapture` scheme adds an authorization step before settlement (or refundability for single-shot). For simple immediate payments where trust and refundability aren't concerns, the `exact` scheme remains more efficient. See [Comparison](/x402-integration/comparison) for detailed trade-offs. @@ -398,25 +413,22 @@ See [Comparison](/x402-integration/comparison) for detailed trade-offs. - Compare escrow vs exact schemes in detail. + Compare authCapture vs exact schemes in detail. - Learn about escrow and operator contracts. + Learn about the escrow and captureAuthorizer contracts. - Build your first escrow-based payment flow. + Build your first authCapture payment flow. ## References -- [Escrow Scheme Proposal (Issue #1011)](https://github.com/coinbase/x402/issues/1011) -- [Escrow Scheme Specification (PR #1425)](https://github.com/coinbase/x402/pull/1425) - [Commerce Payments Protocol](https://blog.base.dev/commerce-payments-protocol) - [AuthCaptureEscrow Contract](https://github.com/base/commerce-payments) - [EIP-3009: Transfer With Authorization](https://eips.ethereum.org/EIPS/eip-3009) -- [x402r Operator Contracts](https://github.com/BackTrackCo/x402r-contracts) -- [x402r Escrow Scheme](https://github.com/BackTrackCo/x402r-scheme) -- [x402 Protocol](https://github.com/coinbase/x402) +- [Uniswap Permit2](https://docs.uniswap.org/contracts/permit2/overview) +- [x402r authCapture Scheme Reference Implementation](https://github.com/BackTrackCo/x402r-scheme) diff --git a/x402-integration/overview.mdx b/x402-integration/overview.mdx index 07863cc..409c04d 100644 --- a/x402-integration/overview.mdx +++ b/x402-integration/overview.mdx @@ -88,7 +88,7 @@ Client authorizes $10 once → Server tracks usage → Periodic batch capture ## How x402r Extends X402 -x402r provides the **escrow scheme implementation** for x402: +x402r provides the **authCapture scheme implementation** for x402: 1. **Base Commerce Payments Integration** - Audited escrow contracts from Base @@ -205,8 +205,8 @@ Smart contract that controls capture/void logic. Different operators enable diff ## Next Steps - - Complete technical specification for the escrow payment scheme. + + Complete technical specification for the authCapture payment scheme. From c5c8d55953162b67fa77aa29b42705b8e419416f Mon Sep 17 00:00:00 2001 From: A1igator Date: Mon, 11 May 2026 19:34:39 -0700 Subject: [PATCH 17/37] docs: rename escrow-scheme route to auth-capture-scheme Match the route to the scheme name. Adds a Mintlify redirect from /x402-integration/escrow-scheme so existing inbound links keep working. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs.json | 10 ++++++++-- roadmap.mdx | 2 +- .../{escrow-scheme.mdx => auth-capture-scheme.mdx} | 0 x402-integration/comparison.mdx | 2 +- x402-integration/overview.mdx | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) rename x402-integration/{escrow-scheme.mdx => auth-capture-scheme.mdx} (100%) diff --git a/docs.json b/docs.json index bda342a..f667699 100644 --- a/docs.json +++ b/docs.json @@ -18,7 +18,7 @@ "pages": [ "index", "x402-integration/overview", - "x402-integration/escrow-scheme", + "x402-integration/auth-capture-scheme", "x402-integration/comparison" ] }, @@ -162,5 +162,11 @@ "x": "https://x.com/x402rorg", "github": "https://github.com/BackTrackCo" } - } + }, + "redirects": [ + { + "source": "/x402-integration/escrow-scheme", + "destination": "/x402-integration/auth-capture-scheme" + } + ] } diff --git a/roadmap.mdx b/roadmap.mdx index 8fceec6..8f15ea2 100644 --- a/roadmap.mdx +++ b/roadmap.mdx @@ -25,7 +25,7 @@ icon: "map" ### authCapture Scheme Spec (In Progress) - Reference implementation: [x402r-scheme](https://github.com/BackTrackCo/x402r-scheme) -- See the [authCapture Scheme Specification](/x402-integration/escrow-scheme) for the wire format and verification logic +- See the [authCapture Scheme Specification](/x402-integration/auth-capture-scheme) for the wire format and verification logic ### Developer Experience (In Progress) diff --git a/x402-integration/escrow-scheme.mdx b/x402-integration/auth-capture-scheme.mdx similarity index 100% rename from x402-integration/escrow-scheme.mdx rename to x402-integration/auth-capture-scheme.mdx diff --git a/x402-integration/comparison.mdx b/x402-integration/comparison.mdx index 02cf8a4..cd356b8 100644 --- a/x402-integration/comparison.mdx +++ b/x402-integration/comparison.mdx @@ -358,7 +358,7 @@ flowchart TD Learn about x402 and why escrow matters. - + Complete technical specification. diff --git a/x402-integration/overview.mdx b/x402-integration/overview.mdx index 409c04d..7651a53 100644 --- a/x402-integration/overview.mdx +++ b/x402-integration/overview.mdx @@ -205,7 +205,7 @@ Smart contract that controls capture/void logic. Different operators enable diff ## Next Steps - + Complete technical specification for the authCapture payment scheme. From dc4e7c02e727bd6c817e7144e7066276175fdd86 Mon Sep 17 00:00:00 2001 From: A1igator Date: Mon, 11 May 2026 19:43:07 -0700 Subject: [PATCH 18/37] docs: split authCapture into its own nav group; strip em dashes Move x402-integration/auth-capture-scheme out of "Getting Started" into a dedicated "authCapture Scheme" nav group in the Protocol tab. Also strip em dashes from the recently-updated docs to match the project style guide (CLAUDE.md: never use em dashes). Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/audits.mdx | 12 ++--- contracts/overview.mdx | 2 +- contracts/periphery/auth-capture-escrow.mdx | 2 +- docs.json | 7 ++- roadmap.mdx | 16 +++--- x402-integration/auth-capture-scheme.mdx | 56 ++++++++++----------- 6 files changed, 50 insertions(+), 45 deletions(-) diff --git a/contracts/audits.mdx b/contracts/audits.mdx index 557d36f..0eb9e8b 100644 --- a/contracts/audits.mdx +++ b/contracts/audits.mdx @@ -34,7 +34,7 @@ These audits cover the core escrow lifecycle: authorize, capture, void, reclaim, ### What This Means -- The audited escrow layer provides a strong foundation — fund custody, token transfers, and payment state transitions have been professionally reviewed +- The audited escrow layer provides a strong foundation, fund custody, token transfers, and payment state transitions have been professionally reviewed - The condition/recorder plugin system is stateless or minimal-state by design, reducing attack surface @@ -47,7 +47,7 @@ Even without a formal audit, the x402r contracts follow established security pat - **CEI (Checks-Effects-Interactions)** ordering in all state-changing functions - **ReentrancyGuardTransient** (EIP-1153) on all external entry points -- **Immutable configuration** — operator conditions and fee calculators cannot be changed after deployment +- **Immutable configuration**: operator conditions and fee calculators cannot be changed after deployment - **7-day timelock** on protocol fee changes via ProtocolFeeConfig - **2-step ownership transfers** via Solady's Ownable - **Comprehensive Forge test suite** covering core flows and edge cases @@ -56,10 +56,10 @@ Even without a formal audit, the x402r contracts follow established security pat We plan to pursue third-party audits as the contract architecture and use cases stabilize. Priority order: -1. **PaymentOperator** — condition dispatch, fee calculation, fee locking, distribution -2. **Plugin system** — conditions, recorders, combinators, and their factories -3. **EscrowPeriod + Freeze** — time-lock enforcement and freeze state management -4. **RefundRequest** — request lifecycle and access control +1. **PaymentOperator**: condition dispatch, fee calculation, fee locking, distribution +2. **Plugin system**: conditions, recorders, combinators, and their factories +3. **EscrowPeriod + Freeze**: time-lock enforcement and freeze state management +4. **RefundRequest**: request lifecycle and access control We'll publish audit reports publicly once completed. diff --git a/contracts/overview.mdx b/contracts/overview.mdx index 2104ef8..75292a4 100644 --- a/contracts/overview.mdx +++ b/contracts/overview.mdx @@ -115,7 +115,7 @@ x402r extends commerce-payments with flexible payment capabilities: Plus factories for: StaticFeeCalculator, StaticAddressCondition, AndCondition, OrCondition, NotCondition, RecorderCombinator. -All factories use **unified CREATE3 addresses** — same address on every supported chain. +All factories use **unified CREATE3 addresses**: same address on every supported chain. **Benefits:** - Predictable addresses for off-chain address generation diff --git a/contracts/periphery/auth-capture-escrow.mdx b/contracts/periphery/auth-capture-escrow.mdx index 95361b7..57940ae 100644 --- a/contracts/periphery/auth-capture-escrow.mdx +++ b/contracts/periphery/auth-capture-escrow.mdx @@ -1,6 +1,6 @@ --- title: "Commerce Payments" -description: "AuthCaptureEscrow and ERC3009PaymentCollector — the base layer from commerce-payments" +description: "AuthCaptureEscrow and ERC3009PaymentCollector, the base layer from commerce-payments" icon: "vault" --- diff --git a/docs.json b/docs.json index f667699..38aa139 100644 --- a/docs.json +++ b/docs.json @@ -18,10 +18,15 @@ "pages": [ "index", "x402-integration/overview", - "x402-integration/auth-capture-scheme", "x402-integration/comparison" ] }, + { + "group": "authCapture Scheme", + "pages": [ + "x402-integration/auth-capture-scheme" + ] + }, { "group": "Roadmap", "pages": [ diff --git a/roadmap.mdx b/roadmap.mdx index 8f15ea2..ba8f0e0 100644 --- a/roadmap.mdx +++ b/roadmap.mdx @@ -62,17 +62,17 @@ icon: "map" ### Phase 2: Core SDK (Completed) -- `@x402r/client` — Refund requests, freeze, escrow period queries, subscriptions -- `@x402r/merchant` — Release, charge, refundInEscrow, refundPostEscrow, refund handling -- `@x402r/arbiter` — Decision submission, batch operations, registry, AI hooks -- `@x402r/helpers` — `refundable()` helper for payment options -- `@x402r/core` — Types, ABIs, config, deploy utilities +- `@x402r/client`, Refund requests, freeze, escrow period queries, subscriptions +- `@x402r/merchant`, Release, charge, refundInEscrow, refundPostEscrow, refund handling +- `@x402r/arbiter`, Decision submission, batch operations, registry, AI hooks +- `@x402r/helpers`, `refundable()` helper for payment options +- `@x402r/core`, Types, ABIs, config, deploy utilities - 310+ tests across all packages ### Phase 3: Client UX (Upcoming) -- Pre-payment info extraction (`getOperatorInfo` — discover arbiter, escrow period from operator address) -- Combined freeze + refund (`freezeAndRequestRefund` — single call) +- Pre-payment info extraction (`getOperatorInfo`, discover arbiter, escrow period from operator address) +- Combined freeze + refund (`freezeAndRequestRefund`, single call) - Condition awareness for clients ### Phase 4: Subgraph Integration (Upcoming) @@ -93,7 +93,7 @@ icon: "map" ## Contract Status -All contracts use **unified CREATE3 addresses** — same address on every supported chain (11 chains: Base, Ethereum, Polygon, Arbitrum, Optimism, Celo, Avalanche, Monad, Linea, Base Sepolia, Ethereum Sepolia). +All contracts use **unified CREATE3 addresses**: same address on every supported chain (11 chains: Base, Ethereum, Polygon, Arbitrum, Optimism, Celo, Avalanche, Monad, Linea, Base Sepolia, Ethereum Sepolia). | Contract | Status | |----------|--------| diff --git a/x402-integration/auth-capture-scheme.mdx b/x402-integration/auth-capture-scheme.mdx index 9117757..482859d 100644 --- a/x402-integration/auth-capture-scheme.mdx +++ b/x402-integration/auth-capture-scheme.mdx @@ -6,7 +6,7 @@ icon: "file-contract" ## Overview -The **`authCapture` scheme** for x402 v2 uses the audited [Commerce Payments Protocol](https://github.com/base/commerce-payments) (`AuthCaptureEscrow` + token collectors) directly — no fork. The client signs a single signature (ERC-3009 or Permit2). The facilitator submits it, either locking funds in escrow for later capture (two-phase) or sending them directly to the receiver with refund capability (single-shot). +The **`authCapture` scheme** for x402 v2 uses the audited [Commerce Payments Protocol](https://github.com/base/commerce-payments) (`AuthCaptureEscrow` + token collectors) directly, no fork. The client signs a single signature (ERC-3009 or Permit2). The facilitator submits it, either locking funds in escrow for later capture (two-phase) or sending them directly to the receiver with refund capability (single-shot). Unlike `exact`, which has no built-in mechanism for returning funds, `authCapture` supports returning funds to the client through void, refund, and reclaim. @@ -27,7 +27,7 @@ AUTHORIZE -> RESOURCE DELIVERED -> CAPTURE / VOID -> (REFUND) - Client authorization is submitted — funds locked in escrow via `AuthCaptureEscrow.authorize()`. The token collector executes the client's signature (ERC-3009 `receiveWithAuthorization` or Permit2 `permitTransferFrom`) to pull tokens into escrow. + Client authorization is submitted, funds locked in escrow via `AuthCaptureEscrow.authorize()`. The token collector executes the client's signature (ERC-3009 `receiveWithAuthorization` or Permit2 `permitTransferFrom`) to pull tokens into escrow. @@ -55,7 +55,7 @@ CHARGE -> RESOURCE DELIVERED -> (REFUND) - Client authorization is submitted — funds sent directly to receiver via `AuthCaptureEscrow.charge()`. No escrow hold. + Client authorization is submitted, funds sent directly to receiver via `AuthCaptureEscrow.charge()`. No escrow hold. @@ -67,7 +67,7 @@ CHARGE -> RESOURCE DELIVERED -> (REFUND) -No capture, void, or reclaim — funds are never held in escrow. +No capture, void, or reclaim, funds are never held in escrow. ## Visual Flow @@ -178,7 +178,7 @@ Server sends this to request payment: A server MAY list multiple `accepts[]` entries with different `assetTransferMethod` values so clients can pick the method matching their token approvals. -### PaymentPayload — EIP-3009 (default) +### PaymentPayload: EIP-3009 (default) Client sends this with a signed ERC-3009 authorization: @@ -213,7 +213,7 @@ Client sends this with a signed ERC-3009 authorization: } ``` -### PaymentPayload — Permit2 +### PaymentPayload: Permit2 When `extra.assetTransferMethod === "permit2"`, the client signs a Permit2 `PermitTransferFrom`: @@ -250,8 +250,8 @@ The merchant address is bound through the deterministic nonce (no witness struct | `name` | string | EIP-712 token-domain name (e.g., `"USDC"`). Used for ERC-3009 signing only. | | `version` | string | EIP-712 token-domain version (e.g., `"2"`). | | `captureAuthorizer` | address | Address authorized to authorize/capture/void/refund/charge. Committed on-chain as `PaymentInfo.operator`. | -| `captureDeadline` | uint48 | Absolute Unix seconds — capture must occur before this. Encoded as `authorizationExpiry`. | -| `refundDeadline` | uint48 | Absolute Unix seconds — refunds allowed until this. Encoded as `refundExpiry`. | +| `captureDeadline` | uint48 | Absolute Unix seconds: capture must occur before this. Encoded as `authorizationExpiry`. | +| `refundDeadline` | uint48 | Absolute Unix seconds: refunds allowed until this. Encoded as `refundExpiry`. | | `feeRecipient` | address | Fee recipient. Set to `address(0)` to let the captureAuthorizer specify any non-zero recipient at capture/charge time. | | `minFeeBps` | uint16 | Minimum fee in basis points the captureAuthorizer must take. `0` = no minimum. | | `maxFeeBps` | uint16 | Maximum fee in basis points the captureAuthorizer can take. | @@ -282,19 +282,19 @@ Freshness is enforced by `salt`: each signing call generates a fresh `bytes32` s The facilitator performs these checks in order: -1. **Type guard** — Payload matches `Eip3009Payload` or `Permit2Payload` (includes `signature` and `salt`). -2. **Scheme match** — `requirements.scheme === "authCapture"` and `payload.accepted.scheme === "authCapture"`. -3. **Network match** — `payload.accepted.network === requirements.network` and format is `eip155:`. -4. **Extra validation** — All required `extra` fields present. -5. **Method routing** — `extra.assetTransferMethod` (default `"eip3009"`) matches the payload shape. -6. **Deadline ordering** — `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and the payload's `validBefore` (EIP-3009) or `deadline` (Permit2) `<= captureDeadline`. -7. **Time window** — `validBefore` / `deadline > now + 6s` (not expired) and `validAfter <= now` (active, EIP-3009 only). -8. **Spender / collector match** — `authorization.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `permit2Authorization.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). -9. **Token match** — `permit2Authorization.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 amount matches `requirements.amount`. -12. **Nonce match** — Reconstruct `PaymentInfo` from extra + salt + payer + requirements; recompute the 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). -13. **Simulate** — Call `AuthCaptureEscrow.authorize(...)` or `.charge(...)` via `eth_call` to verify success. +1. **Type guard**: Payload matches `Eip3009Payload` or `Permit2Payload` (includes `signature` and `salt`). +2. **Scheme match**: `requirements.scheme === "authCapture"` and `payload.accepted.scheme === "authCapture"`. +3. **Network match**: `payload.accepted.network === requirements.network` and format is `eip155:`. +4. **Extra validation**: All required `extra` fields present. +5. **Method routing**: `extra.assetTransferMethod` (default `"eip3009"`) matches the payload shape. +6. **Deadline ordering**: `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and the payload's `validBefore` (EIP-3009) or `deadline` (Permit2) `<= captureDeadline`. +7. **Time window**: `validBefore` / `deadline > now + 6s` (not expired) and `validAfter <= now` (active, EIP-3009 only). +8. **Spender / collector match**: `authorization.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `permit2Authorization.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). +9. **Token match**: `permit2Authorization.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 amount matches `requirements.amount`. +12. **Nonce match**: Reconstruct `PaymentInfo` from extra + salt + payer + requirements; recompute the 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). +13. **Simulate**: Call `AuthCaptureEscrow.authorize(...)` or `.charge(...)` via `eth_call` to verify success. ### EIP-6492 Support @@ -303,16 +303,16 @@ For smart wallet clients, the signature may be EIP-6492 wrapped (containing depl ## Settlement Logic 1. **Re-verify** the payload (catch 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** — `AuthCaptureEscrow.(paymentInfo, amount, tokenCollector, collectorData)`. -6. **Wait for receipt** — 60s timeout. -7. **Return result** — tx hash, network, payer. +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**: `AuthCaptureEscrow.(paymentInfo, amount, tokenCollector, collectorData)`. +6. **Wait for receipt**: 60s timeout. +7. **Return result**: tx hash, network, payer. ## PaymentInfo Struct -This is the on-chain Solidity struct. The `payer` field is not included in the JSON payload — it is derived from the signature recovery at settlement time. Wire-format `extra` uses spec-level field names; the on-chain struct keeps canonical names so the EIP-712 typehash matches the AuthCaptureEscrow contract byte-for-byte. +This is the on-chain Solidity struct. The `payer` field is not included in the JSON payload, it is derived from the signature recovery at settlement time. Wire-format `extra` uses spec-level field names; the on-chain struct keeps canonical names so the EIP-712 typehash matches the AuthCaptureEscrow contract byte-for-byte. ```solidity struct PaymentInfo { From a0daa5ee301a4e57175a40963811e300cb497506 Mon Sep 17 00:00:00 2001 From: A1igator Date: Mon, 11 May 2026 19:55:44 -0700 Subject: [PATCH 19/37] docs: drop in-escrow/post-escrow terminology; use commerce-payments names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the refundInEscrow/refundPostEscrow API everywhere to match the canonical base/commerce-payments naming (void/refund) and the current PaymentOperator interface. The "in-escrow refund vs post-escrow refund" framing was a stale x402r-only distinction; commerce-payments has just void (pre-capture) and refund (post-capture). Method/symbol renames: - refundInEscrow() → voidPayment() (SDK) / void() (Solidity) - refundPostEscrow() → refund() - refundInEscrow{Condition,Recorder} → voidPreActionCondition / voidPostActionHook - refundPostEscrow{Condition,Recorder} → refundPreActionCondition / refundPostActionHook - REFUND_IN_ESCROW_* → VOID_PRE_ACTION_CONDITION / VOID_POST_ACTION_HOOK - REFUND_POST_ESCROW_* → REFUND_PRE_ACTION_CONDITION / REFUND_POST_ACTION_HOOK - RefundInEscrowExecuted → VoidExecuted - RefundPostEscrowExecuted → RefundExecuted - approvePostEscrowRefund(paymentInfo, amount) → approveRefundAllowance(token, amount) - getPostEscrowRefundAllowance(paymentInfo) → getRefundAllowance(token, owner) Signature fixes: - voidPayment(paymentInfo) takes no amount; void is full-only in the canonical contract. Updated 19 SDK examples and reworded the partial- refund pattern to use capture-then-refund (or capture-and-let-expire), since the old refundInEscrow(paymentInfo, amount) suggested partial voids that are no longer supported. - approveRefundAllowance / getRefundAllowance switched from paymentInfo- based to token-based signatures; updated example call shapes. Also stripped em dashes from every newly-touched file per the docs/CLAUDE.md style rule. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/architecture.mdx | 16 ++-- contracts/conditions/always-true.mdx | 6 +- contracts/conditions/overview.mdx | 24 ++--- contracts/conditions/payer.mdx | 8 +- contracts/conditions/receiver.mdx | 6 +- contracts/examples.mdx | 88 +++++++++---------- contracts/factories.mdx | 34 +++---- contracts/gas-costs.mdx | 32 +++---- contracts/overview.mdx | 6 +- contracts/payment-operator.mdx | 36 ++++---- contracts/periphery/auth-capture-escrow.mdx | 6 +- contracts/periphery/overview.mdx | 4 +- .../periphery/receiver-refund-collector.mdx | 8 +- contracts/periphery/refund-request.mdx | 12 +-- contracts/recorders/overview.mdx | 16 ++-- roadmap.mdx | 4 +- sdk/arbiter.mdx | 4 +- sdk/arbiter/ai-integration.mdx | 2 +- sdk/arbiter/batch-operations.mdx | 4 +- sdk/arbiter/decision-submission.mdx | 12 +-- sdk/arbiter/quickstart.mdx | 14 +-- sdk/arbiter/subscriptions.mdx | 4 +- sdk/client/quickstart.mdx | 16 ++-- sdk/client/subscriptions.mdx | 4 +- sdk/concepts.mdx | 18 ++-- sdk/delivery-arbiter.mdx | 4 +- sdk/deploy-operator.mdx | 42 ++++----- sdk/merchant.mdx | 4 +- sdk/merchant/payment-operations.mdx | 42 ++++----- sdk/merchant/quickstart.mdx | 29 +++--- sdk/merchant/refund-handling.mdx | 12 +-- sdk/merchant/subscriptions.mdx | 6 +- 32 files changed, 260 insertions(+), 263 deletions(-) diff --git a/contracts/architecture.mdx b/contracts/architecture.mdx index c5f5458..a74c9eb 100644 --- a/contracts/architecture.mdx +++ b/contracts/architecture.mdx @@ -24,8 +24,8 @@ flowchart TB Auth[authorize] Charge[charge] Release[release] - RefundIE[refundInEscrow] - RefundPE[refundPostEscrow] + RefundIE[voidPayment] + RefundPE[refund] end subgraph Plugins["Conditions & Recorders"] @@ -86,10 +86,10 @@ For additional visual diagrams, see the [x402r-contracts repository](https://git 2. **RefundRequest** creates request with status `Pending` 3. **Designated address** (e.g., arbiter, DAO multisig) reviews dispute 4. **Designated address** calls `refundRequest.updateStatus(paymentInfo, nonce, Approved)` -5. **Designated address** calls `operator.refundInEscrow(paymentInfo, amount)` -6. **Operator** checks `REFUND_IN_ESCROW_CONDITION` (configured per operator) +5. **Designated address** calls `operator.void(paymentInfo, amount)` +6. **Operator** checks `VOID_PRE_ACTION_CONDITION` (configured per operator) 7. **Operator** calls `escrow.partialVoid()` to return funds to payer -8. **Operator** calls `REFUND_IN_ESCROW_RECORDER` +8. **Operator** calls `VOID_POST_ACTION_HOOK` 9. Funds transferred back to payer @@ -224,7 +224,7 @@ Fees accumulate in the operator and are distributed via `distributeFees(token)`. |------|-------------|--------------| | **Payer** | `authorize()`, `freeze()`, `unfreeze()`, `requestRefund()`, `cancelRefundRequest()` | Can only act on own payments | | **Receiver** | `release()` (if condition allows), `charge()`, `requestRefund()` | Can only act on payments where they are receiver | -| **Designated Address** | Any action per conditions (e.g., `refundInEscrow()`, `release()`, `updateStatus()`) | Defined by StaticAddressCondition (arbiter, DAO, service provider, etc.) | +| **Designated Address** | Any action per conditions (e.g., `voidPayment()`, `release()`, `updateStatus()`) | Defined by StaticAddressCondition (arbiter, DAO, service provider, etc.) | | **Protocol Owner** | `queueCalculator()`, `executeCalculator()`, `queueRecipient()`, `executeRecipient()` | 7-day timelock on ProtocolFeeConfig changes | @@ -292,8 +292,8 @@ Ownership transfers use Solady's Ownable pattern: event AuthorizationCreated(bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount, uint256 timestamp); event ChargeExecuted(bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount, uint256 timestamp); event ReleaseExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, uint256 amount, uint256 timestamp); -event RefundInEscrowExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, address indexed payer, uint256 amount); -event RefundPostEscrowExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, address indexed payer, uint256 amount); +event VoidExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, address indexed payer, uint256 amount); +event RefundExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, address indexed payer, uint256 amount); // Fee distribution event FeesDistributed(address indexed token, uint256 protocolAmount, uint256 arbiterAmount); diff --git a/contracts/conditions/always-true.mdx b/contracts/conditions/always-true.mdx index 577ea67..021e7ce 100644 --- a/contracts/conditions/always-true.mdx +++ b/contracts/conditions/always-true.mdx @@ -6,7 +6,7 @@ icon: "check" ## Overview -AlwaysTrueCondition allows anyone to call the action — no restrictions applied. +AlwaysTrueCondition allows anyone to call the action, no restrictions applied. **Type:** Singleton (deployed once, reused by all operators) @@ -31,7 +31,7 @@ function check(PaymentInfo calldata payment, uint256, address caller) | `AUTHORIZE_CONDITION` | Let anyone create payments (common for marketplace/e-commerce) | -**Use with caution for release/refund slots.** Setting `RELEASE_CONDITION` or `REFUND_IN_ESCROW_CONDITION` to AlwaysTrueCondition means anyone can release or refund funds. This is functionally equivalent to leaving the slot as `address(0)` (the default behavior), but makes the intent explicit. +**Use with caution for release/refund slots.** Setting `RELEASE_CONDITION` or `VOID_PRE_ACTION_CONDITION` to AlwaysTrueCondition means anyone can release or refund funds. This is functionally equivalent to leaving the slot as `address(0)` (the default behavior), but makes the intent explicit. ## AlwaysTrueCondition vs `address(0)` @@ -48,7 +48,7 @@ Use `address(0)` when you simply don't need a condition. Use AlwaysTrueCondition ## Gas -**Cost:** Minimal — `pure` function returning a constant. +**Cost:** Minimal, `pure` function returning a constant. ## Next Steps diff --git a/contracts/conditions/overview.mdx b/contracts/conditions/overview.mdx index bd8baed..0c8fcaa 100644 --- a/contracts/conditions/overview.mdx +++ b/contracts/conditions/overview.mdx @@ -6,15 +6,15 @@ icon: "filter" ## What Are Conditions? -Conditions are pluggable contracts that control who can perform actions on a PaymentOperator. Each operator has **5 condition slots** — one per action: +Conditions are pluggable contracts that control who can perform actions on a PaymentOperator. Each operator has **5 condition slots**: one per action: | Slot | Controls | |------|----------| | `AUTHORIZE_CONDITION` | Who can authorize payments | | `CHARGE_CONDITION` | Who can charge partial amounts | | `RELEASE_CONDITION` | Who can release from escrow | -| `REFUND_IN_ESCROW_CONDITION` | Who can refund during escrow | -| `REFUND_POST_ESCROW_CONDITION` | Who can refund after release | +| `VOID_PRE_ACTION_CONDITION` | Who can refund during escrow | +| `REFUND_PRE_ACTION_CONDITION` | Who can refund after release | ## ICondition Interface @@ -29,15 +29,15 @@ interface ICondition { ``` **Parameters:** -- `paymentInfo` — The payment information struct -- `amount` — The amount involved in the action (0 for authorization-only checks like refund request status updates) -- `caller` — The address attempting the action +- `paymentInfo`, The payment information struct +- `amount`, The amount involved in the action (0 for authorization-only checks like refund request status updates) +- `caller`, The address attempting the action **Return:** `true` if the caller is authorized, `false` otherwise. ## Default Behavior -**Condition slot = `address(0)`** — always returns `true` (allow). The action is unrestricted. +**Condition slot = `address(0)`**: always returns `true` (allow). The action is unrestricted. This means you only need to set conditions for slots you want to restrict. Leave the rest as `address(0)`. @@ -52,12 +52,12 @@ This means you only need to set conditions for slots you want to restrict. Leave ## Security Rules -**Conditions MUST NOT revert.** Return `false` to deny — never `revert`. The operator converts `false` into a `ConditionNotMet` error. +**Conditions MUST NOT revert.** Return `false` to deny, never `revert`. The operator converts `false` into a `ConditionNotMet` error. - Conditions should be `view` or `pure` to prevent reentrancy attacks - Never make external state-changing calls inside a condition -- Test thoroughly — edge cases in authorization logic can lead to locked funds +- Test thoroughly, edge cases in authorization logic can lead to locked funds ## Configuration Patterns @@ -91,8 +91,8 @@ config = { authorizeCondition: ARBITER_CONDITION, // Only arbiter chargeCondition: ARBITER_CONDITION, // Only arbiter releaseCondition: ARBITER_CONDITION, // Only arbiter - refundInEscrowCondition: ARBITER_CONDITION, // Only arbiter - refundPostEscrowCondition: ARBITER_CONDITION, + voidPreActionCondition: ARBITER_CONDITION, // Only arbiter + refundPreActionCondition: ARBITER_CONDITION, // ... }; ``` @@ -103,7 +103,7 @@ For complete configuration examples, see the [Examples](/contracts/examples) pag ### Singleton Reuse -Singleton conditions are deployed once and reused by all operators. Reference the existing addresses — don't deploy new instances: +Singleton conditions are deployed once and reused by all operators. Reference the existing addresses, don't deploy new instances: ```typescript // Good: Reference the singleton address diff --git a/contracts/conditions/payer.mdx b/contracts/conditions/payer.mdx index 51ed10f..63d663c 100644 --- a/contracts/conditions/payer.mdx +++ b/contracts/conditions/payer.mdx @@ -24,15 +24,15 @@ function check(PaymentInfo calldata payment, uint256, address caller) } ``` -The condition compares `caller` against `payment.payer` — pure computation with no storage reads. +The condition compares `caller` against `payment.payer`, pure computation with no storage reads. ## When to Use | Slot | Use Case | |------|----------| | `AUTHORIZE_CONDITION` | Let payer create payments (subscriptions, invoices) | -| `REFUND_IN_ESCROW_CONDITION` | Let payer request refunds during escrow | -| `REFUND_POST_ESCROW_CONDITION` | Let payer cancel streams | +| `VOID_PRE_ACTION_CONDITION` | Let payer request refunds during escrow | +| `REFUND_PRE_ACTION_CONDITION` | Let payer cancel streams | Typically paired with [ReceiverCondition](/contracts/conditions/receiver) for release, since payers shouldn't release their own funds in most configurations. @@ -40,7 +40,7 @@ Typically paired with [ReceiverCondition](/contracts/conditions/receiver) for re ## Gas -**Cost:** Minimal — `pure` function with no storage reads. +**Cost:** Minimal, `pure` function with no storage reads. ## Next Steps diff --git a/contracts/conditions/receiver.mdx b/contracts/conditions/receiver.mdx index 3506bd7..a286435 100644 --- a/contracts/conditions/receiver.mdx +++ b/contracts/conditions/receiver.mdx @@ -24,7 +24,7 @@ function check(PaymentInfo calldata payment, uint256, address caller) } ``` -The condition compares `caller` against `payment.receiver` — pure computation with no storage reads. +The condition compares `caller` against `payment.receiver`, pure computation with no storage reads. ## When to Use @@ -32,7 +32,7 @@ The condition compares `caller` against `payment.receiver` — pure computation |------|----------| | `RELEASE_CONDITION` | Let receiver release funds after escrow | | `CHARGE_CONDITION` | Let receiver charge partial amounts | -| `REFUND_IN_ESCROW_CONDITION` | Let receiver voluntarily refund | +| `VOID_PRE_ACTION_CONDITION` | Let receiver voluntarily refund | For release, ReceiverCondition is often composed with [EscrowPeriod](/contracts/conditions/escrow-period) via [AndCondition](/contracts/conditions/combinators) to ensure the escrow window has passed before the receiver can release. @@ -40,7 +40,7 @@ For release, ReceiverCondition is often composed with [EscrowPeriod](/contracts/ ## Gas -**Cost:** Minimal — `pure` function with no storage reads. +**Cost:** Minimal, `pure` function with no storage reads. ## Next Steps diff --git a/contracts/examples.mdx b/contracts/examples.mdx index 136798a..efd2bf3 100644 --- a/contracts/examples.mdx +++ b/contracts/examples.mdx @@ -81,10 +81,10 @@ The configuration examples below use simplified pseudo-code (e.g., `new StaticAd chargeRecorder: '0x0000000000000000000000000000000000000000', releaseCondition: releaseCondition, releaseRecorder: '0x0000000000000000000000000000000000000000', - refundInEscrowCondition: arbiterCondition.address, - refundInEscrowRecorder: '0x0000000000000000000000000000000000000000', - refundPostEscrowCondition: arbiterCondition.address, - refundPostEscrowRecorder: '0x0000000000000000000000000000000000000000' + voidPreActionCondition: arbiterCondition.address, + voidPostActionHook: '0x0000000000000000000000000000000000000000', + refundPreActionCondition: arbiterCondition.address, + refundPostActionHook: '0x0000000000000000000000000000000000000000' }; const operator = await operatorFactory.deployOperator(config); @@ -143,10 +143,10 @@ const config = { chargeRecorder: '0x0000000000000000000000000000000000000000', releaseCondition: RECEIVER_CONDITION, // Fallback to release remaining releaseRecorder: '0x0000000000000000000000000000000000000000', - refundInEscrowCondition: arbiterCondition.address, - refundInEscrowRecorder: '0x0000000000000000000000000000000000000000', - refundPostEscrowCondition: arbiterCondition.address, - refundPostEscrowRecorder: '0x0000000000000000000000000000000000000000' + voidPreActionCondition: arbiterCondition.address, + voidPostActionHook: '0x0000000000000000000000000000000000000000', + refundPreActionCondition: arbiterCondition.address, + refundPostActionHook: '0x0000000000000000000000000000000000000000' }; const operator = await operatorFactory.deployOperator(config); @@ -164,7 +164,7 @@ Buyer approves tokens → Seller calls charge() → Funds transferred in one tx - ✅ Instant delivery for digital goods - ✅ Better UX - seller gets paid immediately - ❌ No buyer protection escrow period -- ❌ Payment immediately moves to post-escrow state +- ❌ Payment immediately moves to after capture state --- @@ -209,10 +209,10 @@ const config = { chargeRecorder: '0x0000000000000000000000000000000000000000', releaseCondition: releaseCondition, releaseRecorder: '0x0000000000000000000000000000000000000000', - refundInEscrowCondition: arbiterCondition.address, - refundInEscrowRecorder: '0x0000000000000000000000000000000000000000', - refundPostEscrowCondition: arbiterCondition.address, - refundPostEscrowRecorder: '0x0000000000000000000000000000000000000000' + voidPreActionCondition: arbiterCondition.address, + voidPostActionHook: '0x0000000000000000000000000000000000000000', + refundPreActionCondition: arbiterCondition.address, + refundPostActionHook: '0x0000000000000000000000000000000000000000' }; const operator = await operatorFactory.deployOperator(config); @@ -246,7 +246,7 @@ sequenceDiagram Arbiter->>Arbiter: Investigates Note over Buyer,Escrow: Day 16 - Resolution - Arbiter->>Operator: refundInEscrow(paymentId) + Arbiter->>Operator: voidPayment(paymentId) Operator->>Escrow: void(paymentId) Escrow->>Buyer: Full refund ``` @@ -284,10 +284,10 @@ const config = { chargeRecorder: '0x0000000000000000000000000000000000000000', releaseCondition: releaseCondition, releaseRecorder: '0x0000000000000000000000000000000000000000', - refundInEscrowCondition: arbiterCondition.address, - refundInEscrowRecorder: '0x0000000000000000000000000000000000000000', - refundPostEscrowCondition: arbiterCondition.address, - refundPostEscrowRecorder: '0x0000000000000000000000000000000000000000' + voidPreActionCondition: arbiterCondition.address, + voidPostActionHook: '0x0000000000000000000000000000000000000000', + refundPreActionCondition: arbiterCondition.address, + refundPostActionHook: '0x0000000000000000000000000000000000000000' }; const operator = await operatorFactory.deployOperator(config); @@ -346,10 +346,10 @@ const config = { chargeRecorder: '0x0000000000000000000000000000000000000000', releaseCondition: arbiterCondition.address, // Arbiter releases releaseRecorder: '0x0000000000000000000000000000000000000000', - refundInEscrowCondition: arbiterCondition.address, // Arbiter refunds - refundInEscrowRecorder: '0x0000000000000000000000000000000000000000', - refundPostEscrowCondition: arbiterCondition.address, - refundPostEscrowRecorder: '0x0000000000000000000000000000000000000000' + voidPreActionCondition: arbiterCondition.address, // Arbiter refunds + voidPostActionHook: '0x0000000000000000000000000000000000000000', + refundPreActionCondition: arbiterCondition.address, + refundPostActionHook: '0x0000000000000000000000000000000000000000' }; const operator = await operatorFactory.deployOperator(config); @@ -403,10 +403,10 @@ const config = { chargeRecorder: '0x0000000000000000000000000000000000000000', releaseCondition: releaseCondition, releaseRecorder: '0x0000000000000000000000000000000000000000', - refundInEscrowCondition: refundCondition, // Receiver OR Arbiter - refundInEscrowRecorder: '0x0000000000000000000000000000000000000000', - refundPostEscrowCondition: RECEIVER_CONDITION, // Only receiver post-escrow - refundPostEscrowRecorder: '0x0000000000000000000000000000000000000000' + voidPreActionCondition: refundCondition, // Receiver OR Arbiter + voidPostActionHook: '0x0000000000000000000000000000000000000000', + refundPreActionCondition: RECEIVER_CONDITION, // Only receiver after capture + refundPostActionHook: '0x0000000000000000000000000000000000000000' }; const operator = await operatorFactory.deployOperator(config); @@ -432,7 +432,7 @@ sequenceDiagram Note over Buyer,Escrow: Day 5 - Return Request Buyer->>Seller: Requests return Note over Seller: Approves return - Seller->>Operator: refundInEscrow(paymentId) + Seller->>Operator: voidPayment(paymentId) Operator->>Escrow: void(paymentId) Escrow->>Buyer: Full refund Note over Buyer,Escrow: Buyer returns product @@ -460,10 +460,10 @@ const config = { chargeRecorder: '0x0000000000000000000000000000000000000000', releaseCondition: providerCondition.address,// Provider releases releaseRecorder: '0x0000000000000000000000000000000000000000', - refundInEscrowCondition: '0x0000000000000000000000000000000000000000', // No refunds (no arbiter) - refundInEscrowRecorder: '0x0000000000000000000000000000000000000000', - refundPostEscrowCondition: '0x0000000000000000000000000000000000000000', - refundPostEscrowRecorder: '0x0000000000000000000000000000000000000000' + voidPreActionCondition: '0x0000000000000000000000000000000000000000', // No refunds (no arbiter) + voidPostActionHook: '0x0000000000000000000000000000000000000000', + refundPreActionCondition: '0x0000000000000000000000000000000000000000', + refundPostActionHook: '0x0000000000000000000000000000000000000000' }; const operator = await operatorFactory.deployOperator(config); @@ -532,10 +532,10 @@ const config = { chargeRecorder: '0x0000000000000000000000000000000000000000', releaseCondition: daoCondition.address, // DAO must approve releases releaseRecorder: '0x0000000000000000000000000000000000000000', - refundInEscrowCondition: daoCondition.address, // DAO can refund if needed - refundInEscrowRecorder: '0x0000000000000000000000000000000000000000', - refundPostEscrowCondition: '0x0000000000000000000000000000000000000000', // No post-escrow refunds - refundPostEscrowRecorder: '0x0000000000000000000000000000000000000000' + voidPreActionCondition: daoCondition.address, // DAO can refund if needed + voidPostActionHook: '0x0000000000000000000000000000000000000000', + refundPreActionCondition: '0x0000000000000000000000000000000000000000', // No refunds + refundPostActionHook: '0x0000000000000000000000000000000000000000' }; const operator = await operatorFactory.deployOperator(config); @@ -588,7 +588,7 @@ sequenceDiagram // Deploy condition for platform address const platformCondition = await new StaticAddressCondition(PLATFORM_ADDRESS); -// Time-proportional charge condition (custom — not provided out of the box, +// Time-proportional charge condition (custom, not provided out of the box, // this is a hypothetical custom condition you would implement yourself) const timeProportionalCondition = await new TimeProportionalCondition(); @@ -604,10 +604,10 @@ const config = { chargeRecorder: '0x0000000000000000000000000000000000000000', releaseCondition: RECEIVER_CONDITION, // Receiver releases remaining releaseRecorder: '0x0000000000000000000000000000000000000000', - refundInEscrowCondition: PAYER_CONDITION, // Payer can cancel stream - refundInEscrowRecorder: '0x0000000000000000000000000000000000000000', - refundPostEscrowCondition: '0x0000000000000000000000000000000000000000', // No refunds after charged - refundPostEscrowRecorder: '0x0000000000000000000000000000000000000000' + voidPreActionCondition: PAYER_CONDITION, // Payer can cancel stream + voidPostActionHook: '0x0000000000000000000000000000000000000000', + refundPreActionCondition: '0x0000000000000000000000000000000000000000', // No refunds after charged + refundPostActionHook: '0x0000000000000000000000000000000000000000' }; const operator = await operatorFactory.deployOperator(config); @@ -673,10 +673,10 @@ const config = { chargeRecorder: '0x0000000000000000000000000000000000000000', releaseCondition: RECEIVER_CONDITION, // Receiver releases on payment terms releaseRecorder: '0x0000000000000000000000000000000000000000', - refundInEscrowCondition: RECEIVER_CONDITION,// Receiver can refund (invoice error) - refundInEscrowRecorder: '0x0000000000000000000000000000000000000000', - refundPostEscrowCondition: '0x0000000000000000000000000000000000000000', // No post-delivery refunds - refundPostEscrowRecorder: '0x0000000000000000000000000000000000000000' + voidPreActionCondition: RECEIVER_CONDITION,// Receiver can refund (invoice error) + voidPostActionHook: '0x0000000000000000000000000000000000000000', + refundPreActionCondition: '0x0000000000000000000000000000000000000000', // No post-delivery refunds + refundPostActionHook: '0x0000000000000000000000000000000000000000' }; const operator = await operatorFactory.deployOperator(config); diff --git a/contracts/factories.mdx b/contracts/factories.mdx index dd8e5ff..871b925 100644 --- a/contracts/factories.mdx +++ b/contracts/factories.mdx @@ -62,10 +62,10 @@ struct OperatorConfig { address chargeRecorder; address releaseCondition; address releaseRecorder; - address refundInEscrowCondition; - address refundInEscrowRecorder; - address refundPostEscrowCondition; - address refundPostEscrowRecorder; + address voidPreActionCondition; + address voidPostActionHook; + address refundPreActionCondition; + address refundPostActionHook; } ``` @@ -79,7 +79,7 @@ function deployOperator( **Parameters (in config):** - `feeRecipient` - Who receives operator fees (arbiter, service provider, treasury, etc.) -- `authorizeCondition` through `refundPostEscrowRecorder` - 10-slot configuration +- `authorizeCondition` through `refundPostActionHook` - 10-slot configuration **Note:** `maxFeeBps` and `protocolFeePct` are set at factory level (shared across all operators) @@ -158,10 +158,10 @@ const config = { chargeRecorder: zeroAddress, // No recording releaseCondition: releaseConditionAddress, releaseRecorder: zeroAddress, - refundInEscrowCondition: arbiterConditionAddress, - refundInEscrowRecorder: zeroAddress, - refundPostEscrowCondition: arbiterConditionAddress, - refundPostEscrowRecorder: zeroAddress + voidPreActionCondition: arbiterConditionAddress, + voidPostActionHook: zeroAddress, + refundPreActionCondition: arbiterConditionAddress, + refundPostActionHook: zeroAddress }; // Deploy operator @@ -186,10 +186,10 @@ const config = { chargeRecorder: zeroAddress, releaseCondition: providerCondition.address, releaseRecorder: zeroAddress, - refundInEscrowCondition: zeroAddress, // No refunds - refundInEscrowRecorder: zeroAddress, - refundPostEscrowCondition: zeroAddress, - refundPostEscrowRecorder: zeroAddress + voidPreActionCondition: zeroAddress, // No refunds + voidPostActionHook: zeroAddress, + refundPreActionCondition: zeroAddress, + refundPostActionHook: zeroAddress }; const hash = await factory.write.deployOperator([config]); @@ -497,10 +497,10 @@ bytes32 key = keccak256(abi.encodePacked( config.chargeRecorder, config.releaseCondition, config.releaseRecorder, - config.refundInEscrowCondition, - config.refundInEscrowRecorder, - config.refundPostEscrowCondition, - config.refundPostEscrowRecorder + config.voidPreActionCondition, + config.voidPostActionHook, + config.refundPreActionCondition, + config.refundPostActionHook )); ``` diff --git a/contracts/gas-costs.mdx b/contracts/gas-costs.mdx index 486f1e1..e5412f3 100644 --- a/contracts/gas-costs.mdx +++ b/contracts/gas-costs.mdx @@ -22,7 +22,7 @@ The buyer never pays gas. They only sign an off-chain ERC-3009 authorization. Al | **Merchant** | `release()` | 150,262 | < $0.005 | | **Happy path total** | authorize + release | 331,806 | **< $0.01** | -Disputes are rare and add < $0.005 with off-chain resolution — see [Dispute Path](#dispute-path) below. +Disputes are rare and add < $0.005 with off-chain resolution, see [Dispute Path](#dispute-path) below. ## Happy Path @@ -33,14 +33,14 @@ The happy path has **2 on-chain transactions**: `authorize` (at purchase time) a | `authorize()` | 181,544 | 17.6x | Facilitator | At purchase (HTTP 402 settlement) | | `release()` | 150,262 | 14.6x | Anyone | After escrow period expires | -The **vs transfer** column shows multiples of a cold ERC-20 `transfer()` (10,305 gas) — the absolute floor for moving tokens on-chain. +The **vs transfer** column shows multiples of a cold ERC-20 `transfer()` (10,305 gas), the absolute floor for moving tokens on-chain. In production, the merchant typically calls `release()`, but the function has no caller restriction beyond the configured release condition (EscrowPeriod + Freeze). After the escrow period passes and the payment isn't frozen, anyone can trigger it. An escrow authorization is inherently more work than a raw ERC-20 transfer: it validates payment info, checks fee bounds, locks fees, transfers tokens into escrow, and records state. The per-plugin section below shows exactly where the gas goes. -**Facilitators: set a gas limit.** The facilitator pays gas for `authorize()`, but the operator chooses which conditions and recorders are configured. Each plugin slot adds cost, and custom plugins can run arbitrary computation. Simulate the transaction with `eth_estimateGas` before submitting and reject operators whose `authorize()` exceeds a reasonable threshold (e.g., 300,000 gas). The full x402r configuration uses ~181,000 — anything significantly above that warrants investigation. +**Facilitators: set a gas limit.** The facilitator pays gas for `authorize()`, but the operator chooses which conditions and recorders are configured. Each plugin slot adds cost, and custom plugins can run arbitrary computation. Simulate the transaction with `eth_estimateGas` before submitting and reject operators whose `authorize()` exceeds a reasonable threshold (e.g., 300,000 gas). The full x402r configuration uses ~181,000, anything significantly above that warrants investigation. ## Per-Plugin Gas Costs @@ -51,10 +51,10 @@ The PaymentOperator is configured with pluggable conditions (checked before an a | Configuration | Gas | Marginal Cost | Plugin | |---------------|-----|---------------|--------| -| Commerce Payments escrow (no operator) | 78,353 | — | Raw `AuthCaptureEscrow.authorize()` — validates payment, escrows tokens via `PreApprovalPaymentCollector` | -| + PaymentOperator layer | 117,250 | **+38,897** | Operator dispatch, plugin slot checks, access control — all conditions, recorders, and fee calculator set to `address(0)` | -| + Fee calculation | 135,961 | **+18,711** | `StaticFeeCalculator` — calculates protocol + operator fees, validates bounds, locks fees in `authorizedFees[hash]` | -| + EscrowPeriod recorder | 162,744 | **+26,783** | `EscrowPeriod.record()` — stores `authorizationTime[hash] = block.timestamp` (cold SSTORE to cross-contract slot) | +| Commerce Payments escrow (no operator) | 78,353 |: | Raw `AuthCaptureEscrow.authorize()`: validates payment, escrows tokens via `PreApprovalPaymentCollector` | +| + PaymentOperator layer | 117,250 | **+38,897** | Operator dispatch, plugin slot checks, access control: all conditions, recorders, and fee calculator set to `address(0)` | +| + Fee calculation | 135,961 | **+18,711** | `StaticFeeCalculator`: calculates protocol + operator fees, validates bounds, locks fees in `authorizedFees[hash]` | +| + EscrowPeriod recorder | 162,744 | **+26,783** | `EscrowPeriod.record()`: stores `authorizationTime[hash] = block.timestamp` (cold SSTORE to cross-contract slot) | The EscrowPeriod recorder is the single most expensive plugin on `authorize` because it writes to a new storage slot in the EscrowPeriod contract. @@ -62,15 +62,15 @@ The EscrowPeriod recorder is the single most expensive plugin on `authorize` bec | Configuration | Gas | Marginal Cost | Plugin | |---------------|-----|---------------|--------| -| Commerce Payments escrow (no operator) | 66,365 | — | Raw `AuthCaptureEscrow.capture()` — validates authorization, distributes tokens to receiver | -| + PaymentOperator layer | 77,926 | **+11,561** | Operator dispatch, plugin slot checks, access control — all conditions, recorders, and fee calculator set to `address(0)` | +| Commerce Payments escrow (no operator) | 66,365 |: | Raw `AuthCaptureEscrow.capture()`: validates authorization, distributes tokens to receiver | +| + PaymentOperator layer | 77,926 | **+11,561** | Operator dispatch, plugin slot checks, access control: all conditions, recorders, and fee calculator set to `address(0)` | | + Fee retrieval | 116,980 | **+39,054** | Reads locked fees from `authorizedFees[hash]`, calculates protocol share, accumulates in `accumulatedProtocolFees[token]` | -| + ReceiverCondition | 121,430 | **+4,450** | Pure calldata comparison: `caller == paymentInfo.receiver` — no storage reads | +| + ReceiverCondition | 121,430 | **+4,450** | Pure calldata comparison: `caller == paymentInfo.receiver`: no storage reads | | + EscrowPeriod condition | 122,520 | **+5,540** | Cross-contract SLOAD: reads `authorizationTime[hash]`, compares against `block.timestamp` | | + Freeze + AndCondition | 142,961 | **+20,441** | AndCondition combinator loop + `Freeze.check()` reads `frozenUntil[hash]` + internal `isDuringEscrowPeriod()` | -**Simple conditions are nearly free.** `ReceiverCondition` and `PayerCondition` cost ~4,500 gas — they only compare calldata fields. Cross-contract conditions like `EscrowPeriod` cost ~5,500 due to a cold SLOAD. The `Freeze` condition is the most expensive single condition (+20,441) because of the AndCondition combinator overhead, its own `frozenUntil` storage read, and an internal escrow period check. +**Simple conditions are nearly free.** `ReceiverCondition` and `PayerCondition` cost ~4,500 gas, they only compare calldata fields. Cross-contract conditions like `EscrowPeriod` cost ~5,500 due to a cold SLOAD. The `Freeze` condition is the most expensive single condition (+20,441) because of the AndCondition combinator overhead, its own `frozenUntil` storage read, and an internal escrow period check. ## Dispute Path @@ -79,12 +79,12 @@ These operations only happen when a payment is disputed. Most payments never tou ### Off-chain resolution -The refund request, evidence submission, and arbiter approval can all happen off-chain. The only on-chain steps are `freeze()` (to lock the payment during the escrow window) and `refundInEscrow()` (to return funds). The arbiter never submits a transaction — their approval is an EIP-712 signature that anyone can relay. +The refund request, evidence submission, and arbiter approval can all happen off-chain. The only on-chain steps are `freeze()` (to lock the payment during the escrow window) and `voidPayment()` (to return funds). The arbiter never submits a transaction, their approval is an EIP-712 signature that anyone can relay. | On-chain step | Gas | vs transfer | Who Calls | |--------------|-----|------------|-----------| | `freeze()` | 44,651 | 4.3x | Buyer | -| `refundInEscrow()` | 65,924 | 6.4x | Anyone | +| `voidPayment()` | 65,924 | 6.4x | Anyone | | **Total** | **110,575** | **10.7x** | | Total dispute cost on Base with off-chain resolution: **< $0.005**. @@ -101,7 +101,7 @@ If the parties choose to handle the dispute fully on-chain instead: | `requestRefund()` | 421,689 | 40.9x | Buyer | Creates refund request with multi-index storage | | `submitEvidence()` | 135,597 | 13.2x | Any party | Stores IPFS CID on-chain | | `approveWithSignature()` | 89,935 | 8.7x | Anyone | Relays arbiter's off-chain EIP-712 signature | -| `refundPostEscrow()` | 54,467 | 5.3x | Anyone | Pulls funds from merchant wallet via ReceiverRefundCollector | +| `refund()` | 54,467 | 5.3x | Anyone | Pulls funds from merchant wallet via ReceiverRefundCollector | | **Total** | **1,078,145** | **104.6x** | | | This total includes the happy path steps (`authorize` + `release`) since those have already been paid. The dispute-only overhead is 746,339 gas (< $0.02 on Base). @@ -131,10 +131,10 @@ This indexing enables efficient off-chain queries but costs more gas upfront. On | x402r dispute (off-chain optimized) | 110,575 | 10.7x | < $0.005 | | x402r dispute (fully on-chain, 7 txns) | 1,078,145 | 104.6x | < $0.05 | -The full x402r happy path uses ~32x the gas of a single ERC-20 transfer — but on Base L2, the absolute cost stays under a penny. The overhead comes from escrow validation, fee locking, cross-contract storage writes, and condition checks, all detailed in the per-plugin breakdown above. +The full x402r happy path uses ~32x the gas of a single ERC-20 transfer, but on Base L2, the absolute cost stays under a penny. The overhead comes from escrow validation, fee locking, cross-contract storage writes, and condition checks, all detailed in the per-plugin breakdown above. -All numbers above assume one payment per transaction. Batching multiple operations in a single transaction (via a multicall contract) can reduce per-payment costs by 37–80% due to warm EVM access — contract addresses and shared storage only need to be loaded once. The benchmark test includes warm measurements for reference. +All numbers above assume one payment per transaction. Batching multiple operations in a single transaction (via a multicall contract) can reduce per-payment costs by 37–80% due to warm EVM access, contract addresses and shared storage only need to be loaded once. The benchmark test includes warm measurements for reference. diff --git a/contracts/overview.mdx b/contracts/overview.mdx index 75292a4..b861a5d 100644 --- a/contracts/overview.mdx +++ b/contracts/overview.mdx @@ -68,8 +68,8 @@ x402r extends commerce-payments with flexible payment capabilities: - Fee recipient for protocol and operator fee distribution - Configurable authorization via conditions (not hardcoded roles) - Refund request states: `Pending` → `Approved`/`Denied`/`Cancelled` -- In-escrow refunds (during escrow period) -- Post-escrow refunds (after release) +- Voids (during escrow period) +- Refunds (after capture) (after release) - Support for marketplace, subscription, streaming, and custom flows ### 2. Pluggable Condition System @@ -79,7 +79,7 @@ x402r extends commerce-payments with flexible payment capabilities: **Recorders (IRecorder)** - State updates after actions **10-slot configuration per operator:** -- 5 condition slots (before action): authorize, charge, release, refundInEscrow, refundPostEscrow +- 5 condition slots (before action): authorize, charge, release, voidPayment, refund - 5 recorder slots (after action): state tracking for each action **Benefits:** diff --git a/contracts/payment-operator.mdx b/contracts/payment-operator.mdx index 8e4a827..53c0f5b 100644 --- a/contracts/payment-operator.mdx +++ b/contracts/payment-operator.mdx @@ -39,8 +39,8 @@ mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees; 1. **AUTHORIZE_CONDITION** - Who can authorize payments 2. **CHARGE_CONDITION** - Who can charge partial amounts 3. **RELEASE_CONDITION** - Who can release from escrow -4. **REFUND_IN_ESCROW_CONDITION** - Who can refund during escrow -5. **REFUND_POST_ESCROW_CONDITION** - Who can refund after release +4. **VOID_PRE_ACTION_CONDITION** - Who can refund during escrow +5. **REFUND_PRE_ACTION_CONDITION** - Who can refund after release **Default:** `address(0)` = always allow @@ -49,8 +49,8 @@ mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees; 1. **AUTHORIZE_RECORDER** - Record authorization (e.g., timestamp) 2. **CHARGE_RECORDER** - Record charge event 3. **RELEASE_RECORDER** - Record release -4. **REFUND_IN_ESCROW_RECORDER** - Record in-escrow refund -5. **REFUND_POST_ESCROW_RECORDER** - Record post-escrow refund +4. **VOID_POST_ACTION_HOOK** - Record void +5. **REFUND_POST_ACTION_HOOK** - Record refund **Default:** `address(0)` = no recording (no-op) @@ -120,7 +120,7 @@ function charge( **Access:** Controlled by `CHARGE_CONDITION` (default: anyone) -Unlike `authorize()`, funds go directly to receiver without escrow hold. Refunds are only possible via `refundPostEscrow()`. +Unlike `authorize()`, funds go directly to receiver without escrow hold. Refunds are only possible via `refund()`. ### release() @@ -154,12 +154,12 @@ function release( **DAO example:** StaticAddressCondition(daoMultisig) -### refundInEscrow() +### voidPayment() Refunds payment while still in escrow (partial void). ```solidity -function refundInEscrow( +function voidPayment( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, uint120 amount ) external nonReentrant @@ -170,12 +170,12 @@ function refundInEscrow( - `amount` - Amount to return to payer **Flow:** -1. Check `REFUND_IN_ESCROW_CONDITION` (if set) +1. Check `VOID_PRE_ACTION_CONDITION` (if set) 2. Call `escrow.partialVoid()` to return funds to payer -3. Call `REFUND_IN_ESCROW_RECORDER` (if set) -4. Emit `RefundInEscrowExecuted` +3. Call `VOID_POST_ACTION_HOOK` (if set) +4. Emit `VoidExecuted` -**Access:** Controlled by `REFUND_IN_ESCROW_CONDITION` +**Access:** Controlled by `VOID_PRE_ACTION_CONDITION` **Marketplace example:** StaticAddressCondition(arbiter) - disputes @@ -184,12 +184,12 @@ function refundInEscrow( **Subscription example:** address(0) - no refunds -### refundPostEscrow() +### refund() Refunds payment after it has been released (captured). ```solidity -function refundPostEscrow( +function refund( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, uint256 amount, address tokenCollector, @@ -204,17 +204,17 @@ function refundPostEscrow( - `collectorData` - Data to pass to the token collector (e.g., signatures) **Flow:** -1. Check `REFUND_POST_ESCROW_CONDITION` (if set) +1. Check `REFUND_PRE_ACTION_CONDITION` (if set) 2. Call `escrow.refund()` - token collector enforces permission -3. Call `REFUND_POST_ESCROW_RECORDER` (if set) -4. Emit `RefundPostEscrowExecuted` +3. Call `REFUND_POST_ACTION_HOOK` (if set) +4. Emit `RefundExecuted` -**Access:** Controlled by `REFUND_POST_ESCROW_CONDITION`. Permission is also enforced by the token collector (e.g., receiver must have approved it, or collectorData contains receiver's signature). +**Access:** Controlled by `REFUND_PRE_ACTION_CONDITION`. Permission is also enforced by the token collector (e.g., receiver must have approved it, or collectorData contains receiver's signature). **Marketplace example:** StaticAddressCondition(arbiter) - post-delivery disputes **Return policy example:** Receiver - voluntary returns -**Most configurations:** address(0) - no post-escrow refunds +**Most configurations:** address(0) - no refunds ## Fee System (Modular, Additive) diff --git a/contracts/periphery/auth-capture-escrow.mdx b/contracts/periphery/auth-capture-escrow.mdx index 57940ae..ac9dc4c 100644 --- a/contracts/periphery/auth-capture-escrow.mdx +++ b/contracts/periphery/auth-capture-escrow.mdx @@ -21,8 +21,8 @@ stateDiagram-v2 [*] --> NonExistent NonExistent --> InEscrow: authorize() InEscrow --> Released: release() - InEscrow --> Settled: void() / refundInEscrow() - Released --> Settled: reclaim() / refundPostEscrow() + InEscrow --> Settled: void() + Released --> Settled: reclaim() / refund() Settled --> [*] note right of InEscrow @@ -32,7 +32,7 @@ stateDiagram-v2 note right of Released Funds transferred to receiver - Can still refund post-escrow + Can still refund end note note right of Settled diff --git a/contracts/periphery/overview.mdx b/contracts/periphery/overview.mdx index 454046b..3974084 100644 --- a/contracts/periphery/overview.mdx +++ b/contracts/periphery/overview.mdx @@ -15,11 +15,11 @@ Periphery contracts support the [PaymentOperator](/contracts/payment-operator) b | [Commerce Payments](/contracts/periphery/auth-capture-escrow) | AuthCaptureEscrow + ERC3009PaymentCollector (base layer) | Singleton | | [RefundRequest](/contracts/periphery/refund-request) | Tracks refund request lifecycle and approvals | Singleton | | [RefundRequestEvidence](/contracts/periphery/refund-request-evidence) | On-chain evidence submission for disputes | Singleton | -| [ReceiverRefundCollector](/contracts/periphery/receiver-refund-collector) | Pulls funds from receiver for post-escrow refunds | Singleton | +| [ReceiverRefundCollector](/contracts/periphery/receiver-refund-collector) | Pulls funds from receiver for refunds | Singleton | ## Contract Addresses -All periphery contracts use **unified CREATE3 addresses** — the same address on every supported chain. +All periphery contracts use **unified CREATE3 addresses**: the same address on every supported chain. | Contract | Address | |----------|---------| diff --git a/contracts/periphery/receiver-refund-collector.mdx b/contracts/periphery/receiver-refund-collector.mdx index 66640fa..e2422d2 100644 --- a/contracts/periphery/receiver-refund-collector.mdx +++ b/contracts/periphery/receiver-refund-collector.mdx @@ -1,6 +1,6 @@ --- title: "ReceiverRefundCollector" -description: "Pulls funds from receiver for post-escrow refunds" +description: "Pulls funds from receiver for refunds" icon: "arrow-rotate-left" --- @@ -12,15 +12,15 @@ icon: "arrow-rotate-left" ## Features -- **Post-escrow refunds** - Pulls funds from the receiver's wallet after funds have already been released +- **Refunds (after capture)** - Pulls funds from the receiver's wallet after funds have already been released - **Receiver approval required** - The receiver must have approved the collector contract or provided a signature -- **Operator integration** - Called by `operator.refundPostEscrow()` via the token collector interface +- **Operator integration** - Called by `operator.refund()` via the token collector interface ## How It Works After funds have been released to the receiver (state: `Released`), refunds require pulling tokens back from the receiver's wallet. The `ReceiverRefundCollector` handles this by: -1. Operator calls `refundPostEscrow(paymentInfo, amount, receiverRefundCollector, collectorData)` +1. Operator calls `refund(paymentInfo, amount, receiverRefundCollector, collectorData)` 2. The collector transfers tokens from the receiver to the escrow contract 3. The escrow contract returns tokens to the payer diff --git a/contracts/periphery/refund-request.mdx b/contracts/periphery/refund-request.mdx index 0b6dc7c..db3fad4 100644 --- a/contracts/periphery/refund-request.mdx +++ b/contracts/periphery/refund-request.mdx @@ -20,7 +20,7 @@ icon: "rotate-left" 1. Payer suspects fraud, requests refund 2. Arbiter investigates 3. Arbiter approves or denies request - 4. If approved, arbiter calls `operator.refundInEscrow()` + 4. If approved, arbiter calls `operator.void()` **Use cases:** - Buyer remorse @@ -35,7 +35,7 @@ icon: "rotate-left" 1. Receiver realizes product defect after release 2. Receiver requests refund 3. Arbiter investigates - 4. If approved, arbiter calls `operator.refundPostEscrow()` + 4. If approved, arbiter calls `operator.refund()` **Use cases:** - Product defects discovered later @@ -56,8 +56,8 @@ stateDiagram-v2 note right of Approved Arbiter must call - operator.refundInEscrow() - or operator.refundPostEscrow() + operator.void() + or operator.refund() end note ``` @@ -103,7 +103,7 @@ function updateStatus( - `nonce` - Record index identifying which refund request - `newStatus` - The new status (`Approved` or `Denied`) -**Access:** Receiver can always approve/deny. While in escrow, anyone passing the operator's `REFUND_IN_ESCROW_CONDITION` can also approve/deny. +**Access:** Receiver can always approve/deny. While in escrow, anyone passing the operator's `VOID_PRE_ACTION_CONDITION` can also approve/deny. **Valid transitions:** - `Pending` -> `Approved` @@ -145,7 +145,7 @@ await refundRequest.updateStatus( // Status: Approved // 3. Execute refund via operator (separate transaction) -await operator.refundInEscrow(paymentInfo, refundAmount); +await operator.void(paymentInfo, refundAmount); // Funds returned to payer ``` diff --git a/contracts/recorders/overview.mdx b/contracts/recorders/overview.mdx index cf073a3..3d641e9 100644 --- a/contracts/recorders/overview.mdx +++ b/contracts/recorders/overview.mdx @@ -6,15 +6,15 @@ icon: "database" ## What Are Recorders? -Recorders are pluggable contracts that update state **after** an action successfully executes on a PaymentOperator. Each operator has **5 recorder slots** — one per action: +Recorders are pluggable contracts that update state **after** an action successfully executes on a PaymentOperator. Each operator has **5 recorder slots**: one per action: | Slot | Records After | |------|---------------| | `AUTHORIZE_RECORDER` | Authorization (e.g., timestamp) | | `CHARGE_RECORDER` | Charge event | | `RELEASE_RECORDER` | Release from escrow | -| `REFUND_IN_ESCROW_RECORDER` | In-escrow refund | -| `REFUND_POST_ESCROW_RECORDER` | Post-escrow refund | +| `VOID_POST_ACTION_HOOK` | Void | +| `REFUND_POST_ACTION_HOOK` | Refund (after capture) | ## IRecorder Interface @@ -29,13 +29,13 @@ interface IRecorder { ``` **Parameters:** -- `paymentInfo` — The payment information struct -- `amount` — The amount involved in the action -- `caller` — The address that executed the action (msg.sender on operator) +- `paymentInfo`, The payment information struct +- `amount`, The amount involved in the action +- `caller`, The address that executed the action (msg.sender on operator) ## Default Behavior -**Recorder slot = `address(0)`** — no-op (does nothing). No state is recorded. +**Recorder slot = `address(0)`**: no-op (does nothing). No state is recorded. This means you only need to set recorders for slots where you want state tracking. Leave the rest as `address(0)`. @@ -63,7 +63,7 @@ Index operator events with a subgraph for rich queries (payment history by payer ### On-Chain Recorders (~20k Gas per Write) -Use recorders when you need **on-chain reads** — other contracts or conditions that depend on recorded state. [EscrowPeriod](/contracts/conditions/escrow-period) is the most common example: it records authorization time so the release condition can check if the escrow window has passed. +Use recorders when you need **on-chain reads**: other contracts or conditions that depend on recorded state. [EscrowPeriod](/contracts/conditions/escrow-period) is the most common example: it records authorization time so the release condition can check if the escrow window has passed. **Best for:** Escrow enforcement, dispute evidence, decentralized frontends, on-chain composability. diff --git a/roadmap.mdx b/roadmap.mdx index ba8f0e0..955412e 100644 --- a/roadmap.mdx +++ b/roadmap.mdx @@ -38,7 +38,7 @@ icon: "map" - Bond-based disputes - Multiple arbiter support per operator -- Post-escrow arbitration handling +- After capture arbitration handling - Reputation system for clients, merchants, and arbiters - Arbiter marketplace - Token wrapper for enforced refund protection @@ -63,7 +63,7 @@ icon: "map" ### Phase 2: Core SDK (Completed) - `@x402r/client`, Refund requests, freeze, escrow period queries, subscriptions -- `@x402r/merchant`, Release, charge, refundInEscrow, refundPostEscrow, refund handling +- `@x402r/merchant`, Release, charge, voidPayment, refund, refund handling - `@x402r/arbiter`, Decision submission, batch operations, registry, AI hooks - `@x402r/helpers`, `refundable()` helper for payment options - `@x402r/core`, Types, ABIs, config, deploy utilities diff --git a/sdk/arbiter.mdx b/sdk/arbiter.mdx index cdc4d92..d04f5d3 100644 --- a/sdk/arbiter.mdx +++ b/sdk/arbiter.mdx @@ -99,11 +99,11 @@ for (const entry of batch!.entries) { ### 5. Approve a Refund -`refundInEscrow()` auto-approves the pending RefundRequest. There is no undo. +`voidPayment()` auto-approves the pending RefundRequest. There is no undo. ```typescript -const tx = await arbiter.payment.refundInEscrow(paymentInfo, request!.amount) +const tx = await arbiter.payment.voidPayment(paymentInfo) console.log('Refund approved:', tx) // Verify diff --git a/sdk/arbiter/ai-integration.mdx b/sdk/arbiter/ai-integration.mdx index 4bc4358..8e19cd7 100644 --- a/sdk/arbiter/ai-integration.mdx +++ b/sdk/arbiter/ai-integration.mdx @@ -37,7 +37,7 @@ const unwatch = arbiter.watch.onRefundRequest(async (logs) => { if (decision.approve && decision.confidence >= 0.9) { const request = await arbiter.refund?.get(paymentInfo) if (request && request.status === RefundRequestStatus.Pending) { - await arbiter.payment.refundInEscrow(paymentInfo, request.amount) + await arbiter.payment.voidPayment(paymentInfo) console.log('Auto-approved refund') } } else if (!decision.approve && decision.confidence >= 0.9) { diff --git a/sdk/arbiter/batch-operations.mdx b/sdk/arbiter/batch-operations.mdx index e7bfc50..a57ae37 100644 --- a/sdk/arbiter/batch-operations.mdx +++ b/sdk/arbiter/batch-operations.mdx @@ -32,7 +32,7 @@ async function batchProcess( const shouldApprove = request.amount < 10_000_000n // Auto-approve < 10 USDC if (shouldApprove) { - const tx = await arbiter.payment.refundInEscrow(paymentInfo, request.amount) + const tx = await arbiter.payment.voidPayment(paymentInfo) approved.push(tx) } else { const tx = await arbiter.refund?.deny(paymentInfo) @@ -80,7 +80,7 @@ async function triageOperatorCases( // Make decision if (request.amount < 10_000_000n) { - await arbiter.payment.refundInEscrow(paymentInfo, request.amount) + await arbiter.payment.voidPayment(paymentInfo) } else { await arbiter.refund?.deny(paymentInfo) } diff --git a/sdk/arbiter/decision-submission.mdx b/sdk/arbiter/decision-submission.mdx index 5c7f75d..2d68776 100644 --- a/sdk/arbiter/decision-submission.mdx +++ b/sdk/arbiter/decision-submission.mdx @@ -6,18 +6,18 @@ icon: "gavel" The arbiter client provides methods for reviewing refund requests, making decisions, and executing refunds through the `refund`, `payment`, and `evidence` action groups. -## Approve a refund (refundInEscrow) +## Approve a refund (voidPayment) -To approve and execute a refund in one step, call `payment.refundInEscrow()`. This auto-approves the pending RefundRequest and transfers funds back to the payer. +To approve and execute a refund in one step, call `payment.voidPayment()`. This auto-approves the pending RefundRequest and transfers funds back to the payer. ```typescript const amounts = await arbiter.payment.getAmounts(paymentInfo) -const tx = await arbiter.payment.refundInEscrow(paymentInfo, amounts.refundableAmount) +const tx = await arbiter.payment.voidPayment(paymentInfo) console.log('Refund approved and executed:', tx) ``` -`payment.refundInEscrow()` auto-approves the pending RefundRequest. There is no undo. +`payment.voidPayment()` auto-approves the pending RefundRequest. There is no undo. ## Deny a refund request @@ -167,7 +167,7 @@ async function processCase( const shouldApprove = await evaluateCase(request) if (shouldApprove) { - const tx = await arbiter.payment.refundInEscrow(paymentInfo, request!.amount) + const tx = await arbiter.payment.voidPayment(paymentInfo) console.log('Refund executed:', tx) } else { const tx = await arbiter.refund?.deny(paymentInfo) @@ -185,7 +185,7 @@ flowchart TD C -->|No| D[Skip] C -->|Yes| E[Review evidence] E --> F{Decision} - F -->|Approve| G[payment.refundInEscrow] + F -->|Approve| G[payment.voidPayment] F -->|Deny| H[refund.deny] F -->|Decline| I[refund.refuse] G --> J[Funds Returned to Payer] diff --git a/sdk/arbiter/quickstart.mdx b/sdk/arbiter/quickstart.mdx index 9fd1d12..289596b 100644 --- a/sdk/arbiter/quickstart.mdx +++ b/sdk/arbiter/quickstart.mdx @@ -35,13 +35,13 @@ const arbiter = createArbiterClient({ The arbiter client provides these action groups: -- **`payment`** — `release`, `refundInEscrow`, `refundPostEscrow`, `getAmounts`, `getState` and more -- **`refund`** — `get`, `getStatus`, `has`, `deny`, `refuse`, `getOperatorRequests` and more -- **`evidence`** — `submit`, `get`, `getBatch`, `count` -- **`escrow`** — `isDuringEscrow`, `getAuthorizationTime`, `getDuration` -- **`freeze`** — `freeze`, `unfreeze`, `isFrozen` -- **`operator`** — `getConfig`, `calculateFees`, `distributeFees`, `getAccumulatedProtocolFees` and more -- **`watch`** — `onPayment`, `onRefundRequest`, `onRefundExecuted`, `onFeeDistribution` +- **`payment`**: `release`, `voidPayment`, `refund`, `getAmounts`, `getState` and more +- **`refund`**: `get`, `getStatus`, `has`, `deny`, `refuse`, `getOperatorRequests` and more +- **`evidence`**: `submit`, `get`, `getBatch`, `count` +- **`escrow`**: `isDuringEscrow`, `getAuthorizationTime`, `getDuration` +- **`freeze`**: `freeze`, `unfreeze`, `isFrozen` +- **`operator`**: `getConfig`, `calculateFees`, `distributeFees`, `getAccumulatedProtocolFees` and more +- **`watch`**: `onPayment`, `onRefundRequest`, `onRefundExecuted`, `onFeeDistribution` ## Try it now diff --git a/sdk/arbiter/subscriptions.mdx b/sdk/arbiter/subscriptions.mdx index 2fbe99e..ff64876 100644 --- a/sdk/arbiter/subscriptions.mdx +++ b/sdk/arbiter/subscriptions.mdx @@ -41,7 +41,7 @@ unwatch() ## watch.onRefundExecuted -Watch for refund execution events: `RefundInEscrowExecuted` and `RefundPostEscrowExecuted`. +Watch for refund execution events: `VoidExecuted` and `RefundExecuted`. ```typescript const unwatch = arbiter.watch.onRefundExecuted((logs) => { @@ -73,7 +73,7 @@ unwatch() |--------|---------------|----------| | `watch.onRefundRequest` | All RefundRequest ABI events | RefundRequest | | `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | -| `watch.onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | +| `watch.onRefundExecuted` | `VoidExecuted`, `RefundExecuted` | PaymentOperator | | `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | diff --git a/sdk/client/quickstart.mdx b/sdk/client/quickstart.mdx index 038e55c..724a52a 100644 --- a/sdk/client/quickstart.mdx +++ b/sdk/client/quickstart.mdx @@ -35,14 +35,14 @@ const client = createPayerClient({ The payer client provides these action groups: -- **`payment`** — `getAmounts`, `getState`, `authorize`, `refundInEscrow` and more -- **`escrow`** — `isDuringEscrow`, `getAuthorizationTime`, `getDuration` -- **`refund`** — `request`, `cancel`, `get`, `getStatus`, `has`, `getByKey`, `getPayerRequests` and more -- **`evidence`** — `submit`, `get`, `getBatch`, `count` -- **`freeze`** — `freeze`, `unfreeze`, `isFrozen` -- **`watch`** — `onPayment`, `onRefundRequest`, `onRefundExecuted`, `onFeeDistribution` -- **`operator`** — `getConfig`, `getFeeAddresses`, `calculateFees` and more -- **`query`** — `getPayerPayments`, `getReceiverPayments`, `getPayment` (requires `paymentIndexRecorderAddress`) +- **`payment`**: `getAmounts`, `getState`, `authorize`, `voidPayment` and more +- **`escrow`**: `isDuringEscrow`, `getAuthorizationTime`, `getDuration` +- **`refund`**: `request`, `cancel`, `get`, `getStatus`, `has`, `getByKey`, `getPayerRequests` and more +- **`evidence`**: `submit`, `get`, `getBatch`, `count` +- **`freeze`**: `freeze`, `unfreeze`, `isFrozen` +- **`watch`**: `onPayment`, `onRefundRequest`, `onRefundExecuted`, `onFeeDistribution` +- **`operator`**: `getConfig`, `getFeeAddresses`, `calculateFees` and more +- **`query`**: `getPayerPayments`, `getReceiverPayments`, `getPayment` (requires `paymentIndexRecorderAddress`) Optional groups (`escrow`, `refund`, `evidence`, `freeze`, `query`) are `undefined` if you do not pass the corresponding contract address. Use optional chaining when calling them. diff --git a/sdk/client/subscriptions.mdx b/sdk/client/subscriptions.mdx index b32878b..fc00f4c 100644 --- a/sdk/client/subscriptions.mdx +++ b/sdk/client/subscriptions.mdx @@ -52,7 +52,7 @@ unwatch() ## watch.onRefundExecuted -Watch for refund execution events: `RefundInEscrowExecuted` and `RefundPostEscrowExecuted` on the PaymentOperator contract. +Watch for refund execution events: `VoidExecuted` and `RefundExecuted` on the PaymentOperator contract. ```typescript const unwatch = client.watch.onRefundExecuted((logs) => { @@ -84,7 +84,7 @@ unwatch() |--------|---------------|----------| | `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | | `watch.onRefundRequest` | All RefundRequest ABI events | RefundRequest | -| `watch.onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | +| `watch.onRefundExecuted` | `VoidExecuted`, `RefundExecuted` | PaymentOperator | | `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | diff --git a/sdk/concepts.mdx b/sdk/concepts.mdx index e570285..64acace 100644 --- a/sdk/concepts.mdx +++ b/sdk/concepts.mdx @@ -23,7 +23,7 @@ PaymentState.Expired // 4 - Payment expired stateDiagram-v2 [*] --> InEscrow: authorize() InEscrow --> Released: release() - InEscrow --> Settled: refundInEscrow() + InEscrow --> Settled: voidPayment() InEscrow --> Expired: escrow period passes Released --> Settled: settle() Expired --> [*] @@ -61,7 +61,7 @@ The **EscrowPeriod** contract tracks when a payment was authorized and enforces - **Payers** can request refunds or freeze the payment - **Merchants** can refund but cannot release -- **After the period** — merchants can release funds to themselves +- **After the period**: merchants can release funds to themselves ```typescript import { createPayerClient } from '@x402r/sdk' @@ -100,7 +100,7 @@ RefundRequestStatus.Refused // 4 - Arbiter declined to rule ### Refund flow -In v3, RefundRequest is wired as an IRecorder plugin. Refund approval happens automatically when the merchant or arbiter calls `refundInEscrow()` on the operator — no separate approve step is needed. +In v3, RefundRequest is wired as an IRecorder plugin. Refund approval happens automatically when the merchant or arbiter calls `voidPayment()` on the operator, no separate approve step is needed. ```mermaid sequenceDiagram @@ -114,13 +114,13 @@ sequenceDiagram R-->>M: RefundRequested event alt Merchant refunds - M->>O: refundInEscrow(paymentInfo, amount) + M->>O: voidPayment(paymentInfo) O->>R: record() auto-approves pending request O->>P: Funds returned else Merchant denies M->>R: denyRefundRequest(paymentInfo) else Escalate to arbiter - A->>O: refundInEscrow(paymentInfo, amount) + A->>O: voidPayment(paymentInfo) O->>R: record() auto-approves pending request O->>P: Funds returned end @@ -158,8 +158,8 @@ Freezing a payment does not automatically escalate to an arbiter. It pauses the | Role | Can Do | |------|--------| | **Payer** | Request refunds, freeze payments, cancel requests, query escrow state | -| **Merchant** | Release payments, charge, refund in escrow (auto-approves requests), deny refunds | -| **Arbiter** | Deny/refuse disputed refunds, refund in escrow, review evidence | +| **Merchant** | Release payments, charge, void (auto-approves requests), deny refunds | +| **Arbiter** | Deny/refuse disputed refunds, void, review evidence | ## Contract Architecture @@ -170,8 +170,8 @@ flowchart TB auth[authorize] rel[release] chg[charge] - ref[refundInEscrow] - refp[refundPostEscrow] + ref[voidPayment] + refp[refund] end subgraph Components[Supporting Contracts] diff --git a/sdk/delivery-arbiter.mdx b/sdk/delivery-arbiter.mdx index 6ac4f48..5ffd059 100644 --- a/sdk/delivery-arbiter.mdx +++ b/sdk/delivery-arbiter.mdx @@ -78,7 +78,7 @@ app.post('/verify', async (req, res) => { } else { // Arbiter can refund immediately without waiting for escrow expiry. const amounts = await arbiter.payment.getAmounts(paymentInfo) - await arbiter.refund.refundInEscrow(paymentInfo, amounts.capturableAmount) + await arbiter.refund.voidPayment(paymentInfo) res.json({ verdict: 'FAIL' }) } }) @@ -110,7 +110,7 @@ async function evaluate(responseBody: string): Promise { ### 5. What Happens on Failure -With delivery protection v2, the arbiter can call `refundInEscrow()` immediately on a FAIL verdict. You do not need to wait for escrow expiry. The receiver (merchant) can also trigger a voluntary refund at any time. +With delivery protection v2, the arbiter can call `voidPayment()` immediately on a FAIL verdict. You do not need to wait for escrow expiry. The receiver (merchant) can also trigger a voluntary refund at any time. If your service goes down, no payments get evaluated and funds stay in escrow until timeout. The escrow period protects payers, but add uptime monitoring and alerting. diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index e67c05d..64f7af8 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -25,12 +25,12 @@ All contracts are deployed via factories using CREATE2, so identical configurati A complete marketplace operator deployment includes: -1. **EscrowPeriod** — Records authorization time, enforces waiting period before release -2. **Freeze** — Allows payer to freeze payment during escrow, receiver to unfreeze -3. **ReceiverCondition** — Gates in-escrow refunds to the merchant (receiver) -4. **RefundRequest (IRecorder)** — Wired as `refundInEscrowRecorder`, auto-approves pending refund requests during `refundInEscrow()` -5. **StaticFeeCalculator** — Optional operator fee (basis points) -6. **PaymentOperator** — The main contract tying everything together +1. **EscrowPeriod**: Records authorization time, enforces waiting period before release +2. **Freeze**: Allows payer to freeze payment during escrow, receiver to unfreeze +3. **ReceiverCondition**: Gates voids to the merchant (receiver) +4. **RefundRequest (IRecorder)**: Wired as `voidPostActionHook`, auto-approves pending refund requests during `voidPayment()` +5. **StaticFeeCalculator**: Optional operator fee (basis points) +6. **PaymentOperator**: The main contract tying everything together ## Deploy your operator @@ -53,11 +53,11 @@ A complete marketplace operator deployment includes: cp .env.example .env ``` - Edit `.env` with your values. Only `PRIVATE_KEY` is required — everything else has sensible defaults: + Edit `.env` with your values. Only `PRIVATE_KEY` is required, everything else has sensible defaults: | Variable | Default | Description | |----------|---------|-------------| - | `PRIVATE_KEY` | — | Deployer wallet (required) | + | `PRIVATE_KEY` |, | Deployer wallet (required) | | `ARBITER` | deployer address | Dispute resolver | | `FEE_RECIPIENT` | deployer address | Receives operator fees | | `ESCROW_PERIOD` | `604800` (7 days) | Escrow period in seconds | @@ -157,7 +157,7 @@ interface MarketplaceOperatorDeployment { freezeAddress: Address | null; // Freeze condition (null if disabled) refundRequestAddress: Address; // RefundRequest contract refundRequestEvidenceAddress: Address; // RefundRequestEvidence contract - refundInEscrowConditionAddress: Address; // OR(Receiver, Arbiter) + voidPreActionConditionAddress: Address; // OR(Receiver, Arbiter) feeCalculatorAddress: Address | null; // null if no fee operatorConfig: OperatorConfig; // Full operator slot configuration deployments: DeployResult[]; // Per-contract deploy details @@ -170,7 +170,7 @@ interface MarketplaceOperatorDeployment { ``` -Because all contracts use CREATE3, redeploying with the same parameters is idempotent — it will detect existing contracts and skip them. The `summary` tells you what was new vs reused. +Because all contracts use CREATE3, redeploying with the same parameters is idempotent, it will detect existing contracts and skip them. The `summary` tells you what was new vs reused. ## Preview addresses (no deploy) @@ -187,8 +187,8 @@ Because all contracts use CREATE3, redeploying with the same parameters is idemp | `AUTHORIZE_RECORDER` | EscrowPeriod | Records authorization timestamp | | `CHARGE_CONDITION` | (none) | No restrictions on charge | | `RELEASE_CONDITION` | EscrowPeriod | Blocks release during escrow period | - | `REFUND_IN_ESCROW_CONDITION` | OR(Receiver, Arbiter) | Receiver or arbiter can approve | - | `REFUND_POST_ESCROW_CONDITION` | Receiver | Only receiver after escrow | + | `VOID_PRE_ACTION_CONDITION` | OR(Receiver, Arbiter) | Receiver or arbiter can approve | + | `REFUND_PRE_ACTION_CONDITION` | Receiver | Only receiver after escrow | | `FEE_CALCULATOR` | StaticFeeCalculator | Fixed percentage fee | | `FEE_RECIPIENT` | Your address | Receives fees | @@ -218,9 +218,9 @@ The deployed marketplace operator has the following slot configuration: | `AUTHORIZE_RECORDER` | EscrowPeriod | Records authorization timestamp | | `CHARGE_CONDITION` | (none) | No restrictions on charge | | `RELEASE_CONDITION` | EscrowPeriod (or AND(EscrowPeriod, Freeze) if freeze enabled) | Blocks release during escrow period | -| `REFUND_IN_ESCROW_CONDITION` | OR(Receiver, Arbiter) | Receiver or arbiter can approve | -| `REFUND_IN_ESCROW_RECORDER` | RefundRequest | Tracks refund request state | -| `REFUND_POST_ESCROW_CONDITION` | Receiver | Only receiver after escrow | +| `VOID_PRE_ACTION_CONDITION` | OR(Receiver, Arbiter) | Receiver or arbiter can approve | +| `VOID_POST_ACTION_HOOK` | RefundRequest | Tracks refund request state | +| `REFUND_PRE_ACTION_CONDITION` | Receiver | Only receiver after escrow | | `FEE_CALCULATOR` | StaticFeeCalculator | Fixed percentage fee (if configured) | | `FEE_RECIPIENT` | Your address | Receives fees | @@ -230,7 +230,7 @@ The deployed marketplace operator has the following slot configuration: The delivery protection preset is a simpler operator designed for garbage detection and content verification use cases. An arbiter verifies that content was delivered correctly and releases funds. If the arbiter does nothing, the escrow window expires and anyone can trigger a refund. -This preset has **no freeze, no fees, and no RefundRequest** contracts — making it cheaper to deploy. +This preset has **no freeze, no fees, and no RefundRequest** contracts, making it cheaper to deploy. ### Condition layout @@ -238,8 +238,8 @@ This preset has **no freeze, no fees, and no RefundRequest** contracts — makin |------|----------|---------| | `RELEASE_CONDITION` | StaticAddressCondition(arbiter) | Only the arbiter can release | | `AUTHORIZE_RECORDER` | EscrowPeriod | Records authorization timestamp | -| `REFUND_IN_ESCROW_CONDITION` | EscrowPeriod | Anyone can refund after escrow window expires | -| `REFUND_POST_ESCROW_CONDITION` | Receiver | Receiver can refund post-escrow | +| `VOID_PRE_ACTION_CONDITION` | EscrowPeriod | Anyone can refund after escrow window expires | +| `REFUND_PRE_ACTION_CONDITION` | Receiver | Receiver can refund | ### Deploy a delivery protection operator @@ -409,7 +409,7 @@ console.log('AuthorizeRecorder:', deployment.authorizeRecorderAddress) escrowPeriodAddress: Address arbiterConditionAddress: Address releaseConditionAddress: Address // OrCondition([arbiter, payer]) - refundInEscrowConditionAddress: Address // OrCondition([escrowPeriod, receiver, arbiter]) + voidPreActionConditionAddress: Address // OrCondition([escrowPeriod, receiver, arbiter]) authorizeRecorderAddress: Address // RecorderCombinator([escrowPeriod, paymentIndexRecorder]) paymentIndexRecorderAddress: Address operatorConfig: OperatorConfig @@ -451,8 +451,8 @@ console.log('AuthorizeRecorder:', deployment.authorizeRecorderAddress) |------|----------|---------| | `RELEASE_CONDITION` | OrCondition([SAC(arbiter), PayerCondition]) | Arbiter or satisfied payer can release | | `AUTHORIZE_RECORDER` | RecorderCombinator([EscrowPeriod, PaymentIndexRecorder]) | Records authorization time and indexes payments on-chain | - | `REFUND_IN_ESCROW_CONDITION` | OrCondition([EscrowPeriod, ReceiverCondition, SAC(arbiter)]) | Escrow expiry, receiver voluntary refund, or arbiter immediate refund | - | `REFUND_POST_ESCROW_CONDITION` | ReceiverCondition | Only receiver after escrow | + | `VOID_PRE_ACTION_CONDITION` | OrCondition([EscrowPeriod, ReceiverCondition, SAC(arbiter)]) | Escrow expiry, receiver voluntary refund, or arbiter immediate refund | + | `REFUND_PRE_ACTION_CONDITION` | ReceiverCondition | Only receiver after escrow | diff --git a/sdk/merchant.mdx b/sdk/merchant.mdx index c1c9864..4787aac 100644 --- a/sdk/merchant.mdx +++ b/sdk/merchant.mdx @@ -97,8 +97,8 @@ if (hasRefund) { console.log('Refund amount:', request?.amount) console.log('Status:', request?.status) // 0 = Pending - // Approve by executing refundInEscrow (recorder auto-approves) - const refundTx = await merchant.payment.refundInEscrow( + // Approve by executing voidPayment (recorder auto-approves) + const refundTx = await merchant.payment.voidPayment( paymentInfo, request!.amount, ) diff --git a/sdk/merchant/payment-operations.mdx b/sdk/merchant/payment-operations.mdx index a94e230..ade1088 100644 --- a/sdk/merchant/payment-operations.mdx +++ b/sdk/merchant/payment-operations.mdx @@ -33,19 +33,13 @@ console.log('Remaining in escrow:', amounts.capturableAmount) // 7000000n Always query `payment.getAmounts()` first to determine the available capturable amount. -### payment.refundInEscrow +### payment.voidPayment -Return escrowed funds to the payer before release. +Return all escrowed funds to the payer before capture. Void is full-only: it empties the authorization in one transaction. For a partial return, call `payment.capture()` for the part to keep, then void or let the remainder expire. ```typescript -// Full refund of 10 USDC -const tx = await merchant.payment.refundInEscrow(paymentInfo, 10_000_000n) -console.log('Refunded from escrow:', tx) -``` - -```typescript -// Partial refund: return 2 USDC, keep 8 USDC in escrow -const tx = await merchant.payment.refundInEscrow(paymentInfo, 2_000_000n) +const tx = await merchant.payment.voidPayment(paymentInfo) +console.log('Voided:', tx) ``` ### payment.charge @@ -66,39 +60,45 @@ console.log('Charged:', tx) The `charge()` method is designed for recurring payments and session-based billing where funds are not pre-escrowed. -### payment.refundPostEscrow +### payment.refund Refund funds that have already been released. Requires a token collector to source the refund from the merchant's balance. ```typescript -const tx = await merchant.payment.refundPostEscrow( +const tx = await merchant.payment.refund( paymentInfo, 5_000_000n, // 5 USDC to refund '0xTokenCollector...' as `0x${string}`, // sources the refund '0xSignatureData...' as `0x${string}`, // authorization data ) -console.log('Post-escrow refund:', tx) +console.log('Refund (after capture):', tx) ``` -Post-escrow refunds require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer. +Refunds (after capture) require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer. -### payment.approvePostEscrowRefund +### payment.approveRefundAllowance -Approve a post-escrow refund allowance. This sets how much the refund budget contract can pull from the merchant. +Approve a refund allowance. This sets how much the refund collector can pull from the merchant for a given token. ```typescript -const tx = await merchant.payment.approvePostEscrowRefund(paymentInfo, 5_000_000n) +const tx = await merchant.payment.approveRefundAllowance( + '0xTokenAddress...' as `0x${string}`, // ERC-20 token + 5_000_000n, // 5 USDC +) console.log('Allowance approved:', tx) ``` -### payment.getPostEscrowRefundAllowance +### payment.getRefundAllowance -Check the current post-escrow refund allowance. +Check the current refund allowance for a token/owner pair. ```typescript -const allowance = await merchant.payment.getPostEscrowRefundAllowance(paymentInfo) +const allowance = await merchant.payment.getRefundAllowance( + '0xTokenAddress...' as `0x${string}`, + '0xMerchantAddress...' as `0x${string}`, +) console.log('Allowance:', allowance) ``` @@ -173,7 +173,7 @@ flowchart TD D -->|No| F[Safe to release] D -->|Yes| G{Approve refund?} F --> H["payment.release(paymentInfo, amount)"] - G -->|Yes| I["payment.refundInEscrow(paymentInfo, amount)"] + G -->|Yes| I["payment.voidPayment(paymentInfo)"] G -->|No| J[Deny request, then release] J --> H ``` diff --git a/sdk/merchant/quickstart.mdx b/sdk/merchant/quickstart.mdx index 0c673ea..30f9425 100644 --- a/sdk/merchant/quickstart.mdx +++ b/sdk/merchant/quickstart.mdx @@ -75,21 +75,18 @@ console.log('Remaining in escrow:', amounts.capturableAmount) // 7000000n Always query `payment.getAmounts()` first to determine the available capturable amount. -## Refund while in escrow +## Void while in escrow -Use `payment.refundInEscrow()` to return escrowed funds to the payer before release. +Use `payment.voidPayment()` to return all escrowed funds to the payer before capture. Void is full-only: it empties the authorization in one transaction. ```typescript -// Full refund of 10 USDC -const tx = await merchant.payment.refundInEscrow(paymentInfo, 10_000_000n) -console.log('Refunded from escrow:', tx) +const tx = await merchant.payment.voidPayment(paymentInfo) +console.log('Voided:', tx) ``` -```typescript -// Partial refund: return 2 USDC, keep 8 USDC in escrow -const tx = await merchant.payment.refundInEscrow(paymentInfo, 2_000_000n) -console.log('Partial refund:', tx) -``` + +For a partial return (capture some, return the rest), call `payment.capture(paymentInfo, amount)` for the part you want to keep. The unused authorization can then be voided, or it will expire at `captureDeadline` and become reclaimable by the payer. + ## Charge directly @@ -109,22 +106,22 @@ console.log('Charged:', tx) The `charge()` method is designed for recurring payments and session-based billing where funds are not pre-escrowed. The token collector contract handles the actual token transfer. -## Refund after release (post-escrow) +## Refund after release (after capture) -Use `payment.refundPostEscrow()` to refund funds that have already been released. This requires a token collector to source the refund from the merchant's balance. +Use `payment.refund()` to refund funds that have already been released. This requires a token collector to source the refund from the merchant's balance. ```typescript -const tx = await merchant.payment.refundPostEscrow( +const tx = await merchant.payment.refund( paymentInfo, 5_000_000n, // 5 USDC to refund '0xTokenCollector...' as `0x${string}`, // sources the refund '0xSignatureData...' as `0x${string}`, // authorization data ) -console.log('Post-escrow refund:', tx) +console.log('Refund (after capture):', tx) ``` -Post-escrow refunds require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer. +Refunds (after capture) require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer. ## Query methods @@ -201,7 +198,7 @@ flowchart TD D -->|No| F[Safe to release] D -->|Yes| G{Approve refund?} F --> H["payment.release(paymentInfo, amount)"] - G -->|Yes| I["payment.refundInEscrow(paymentInfo, amount)"] + G -->|Yes| I["payment.voidPayment(paymentInfo)"] G -->|No| J[Deny request, then release] J --> H ``` diff --git a/sdk/merchant/refund-handling.mdx b/sdk/merchant/refund-handling.mdx index 7e9fd41..7d5bc41 100644 --- a/sdk/merchant/refund-handling.mdx +++ b/sdk/merchant/refund-handling.mdx @@ -116,17 +116,17 @@ console.log('Refund denied:', tx) If you deny a request, the payer may escalate to an arbiter for dispute resolution. Consider providing a reason off-chain to reduce escalation risk. -### payment.refundInEscrow +### payment.voidPayment -To approve and execute a refund, call `payment.refundInEscrow()`. This both auto-approves the pending RefundRequest and transfers funds back to the payer. +To approve and execute a refund, call `payment.voidPayment()`. This both auto-approves the pending RefundRequest and transfers funds back to the payer. ```typescript -const tx = await merchant.payment.refundInEscrow(paymentInfo, request!.amount) +const tx = await merchant.payment.voidPayment(paymentInfo) console.log('Refund approved and executed:', tx) ``` -`refundInEscrow()` auto-approves the pending RefundRequest. There is no undo. +`voidPayment()` auto-approves the pending RefundRequest. There is no undo. ## Freeze management @@ -197,7 +197,7 @@ async function handleRefundWorkflow( if (shouldApprove) { // Execute the refund (auto-approves the request) - const tx = await merchant.payment.refundInEscrow(paymentInfo, request.amount) + const tx = await merchant.payment.voidPayment(paymentInfo) console.log('Refund executed:', tx) } else { // Deny the request @@ -227,7 +227,7 @@ sequenceDiagram M->>M: Review request (policy check) alt Approve - M->>O: payment.refundInEscrow(paymentInfo, amount) + M->>O: payment.voidPayment(paymentInfo) O->>P: Funds returned to payer else Deny M->>R: refund.deny(paymentInfo) diff --git a/sdk/merchant/subscriptions.mdx b/sdk/merchant/subscriptions.mdx index 44f4eb2..38fbbce 100644 --- a/sdk/merchant/subscriptions.mdx +++ b/sdk/merchant/subscriptions.mdx @@ -66,7 +66,7 @@ const unwatch = merchant.watch.onRefundRequest(async (logs) => { if (amount && amount < AUTO_APPROVE_THRESHOLD) { console.log('Auto-approving small refund request') // Look up the paymentInfo from your database and call - // merchant.payment.refundInEscrow(paymentInfo, amount) + // merchant.payment.voidPayment(paymentInfo) } else { console.log('Queuing for manual review') } @@ -76,7 +76,7 @@ const unwatch = merchant.watch.onRefundRequest(async (logs) => { ## watch.onRefundExecuted -Watch for refund execution events: `RefundInEscrowExecuted` and `RefundPostEscrowExecuted`. +Watch for refund execution events: `VoidExecuted` and `RefundExecuted`. ```typescript const unwatch = merchant.watch.onRefundExecuted((logs) => { @@ -108,7 +108,7 @@ unwatch() |--------|---------------|----------| | `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | | `watch.onRefundRequest` | All RefundRequest ABI events | RefundRequest | -| `watch.onRefundExecuted` | `RefundInEscrowExecuted`, `RefundPostEscrowExecuted` | PaymentOperator | +| `watch.onRefundExecuted` | `VoidExecuted`, `RefundExecuted` | PaymentOperator | | `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | From cdc84ddecab6ce415442666b7d59bdc735c96b2c Mon Sep 17 00:00:00 2001 From: A1igator Date: Mon, 11 May 2026 20:08:10 -0700 Subject: [PATCH 20/37] docs: catch leftover In-Escrow/Post-Escrow refund framing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three sites the previous pass missed: - contracts/periphery/refund-request.mdx: tab titles "In-Escrow Refunds" / "Post-Escrow Refunds" → "Void (before capture)" / "Refund (after capture)"; fixed wrong operator.void(paymentInfo, amount) call to operator.void(paymentInfo) and noted the post-capture path uses operator.refund(paymentInfo, amount, tokenCollector, collectorData) - contracts/architecture.mdx: "Refund Flow (In-Escrow)" section heading + 3 stale signatures (operator.void took an amount, escrow.partialVoid no longer exists) - contracts/payment-operator.mdx: voidPayment() section was still documenting a partial-void signature with an amount; rewrote to match the actual operator API void(paymentInfo, data) backed by canonical escrow.void() After this pass remaining "in escrow" / "post escrow" references are all legitimate state descriptors (while in escrow, isDuringEscrow, InEscrow state name), not the dropped refund-flow distinction. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/architecture.mdx | 6 +++--- contracts/payment-operator.mdx | 20 ++++++++++++-------- contracts/periphery/refund-request.mdx | 16 +++++++++------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/contracts/architecture.mdx b/contracts/architecture.mdx index a74c9eb..4f8817e 100644 --- a/contracts/architecture.mdx +++ b/contracts/architecture.mdx @@ -78,7 +78,7 @@ For additional visual diagrams, see the [x402r-contracts repository](https://git 10. **Operator** accumulates protocol fees for later distribution 11. **Operator** calls `RELEASE_RECORDER` to update state -### Refund Flow (In-Escrow) +### Void Flow (before capture) **Example: Marketplace with arbiter dispute resolution** @@ -86,9 +86,9 @@ For additional visual diagrams, see the [x402r-contracts repository](https://git 2. **RefundRequest** creates request with status `Pending` 3. **Designated address** (e.g., arbiter, DAO multisig) reviews dispute 4. **Designated address** calls `refundRequest.updateStatus(paymentInfo, nonce, Approved)` -5. **Designated address** calls `operator.void(paymentInfo, amount)` +5. **Designated address** calls `operator.void(paymentInfo)` 6. **Operator** checks `VOID_PRE_ACTION_CONDITION` (configured per operator) -7. **Operator** calls `escrow.partialVoid()` to return funds to payer +7. **Operator** calls `escrow.void()` to return all escrowed funds to payer 8. **Operator** calls `VOID_POST_ACTION_HOOK` 9. Funds transferred back to payer diff --git a/contracts/payment-operator.mdx b/contracts/payment-operator.mdx index 53c0f5b..9504373 100644 --- a/contracts/payment-operator.mdx +++ b/contracts/payment-operator.mdx @@ -154,36 +154,40 @@ function release( **DAO example:** StaticAddressCondition(daoMultisig) -### voidPayment() +### void() -Refunds payment while still in escrow (partial void). +Returns all escrowed funds to the payer before capture. Full-only: `escrow.void()` empties the authorization in one transaction. ```solidity -function voidPayment( +function void( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, - uint120 amount + bytes calldata data ) external nonReentrant ``` **Parameters:** - `paymentInfo` - Payment info struct -- `amount` - Amount to return to payer +- `data` - Optional pass-through data for the pre/post action plugins **Flow:** 1. Check `VOID_PRE_ACTION_CONDITION` (if set) -2. Call `escrow.partialVoid()` to return funds to payer +2. Call `escrow.void()` to return escrowed funds to payer 3. Call `VOID_POST_ACTION_HOOK` (if set) 4. Emit `VoidExecuted` **Access:** Controlled by `VOID_PRE_ACTION_CONDITION` -**Marketplace example:** StaticAddressCondition(arbiter) - disputes +**Marketplace example:** StaticAddressCondition(arbiter) for disputes **Return policy example:** Receiver OR StaticAddressCondition(arbiter) **DAO example:** StaticAddressCondition(daoMultisig) -**Subscription example:** address(0) - no refunds +**Subscription example:** address(0), no voids allowed + +For a partial return, call `capture()` for the portion to keep. The unused authorization can then be voided, or it will become reclaimable by the payer after `captureDeadline`. + + ### refund() Refunds payment after it has been released (captured). diff --git a/contracts/periphery/refund-request.mdx b/contracts/periphery/refund-request.mdx index db3fad4..8c81763 100644 --- a/contracts/periphery/refund-request.mdx +++ b/contracts/periphery/refund-request.mdx @@ -13,13 +13,13 @@ icon: "rotate-left" ## Request Types - + **Who can request:** Payer, Receiver, OR Arbiter **Typical flow:** - 1. Payer suspects fraud, requests refund + 1. Payer suspects fraud, requests a refund 2. Arbiter investigates - 3. Arbiter approves or denies request + 3. Arbiter approves or denies the request 4. If approved, arbiter calls `operator.void()` **Use cases:** @@ -28,12 +28,12 @@ icon: "rotate-left" - Payment error - + **Who can request:** Receiver only **Typical flow:** - 1. Receiver realizes product defect after release - 2. Receiver requests refund + 1. Receiver realizes product defect after capture + 2. Receiver requests a refund 3. Arbiter investigates 4. If approved, arbiter calls `operator.refund()` @@ -145,7 +145,9 @@ await refundRequest.updateStatus( // Status: Approved // 3. Execute refund via operator (separate transaction) -await operator.void(paymentInfo, refundAmount); +// For pre-capture: operator.void() empties the auth (full only). +// For post-capture: operator.refund(paymentInfo, amount, tokenCollector, collectorData). +await operator.void(paymentInfo); // Funds returned to payer ``` From 146421b0a027ec1b9add25a4e51e58e631365d53 Mon Sep 17 00:00:00 2001 From: A1igator Date: Mon, 11 May 2026 20:23:43 -0700 Subject: [PATCH 21/37] docs(roadmap): rewrite around current state; flag SVM alpha; correct chain support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the stale Phase-N framing with three honest sections per workstream (Shipped / In flight / Planned) tracking what is actually in the repos: Protocol — authCapture on canonical commerce-payments (no fork), two settlement paths, ERC-3009 + Permit2 client signing, PaymentOperator hooks/conditions, RefundRequest, Freeze, EscrowPeriod. SVM (new section) — alpha/experimental Solana pilot called out prominently. Six Anchor programs + @x402r/svm package shipped, unaudited, not yet on mainnet-beta. Devnet demo + spec doc planned. SDK — drop the old 5-package list (was partly stale post-rewrite) and reflect the current package set (core, sdk, helpers, cli) plus the ERC-8004 plugin work, Anvil-fork testing, role-based presets. Chain support — supported chains are Base mainnet + Base Sepolia (matching x402r-sdk/packages/core/src/config). The wider CREATE2 redeploy via CreateX is preparatory; additional EVMs will be added as canonical commerce-payments@v1.0.0 coverage extends. No upstream-org PR/issue references in commits or content. No em dashes. Co-Authored-By: Claude Opus 4.7 (1M context) --- roadmap.mdx | 135 ++++++++++++++++++++++++++++------------------------ 1 file changed, 72 insertions(+), 63 deletions(-) diff --git a/roadmap.mdx b/roadmap.mdx index 955412e..99c00ea 100644 --- a/roadmap.mdx +++ b/roadmap.mdx @@ -1,112 +1,121 @@ --- title: "Roadmap" -description: "Current progress and upcoming features for the x402r protocol and SDK" +description: "What x402r has shipped, what's in flight, and what's planned next" icon: "map" --- ## Protocol -### V0 Demo (Completed) +### Shipped -- Client SDK wrapper and MCP tool -- NPM package published -- Refund request encryption (Lit Protocol) -- Basic refund flow (cancel, replace, approve/reject) +- **authCapture scheme** built on the audited [Commerce Payments Protocol](https://github.com/base/commerce-payments) (`AuthCaptureEscrow` + token collectors), used directly at universal CREATE2 addresses (no fork) +- **Two settlement paths**: two-phase `authorize` then `capture` (default), and single-shot `autoCapture` for atomic `charge` +- **Client signing**: ERC-3009 (`receiveWithAuthorization`) or Uniswap Permit2 (`permitTransferFrom`), selectable per request via `assetTransferMethod` +- **PaymentOperator** with pluggable pre-action conditions and post-action hooks for `void`, `refund`, `capture`, `charge` +- **EscrowPeriod**, **Freeze**, **RefundRequest**, and **RefundRequestEvidence** plugins for dispute and arbitration flows +- **Factory pattern** with CREATE2 deterministic deployment +- **Reference implementation**: [x402r-scheme](https://github.com/BackTrackCo/x402r-scheme) (`@x402r/evm`) +- **Solana (`@x402r/svm`)**: 6-program Anchor pilot landed alongside the EVM mechanism package. See [SVM Pilot](#solana-pilot-alpha) below. -### Protocol V2 (Completed) +### In flight -- Switched from proxy pattern to authCapture scheme on the canonical [Commerce Payments Protocol](https://github.com/base/commerce-payments) -- PaymentOperator with pluggable conditions and recorders -- EscrowPeriod and Freeze contracts -- Factory pattern with unified CREATE2 deterministic deployment -- RefundRequestEvidence for on-chain evidence submission -- Deployed across supported chains with unified addresses (same address everywhere) +- **authCapture scheme spec** moving toward upstream merge into the x402 protocol. See the [authCapture Scheme Specification](/x402-integration/auth-capture-scheme) for the wire format and verification logic. +- Documentation restructure and accuracy fixes (you are reading the result) +- Simple "API down" arbiter template for first merchants +- Tenjin: description-vs-content arbiter using an LLM in a TEE -### authCapture Scheme Spec (In Progress) +### Planned -- Reference implementation: [x402r-scheme](https://github.com/BackTrackCo/x402r-scheme) -- See the [authCapture Scheme Specification](/x402-integration/auth-capture-scheme) for the wire format and verification logic +- Bond-based disputes +- Multi-level verification (L1 schema/auto, L2 AI review, L3 human arbitration) with per-tier `MaxAmountCondition` exposure caps +- Multiple arbiters per operator and arbiter marketplace +- After-capture arbitration handling +- Reputation system for clients, merchants, and arbiters (via ERC-8004) +- Token wrapper for enforced refund protection -### Developer Experience (In Progress) +--- -- Documentation restructure and accuracy fixes -- LLM-friendly docs with MCP integration -- Simple "API Down" arbiter template for first merchants -- CI/CD pipeline for SDK and contracts +## Solana Pilot (Alpha) -### Protocol Extensions (Future) +Solana support is **alpha/experimental**. The Anchor programs and TypeScript mechanism package are implemented, but the pilot is **unaudited** and not yet deployed to mainnet-beta. Use at your own risk; flag any production use to the team first. -- Bond-based disputes -- Multiple arbiter support per operator -- After capture arbitration handling -- Reputation system for clients, merchants, and arbiters -- Arbiter marketplace -- Token wrapper for enforced refund protection +### Shipped ---- +- Six Anchor programs at [x402r-contracts-svm](https://github.com/BackTrackCo/x402r-contracts-svm): `authCapture` escrow with `authorize`, `charge`, `capture`, `void`, `refund`, `reclaim` instructions, plus address-match condition programs (`static-address`, `receiver`, `payer`) +- `@x402r/svm` TypeScript package (Solana Kit + Codama-generated client) mirroring `@x402r/evm`'s client/server/facilitator/shared shape +- Plugin slot plumbing: 5 pre-action `condition_programs` and 5 post-action `hook_programs` per `PaymentInfo`, with canonical `ICondition` / `IHook` instruction signatures so third-party arbiter programs slot in without escrow changes +- Fee enforcement (protocol + operator splits) parity with EVM -## SDK +### Planned -### Phase 0: Operator Deployment (Completed) +- Devnet + mainnet-beta deployment +- Devnet demo exercising all three lifecycle paths (`authorize, capture`, `authorize, void`, `charge, refund`) with a sample arbiter program +- `scheme_authCapture_svm.md` spec doc upstream -- Fixed all SDK ABIs to match contracts -- Factory deployment helpers (`deployMarketplaceOperator`) -- Condition composition (AND/OR/NOT combinators) -- Deterministic address computation (CREATE2) -- Deployed all contracts on Base Sepolia and Base Mainnet +--- -### Phase 1: MVP Examples (Completed) +## SDK -- 8 examples (deploy-operator, facilitator, server-express, server-hono, merchant-cli, client-cli, arbiter-cli, shared) -- Full e2e dispute resolution flow +The SDK lives at [x402r-sdk](https://github.com/BackTrackCo/x402r-sdk). All packages target the canonical `authCapture` contract surface and the `@x402r/evm` v0.2.x scheme adapter. -### Phase 2: Core SDK (Completed) +### Shipped -- `@x402r/client`, Refund requests, freeze, escrow period queries, subscriptions -- `@x402r/merchant`, Release, charge, voidPayment, refund, refund handling -- `@x402r/arbiter`, Decision submission, batch operations, registry, AI hooks -- `@x402r/helpers`, `refundable()` helper for payment options -- `@x402r/core`, Types, ABIs, config, deploy utilities -- 310+ tests across all packages +| Package | Purpose | +|---|---| +| `@x402r/core` | Stateless viem-style action functions (payment, operator, refund, evidence, escrow, freeze, watch) | +| `@x402r/sdk` | Stateful client with action groups and a `.extend()` plugin pattern | +| `@x402r/helpers` | `x402rDefaults()` and wire-format re-exports | +| `@x402r/cli` | Wallet-agnostic one-shot payment CLI (0.2.0+) | -### Phase 3: Client UX (Upcoming) +Other shipped capabilities: -- Pre-payment info extraction (`getOperatorInfo`, discover arbiter, escrow period from operator address) -- Combined freeze + refund (`freezeAndRequestRefund`, single call) -- Condition awareness for clients +- **ERC-8004 plugin**: identity, reputation, and discovery actions +- **ERC-8004 extraction helpers** with `identity.check()` +- **Anvil fork testing** via prool, 80% coverage threshold in CI +- **Biome** + **@wagmi/cli** ABI codegen against the contracts +- Role-based presets: `createPayerClient()`, `createMerchantClient()`, `createArbiterClient()` -### Phase 4: Subgraph Integration (Upcoming) +### In flight -- Deploy subgraph for payment event indexing -- Implement 8 stubbed methods (`getPaymentState`, `getMyPayments`, etc.) -- Historical payment listing for all roles +- Pre-payment info extraction (`getOperatorInfo`: discover arbiter and escrow period from an operator address) +- Combined freeze + refund (`freezeAndRequestRefund` single call) +- Subgraph integration for historical payment listing and stubbed query methods -### Future SDK Work +### Planned -- Evidence/metadata system with pluggable backends (IPFS, Arweave) +- Evidence and metadata system with pluggable backends (IPFS, Arweave) - Encrypted communication channels (XMTP) - Session-based billing patterns - Multi-arbiter support -- Dedicated Express/Hono middleware +- Dedicated Express and Hono middleware +- Garbage-detection / multi-tier verification SDK extensions (paired with the arbiter work above) --- ## Contract Status -All contracts use **unified CREATE3 addresses**: same address on every supported chain (11 chains: Base, Ethereum, Polygon, Arbitrum, Optimism, Celo, Avalanche, Monad, Linea, Base Sepolia, Ethereum Sepolia). +All EVM contracts use **universal CREATE2 addresses**: when a chain is supported, the address is the same as on every other supported chain. The canonical commerce-payments primitives come from [`base/commerce-payments@v1.0.0`](https://github.com/base/commerce-payments/releases/tag/v1.0.0). + +### Supported chains + +Today: **Base mainnet** and **Base Sepolia**. Additional EVMs will be added as canonical `commerce-payments@v1.0.0` coverage from Base extends. + +Solana support is alpha and not yet on mainnet-beta. See [Solana Pilot (Alpha)](#solana-pilot-alpha). + +### Contract inventory | Contract | Status | -|----------|--------| -| AuthCaptureEscrow | Deployed | -| ERC3009PaymentCollector | Deployed | +|---|---| +| AuthCaptureEscrow (canonical) | Deployed | +| ERC3009PaymentCollector (canonical) | Deployed | +| Permit2PaymentCollector (canonical) | Deployed | | PaymentOperatorFactory | Deployed | | EscrowPeriodFactory | Deployed | | FreezeFactory | Deployed | | StaticFeeCalculatorFactory | Deployed | -| All condition/combinator factories | Deployed | +| All condition / combinator factories | Deployed | | ProtocolFeeConfig | Deployed | | RefundRequestEvidence | Deployed | -| ReceiverRefundCollector | Deployed | | Condition singletons (Payer, Receiver, AlwaysTrue) | Deployed | From 09a8265de5d3e7fd0fb893ff7c757e4e60cfccb7 Mon Sep 17 00:00:00 2001 From: A1igator Date: Mon, 11 May 2026 20:32:40 -0700 Subject: [PATCH 22/37] =?UTF-8?q?docs:=20comprehensive=20sweep=20=E2=80=94?= =?UTF-8?q?=20rename=20release=E2=86=92capture,=20drop=20multi-chain=20cla?= =?UTF-8?q?ims,=20fix=20scheme=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This sweep brings the rest of the docs (everything PR #35 hadn't already touched) into alignment with the current x402r-sdk + x402r-contracts + x402r-scheme repos. Covers ~35 files. Key axes: Method/symbol renames (current PaymentOperator + SDK API): - release() → capture() (operator and SDK action) - AUTHORIZE_CONDITION / RELEASE_CONDITION / CHARGE_CONDITION → AUTHORIZE_PRE_ACTION_CONDITION / CAPTURE_PRE_ACTION_CONDITION / CHARGE_PRE_ACTION_CONDITION - AUTHORIZE_RECORDER / RELEASE_RECORDER / CHARGE_RECORDER → AUTHORIZE_POST_ACTION_HOOK / CAPTURE_POST_ACTION_HOOK / CHARGE_POST_ACTION_HOOK - camelCase config fields: releaseCondition→capturePreActionCondition, releaseRecorder→capturePostActionHook (and authorize/charge variants) - Event renames: ReleaseExecuted→CaptureExecuted, AuthorizationCreated→AuthorizeExecuted Scheme + class names (matches @x402r/evm v0.2.x): - EscrowServerScheme → AuthCaptureServerScheme - EscrowFacilitatorScheme → AuthCaptureFacilitatorScheme - EscrowClientScheme / EscrowEvmScheme → AuthCaptureEvmScheme - @x402r/evm/escrow/{client,server,facilitator} → @x402r/evm/authCapture/{client,server,facilitator} - scheme: "escrow" → scheme: "authCapture" (in example JSON + TS) - settlementMethod: "authorize"|"charge" → autoCapture: false|true Chain support: corrected to Base + Base Sepolia everywhere (was claiming 11 chains via unified CREATE3, mismatch with x402r-sdk/packages/core/src/config). All "CREATE3" → "CREATE2". Optimism Sepolia and other non-Base examples replaced. Wire format fixes (extra shape): - merchant getting-started.mdx and comparison.mdx examples were using old extra fields (escrowAddress, operatorAddress, feeReceiver on the wire). Rewritten to current shape: name, version, captureAuthorizer, captureDeadline, refundDeadline, feeRecipient, minFeeBps, maxFeeBps, optional autoCapture/assetTransferMethod. - payment-operator.mdx void() signature corrected to current (paymentInfo, bytes data), no amount param. Canonical commerce-payments page rewritten: - Correct CREATE2 addresses (was showing legacy CREATE3 addresses) - Added Permit2PaymentCollector section - Real method signatures (no fake onlyOperator modifier; access is captureAuthorizer via PaymentInfo.operator) - State machine fixed: void() instead of void()/refundInEscrow() Other cleanup: - SDK overview cards: removed @x402r/merchant / @x402r/arbiter as separate packages (those packages don't exist; the API is in @x402r/sdk via role presets), labelled as guides instead. - Cross-org ping in helpers/erc8004.mdx (x402-foundation/x402#931) removed per the no-cross-org-pings rule. - 22 more em dashes stripped (docs/CLAUDE.md style rule). Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/architecture.mdx | 22 +-- contracts/conditions/always-true.mdx | 4 +- contracts/conditions/combinators.mdx | 12 +- contracts/conditions/custom.mdx | 10 +- contracts/conditions/escrow-period.mdx | 24 +-- contracts/conditions/freeze.mdx | 14 +- contracts/conditions/overview.mdx | 26 ++-- contracts/conditions/payer.mdx | 2 +- contracts/conditions/receiver.mdx | 4 +- contracts/examples.mdx | 128 +++++++-------- contracts/factories.mdx | 108 +++++-------- contracts/overview.mdx | 8 +- contracts/payment-operator.mdx | 38 ++--- contracts/periphery/auth-capture-escrow.mdx | 163 ++++++++++++-------- contracts/periphery/overview.mdx | 2 +- contracts/recorders/combinator.mdx | 4 +- contracts/recorders/overview.mdx | 12 +- index.mdx | 20 +-- sdk/arbiter/subscriptions.mdx | 4 +- sdk/client/payment-queries.mdx | 2 +- sdk/client/subscriptions.mdx | 8 +- sdk/delivery-arbiter.mdx | 2 +- sdk/deploy-operator.mdx | 59 +++---- sdk/examples.mdx | 6 +- sdk/facilitator/getting-started.mdx | 2 +- sdk/helpers/erc8004.mdx | 5 +- sdk/helpers/forward-to-arbiter.mdx | 11 +- sdk/limitations.mdx | 4 +- sdk/merchant.mdx | 2 +- sdk/merchant/getting-started.mdx | 41 +++-- sdk/merchant/payment-operations.mdx | 10 +- sdk/merchant/quickstart.mdx | 12 +- sdk/merchant/subscriptions.mdx | 6 +- sdk/overview.mdx | 45 ++---- x402-integration/comparison.mdx | 24 +-- 35 files changed, 411 insertions(+), 433 deletions(-) diff --git a/contracts/architecture.mdx b/contracts/architecture.mdx index 4f8817e..86646d0 100644 --- a/contracts/architecture.mdx +++ b/contracts/architecture.mdx @@ -67,16 +67,16 @@ For additional visual diagrams, see the [x402r-contracts repository](https://git ### Standard Payment (Happy Path) 1. **Payer** calls `operator.authorize(paymentInfo, amount, tokenCollector, collectorData)` -2. **Operator** checks `AUTHORIZE_CONDITION` (if set) +2. **Operator** checks `AUTHORIZE_PRE_ACTION_CONDITION` (if set) 3. **Operator** validates fee bounds and stores fees at authorization time 4. **Operator** calls `escrow.authorize()` to lock funds -5. **Operator** calls `AUTHORIZE_RECORDER` to record timestamp +5. **Operator** calls `AUTHORIZE_POST_ACTION_HOOK` to record timestamp 6. **Escrow period** begins (e.g., 7 days) - if configured -7. After escrow period: **Authorized address(es)** call `operator.release(paymentInfo, amount)` (e.g., receiver, designated address, or both) -8. **Operator** checks `RELEASE_CONDITION` (configurable - can include time checks, role checks, etc.) +7. After escrow period: **Authorized address(es)** call `operator.capture(paymentInfo, amount)` (e.g., receiver, designated address, or both) +8. **Operator** checks `CAPTURE_PRE_ACTION_CONDITION` (configurable - can include time checks, role checks, etc.) 9. **Operator** calls `escrow.capture()` to transfer funds to receiver 10. **Operator** accumulates protocol fees for later distribution -11. **Operator** calls `RELEASE_RECORDER` to update state +11. **Operator** calls `CAPTURE_POST_ACTION_HOOK` to update state ### Void Flow (before capture) @@ -121,7 +121,7 @@ Freeze policies are optional and configurable. Define who can freeze, who can un ### Authorization Check (Before Action) -When an action is called (e.g., `release()`): +When an action is called (e.g., `capture()`): 1. **Load Condition** - Get the condition address from operator slot 2. **Evaluate Condition** - Call `condition.check(paymentInfo, amount, caller)` @@ -223,8 +223,8 @@ Fees accumulate in the operator and are distributed via `distributeFees(token)`. | Role | Capabilities | Restrictions | |------|-------------|--------------| | **Payer** | `authorize()`, `freeze()`, `unfreeze()`, `requestRefund()`, `cancelRefundRequest()` | Can only act on own payments | -| **Receiver** | `release()` (if condition allows), `charge()`, `requestRefund()` | Can only act on payments where they are receiver | -| **Designated Address** | Any action per conditions (e.g., `voidPayment()`, `release()`, `updateStatus()`) | Defined by StaticAddressCondition (arbiter, DAO, service provider, etc.) | +| **Receiver** | `capture()` (if condition allows), `charge()`, `requestRefund()` | Can only act on payments where they are receiver | +| **Designated Address** | Any action per conditions (e.g., `voidPayment()`, `capture()`, `updateStatus()`) | Defined by StaticAddressCondition (arbiter, DAO, service provider, etc.) | | **Protocol Owner** | `queueCalculator()`, `executeCalculator()`, `queueRecipient()`, `executeRecipient()` | 7-day timelock on ProtocolFeeConfig changes | @@ -289,15 +289,15 @@ Ownership transfers use Solady's Ownable pattern: ```solidity // Payment lifecycle (PaymentOperator events) -event AuthorizationCreated(bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount, uint256 timestamp); +event AuthorizeExecuted(bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount, uint256 timestamp); event ChargeExecuted(bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount, uint256 timestamp); -event ReleaseExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, uint256 amount, uint256 timestamp); +event CaptureExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, uint256 amount, uint256 timestamp); event VoidExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, address indexed payer, uint256 amount); event RefundExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, address indexed payer, uint256 amount); // Fee distribution event FeesDistributed(address indexed token, uint256 protocolAmount, uint256 arbiterAmount); -event OperatorDeployed(address indexed operator, address indexed feeRecipient, address indexed releaseCondition); +event OperatorDeployed(address indexed operator, address indexed feeRecipient, address indexed capturePreActionCondition); // Freeze state (Freeze contract events) event PaymentFrozen(bytes32 indexed paymentInfoHash, uint40 frozenAt); diff --git a/contracts/conditions/always-true.mdx b/contracts/conditions/always-true.mdx index 021e7ce..31ea543 100644 --- a/contracts/conditions/always-true.mdx +++ b/contracts/conditions/always-true.mdx @@ -28,10 +28,10 @@ function check(PaymentInfo calldata payment, uint256, address caller) | Slot | Use Case | |------|----------| -| `AUTHORIZE_CONDITION` | Let anyone create payments (common for marketplace/e-commerce) | +| `AUTHORIZE_PRE_ACTION_CONDITION` | Let anyone create payments (common for marketplace/e-commerce) | -**Use with caution for release/refund slots.** Setting `RELEASE_CONDITION` or `VOID_PRE_ACTION_CONDITION` to AlwaysTrueCondition means anyone can release or refund funds. This is functionally equivalent to leaving the slot as `address(0)` (the default behavior), but makes the intent explicit. +**Use with caution for release/refund slots.** Setting `CAPTURE_PRE_ACTION_CONDITION` or `VOID_PRE_ACTION_CONDITION` to AlwaysTrueCondition means anyone can release or refund funds. This is functionally equivalent to leaving the slot as `address(0)` (the default behavior), but makes the intent explicit. ## AlwaysTrueCondition vs `address(0)` diff --git a/contracts/conditions/combinators.mdx b/contracts/conditions/combinators.mdx index 1ab5c22..5f00507 100644 --- a/contracts/conditions/combinators.mdx +++ b/contracts/conditions/combinators.mdx @@ -19,7 +19,7 @@ const comboAddress = await andConditionFactory.write.deploy([ ]); // Use in operator config -config.releaseCondition = comboAddress; +config.capturePreActionCondition = comboAddress; ``` **Example:** Release requires receiver AND escrow period passed. @@ -34,7 +34,7 @@ const comboAddress = await orConditionFactory.write.deploy([ [RECEIVER_CONDITION, ARBITER_CONDITION] ]); -config.releaseCondition = comboAddress; +config.capturePreActionCondition = comboAddress; ``` **Example:** Either receiver or arbiter can release. @@ -47,7 +47,7 @@ Inverts a condition (`!A`). // Anyone EXCEPT payer can call const comboAddress = await notConditionFactory.write.deploy([PAYER_CONDITION]); -config.releaseCondition = comboAddress; +config.capturePreActionCondition = comboAddress; ``` **Example:** Prevent payer from releasing their own payment. @@ -62,11 +62,11 @@ const receiverOrArbiter = await orConditionFactory.write.deploy([ [RECEIVER_CONDITION, ARBITER_CONDITION] ]); -const releaseCondition = await andConditionFactory.write.deploy([ +const capturePreActionCondition = await andConditionFactory.write.deploy([ [receiverOrArbiter, ESCROW_PERIOD_ADDRESS] ]); -config.releaseCondition = releaseCondition; +config.capturePreActionCondition = capturePreActionCondition; ``` **Logic Tree:** @@ -85,7 +85,7 @@ flowchart TD ## Limits -**Max 10 conditions per combinator.** Keep combinators simple — deeply nested trees increase gas costs and make debugging harder. +**Max 10 conditions per combinator.** Keep combinators simple, deeply nested trees increase gas costs and make debugging harder. ## Gas diff --git a/contracts/conditions/custom.mdx b/contracts/conditions/custom.mdx index dc3e6c9..9d1ef80 100644 --- a/contracts/conditions/custom.mdx +++ b/contracts/conditions/custom.mdx @@ -12,9 +12,9 @@ You can create custom conditions for specialized logic beyond what the built-in From the `ICondition.sol` NatSpec: -1. **MUST NOT revert** — return `false` to deny, never `revert` -2. **Should be `view` or `pure`** — no state-changing operations -3. **No external calls** — avoid calling other contracts to prevent reentrancy risks +1. **MUST NOT revert**: return `false` to deny, never `revert` +2. **Should be `view` or `pure`**: no state-changing operations +3. **No external calls**: avoid calling other contracts to prevent reentrancy risks 4. **Return `true` to allow, `false` to deny** The operator converts a `false` return into a `ConditionNotMet` revert. @@ -44,14 +44,14 @@ contract TimeOfDayCondition is ICondition { } ``` -Usage — deploy via a factory and use in operator config: +Usage, deploy via a factory and use in operator config: ```typescript // Deploy via your custom factory const businessHours = await timeOfDayConditionFactory.write.deploy([9, 17]); // Use in operator config -config.releaseCondition = businessHours; +config.capturePreActionCondition = businessHours; ``` ## Security Checklist diff --git a/contracts/conditions/escrow-period.mdx b/contracts/conditions/escrow-period.mdx index f97387f..cf7e59d 100644 --- a/contracts/conditions/escrow-period.mdx +++ b/contracts/conditions/escrow-period.mdx @@ -6,12 +6,12 @@ icon: "clock" ## Overview -EscrowPeriod is a dual-purpose contract — it functions as both a **recorder** and a **condition**: +EscrowPeriod is a dual-purpose contract, it functions as both a **recorder** and a **condition**: - **As a recorder:** Records the `block.timestamp` when a payment is authorized - **As a condition:** Returns `true` only after the escrow period has elapsed -Use the **same address** for both `AUTHORIZE_RECORDER` and `RELEASE_CONDITION` slots on the operator. +Use the **same address** for both `AUTHORIZE_POST_ACTION_HOOK` and `CAPTURE_PRE_ACTION_CONDITION` slots on the operator. **Type:** Per-deployment via [EscrowPeriodFactory](/contracts/factories) @@ -24,12 +24,12 @@ flowchart LR ATR -->|implements| IR[IRecorder] ``` -EscrowPeriod extends [AuthorizationTimeRecorder](/contracts/recorders/authorization-time) and adds `ICondition` implementation. You don't need to deploy AuthorizationTimeRecorder separately — use EscrowPeriod directly. +EscrowPeriod extends [AuthorizationTimeRecorder](/contracts/recorders/authorization-time) and adds `ICondition` implementation. You don't need to deploy AuthorizationTimeRecorder separately, use EscrowPeriod directly. ## Logic ```solidity -// ICondition — returns true when escrow period has passed +// ICondition, returns true when escrow period has passed function check( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, uint256, @@ -68,8 +68,8 @@ Then configure the operator: ```typescript const config = { - authorizeRecorder: escrowPeriodAddress, // Record auth time - releaseCondition: escrowPeriodAddress, // Check escrow passed + authorizePostActionHook: escrowPeriodAddress, // Record auth time + capturePreActionCondition: escrowPeriodAddress, // Check escrow passed // ... }; ``` @@ -79,16 +79,16 @@ const config = { For freeze functionality, deploy a separate [Freeze](/contracts/conditions/freeze) condition and compose via [AndCondition](/contracts/conditions/combinators): ```solidity -// Escrow period only: releaseCondition = escrowPeriod -// Freeze only: releaseCondition = freeze -// Both: releaseCondition = AndCondition([escrowPeriod, freeze]) +// Escrow period only: capturePreActionCondition = escrowPeriod +// Freeze only: capturePreActionCondition = freeze +// Both: capturePreActionCondition = AndCondition([escrowPeriod, freeze]) ``` ## Use Cases -- **Time-lock releases** — 7-day escrow for e-commerce -- **Delayed fund access** — Grace period before receiver can access funds -- **Buyer protection** — Give payers time to freeze or request refunds +- **Time-lock releases**: 7-day escrow for e-commerce +- **Delayed fund access**: Grace period before receiver can access funds +- **Buyer protection**: Give payers time to freeze or request refunds ## Gas diff --git a/contracts/conditions/freeze.mdx b/contracts/conditions/freeze.mdx index 9297f49..5d3b796 100644 --- a/contracts/conditions/freeze.mdx +++ b/contracts/conditions/freeze.mdx @@ -19,7 +19,7 @@ Freeze is a standalone condition that blocks release when a payment is frozen. I ## Logic ```solidity -// ICondition — returns false when frozen (blocks release) +// ICondition, returns false when frozen (blocks release) function check( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, uint256, @@ -46,9 +46,9 @@ const freeze = await freezeFactory.deploy( ## Composition Patterns ```solidity -// Escrow period only: releaseCondition = escrowPeriod -// Freeze only: releaseCondition = freeze -// Both: releaseCondition = AndCondition([escrowPeriod, freeze]) +// Escrow period only: capturePreActionCondition = escrowPeriod +// Freeze only: capturePreActionCondition = freeze +// Both: capturePreActionCondition = AndCondition([escrowPeriod, freeze]) ``` Use [AndCondition](/contracts/conditions/combinators) to require both escrow period elapsed **and** not frozen before release. @@ -74,9 +74,9 @@ Freeze duration should balance payer protection with receiver UX. Too long and r ## Use Cases -- **Buyer protection** — Payer freezes suspicious payments during escrow -- **Dispute holds** — Arbiter freezes payments pending investigation -- **Compliance** — Compliance officer freezes flagged transactions +- **Buyer protection**: Payer freezes suspicious payments during escrow +- **Dispute holds**: Arbiter freezes payments pending investigation +- **Compliance**: Compliance officer freezes flagged transactions ## Gas diff --git a/contracts/conditions/overview.mdx b/contracts/conditions/overview.mdx index 0c8fcaa..cf798e7 100644 --- a/contracts/conditions/overview.mdx +++ b/contracts/conditions/overview.mdx @@ -10,9 +10,9 @@ Conditions are pluggable contracts that control who can perform actions on a Pay | Slot | Controls | |------|----------| -| `AUTHORIZE_CONDITION` | Who can authorize payments | -| `CHARGE_CONDITION` | Who can charge partial amounts | -| `RELEASE_CONDITION` | Who can release from escrow | +| `AUTHORIZE_PRE_ACTION_CONDITION` | Who can authorize payments | +| `CHARGE_PRE_ACTION_CONDITION` | Who can charge partial amounts | +| `CAPTURE_PRE_ACTION_CONDITION` | Who can release from escrow | | `VOID_PRE_ACTION_CONDITION` | Who can refund during escrow | | `REFUND_PRE_ACTION_CONDITION` | Who can refund after release | @@ -67,9 +67,9 @@ Conditions compose to create flexible authorization policies. Here are common pa ```solidity config = { - authorizeCondition: ALWAYS_TRUE_CONDITION, // Anyone can authorize - authorizeRecorder: escrowRecorder, // Record time - releaseCondition: releaseCondition, // Restricted + authorizePreActionCondition: ALWAYS_TRUE_CONDITION, // Anyone can authorize + authorizePostActionHook: escrowRecorder, // Record time + capturePreActionCondition: capturePreActionCondition, // Restricted // ... }; ``` @@ -78,8 +78,8 @@ config = { ```solidity config = { - authorizeCondition: PAYER_CONDITION, // Only payer - releaseCondition: PAYER_CONDITION, // Only payer + authorizePreActionCondition: PAYER_CONDITION, // Only payer + capturePreActionCondition: PAYER_CONDITION, // Only payer // ... }; ``` @@ -88,9 +88,9 @@ config = { ```solidity config = { - authorizeCondition: ARBITER_CONDITION, // Only arbiter - chargeCondition: ARBITER_CONDITION, // Only arbiter - releaseCondition: ARBITER_CONDITION, // Only arbiter + authorizePreActionCondition: ARBITER_CONDITION, // Only arbiter + chargePreActionCondition: ARBITER_CONDITION, // Only arbiter + capturePreActionCondition: ARBITER_CONDITION, // Only arbiter voidPreActionCondition: ARBITER_CONDITION, // Only arbiter refundPreActionCondition: ARBITER_CONDITION, // ... @@ -107,8 +107,8 @@ Singleton conditions are deployed once and reused by all operators. Reference th ```typescript // Good: Reference the singleton address -const config1 = { authorizeCondition: PAYER_CONDITION }; -const config2 = { authorizeCondition: PAYER_CONDITION }; // Same address +const config1 = { authorizePreActionCondition: PAYER_CONDITION }; +const config2 = { authorizePreActionCondition: PAYER_CONDITION }; // Same address ``` ### Stateless Conditions diff --git a/contracts/conditions/payer.mdx b/contracts/conditions/payer.mdx index 63d663c..5e09905 100644 --- a/contracts/conditions/payer.mdx +++ b/contracts/conditions/payer.mdx @@ -30,7 +30,7 @@ The condition compares `caller` against `payment.payer`, pure computation with n | Slot | Use Case | |------|----------| -| `AUTHORIZE_CONDITION` | Let payer create payments (subscriptions, invoices) | +| `AUTHORIZE_PRE_ACTION_CONDITION` | Let payer create payments (subscriptions, invoices) | | `VOID_PRE_ACTION_CONDITION` | Let payer request refunds during escrow | | `REFUND_PRE_ACTION_CONDITION` | Let payer cancel streams | diff --git a/contracts/conditions/receiver.mdx b/contracts/conditions/receiver.mdx index a286435..f24a8cb 100644 --- a/contracts/conditions/receiver.mdx +++ b/contracts/conditions/receiver.mdx @@ -30,8 +30,8 @@ The condition compares `caller` against `payment.receiver`, pure computation wit | Slot | Use Case | |------|----------| -| `RELEASE_CONDITION` | Let receiver release funds after escrow | -| `CHARGE_CONDITION` | Let receiver charge partial amounts | +| `CAPTURE_PRE_ACTION_CONDITION` | Let receiver release funds after escrow | +| `CHARGE_PRE_ACTION_CONDITION` | Let receiver charge partial amounts | | `VOID_PRE_ACTION_CONDITION` | Let receiver voluntarily refund | diff --git a/contracts/examples.mdx b/contracts/examples.mdx index efd2bf3..5ee17c1 100644 --- a/contracts/examples.mdx +++ b/contracts/examples.mdx @@ -63,7 +63,7 @@ The configuration examples below use simplified pseudo-code (e.g., `new StaticAd freeze // Not frozen ]); - const releaseCondition = await new AndCondition([ + const capturePreActionCondition = await new AndCondition([ receiverOrArbiter, escrowAndFreeze ]); @@ -75,12 +75,12 @@ The configuration examples below use simplified pseudo-code (e.g., `new StaticAd const config = { feeRecipient: arbiterAddress, // Arbiter earns fees for dispute resolution feeCalculator: feeCalculatorAddress, // Operator fee calculator - authorizeCondition: ALWAYS_TRUE_CONDITION, - authorizeRecorder: escrowPeriod, // Same address for recording auth time - chargeCondition: RECEIVER_CONDITION, - chargeRecorder: '0x0000000000000000000000000000000000000000', - releaseCondition: releaseCondition, - releaseRecorder: '0x0000000000000000000000000000000000000000', + authorizePreActionCondition: ALWAYS_TRUE_CONDITION, + authorizePostActionHook: escrowPeriod, // Same address for recording auth time + chargePreActionCondition: RECEIVER_CONDITION, + chargePostActionHook: '0x0000000000000000000000000000000000000000', + capturePreActionCondition: capturePreActionCondition, + capturePostActionHook: '0x0000000000000000000000000000000000000000', voidPreActionCondition: arbiterCondition.address, voidPostActionHook: '0x0000000000000000000000000000000000000000', refundPreActionCondition: arbiterCondition.address, @@ -137,12 +137,12 @@ const arbiterCondition = await new StaticAddressCondition(arbiterAddress); const config = { feeRecipient: arbiterAddress, // Arbiter earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator - authorizeCondition: '0x0000000000000000000000000000000000000000', // Not used (charge handles auth) - authorizeRecorder: '0x0000000000000000000000000000000000000000', - chargeCondition: RECEIVER_CONDITION, // Only receiver can charge - chargeRecorder: '0x0000000000000000000000000000000000000000', - releaseCondition: RECEIVER_CONDITION, // Fallback to release remaining - releaseRecorder: '0x0000000000000000000000000000000000000000', + authorizePreActionCondition: '0x0000000000000000000000000000000000000000', // Not used (charge handles auth) + authorizePostActionHook: '0x0000000000000000000000000000000000000000', + chargePreActionCondition: RECEIVER_CONDITION, // Only receiver can charge + chargePostActionHook: '0x0000000000000000000000000000000000000000', + capturePreActionCondition: RECEIVER_CONDITION, // Fallback to release remaining + capturePostActionHook: '0x0000000000000000000000000000000000000000', voidPreActionCondition: arbiterCondition.address, voidPostActionHook: '0x0000000000000000000000000000000000000000', refundPreActionCondition: arbiterCondition.address, @@ -195,7 +195,7 @@ const escrowPeriod = await escrowPeriodFactory.deploy( const freeze = await freezeFactory.deploy(freezePolicy, escrowPeriod); // Receiver OR Arbiter can release (after escrow + not frozen) -const releaseCondition = await new AndCondition([ +const capturePreActionCondition = await new AndCondition([ await new OrCondition([RECEIVER_CONDITION, arbiterCondition.address]), await new AndCondition([escrowPeriod, freeze]) ]); @@ -203,12 +203,12 @@ const releaseCondition = await new AndCondition([ const config = { feeRecipient: arbiterAddress, // Arbiter earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator - authorizeCondition: ALWAYS_TRUE_CONDITION, - authorizeRecorder: escrowPeriod, // Same address for recording auth time - chargeCondition: '0x0000000000000000000000000000000000000000', - chargeRecorder: '0x0000000000000000000000000000000000000000', - releaseCondition: releaseCondition, - releaseRecorder: '0x0000000000000000000000000000000000000000', + authorizePreActionCondition: ALWAYS_TRUE_CONDITION, + authorizePostActionHook: escrowPeriod, // Same address for recording auth time + chargePreActionCondition: '0x0000000000000000000000000000000000000000', + chargePostActionHook: '0x0000000000000000000000000000000000000000', + capturePreActionCondition: capturePreActionCondition, + capturePostActionHook: '0x0000000000000000000000000000000000000000', voidPreActionCondition: arbiterCondition.address, voidPostActionHook: '0x0000000000000000000000000000000000000000', refundPreActionCondition: arbiterCondition.address, @@ -270,7 +270,7 @@ const escrowPeriod = await escrowPeriodFactory.deploy( ); // Receiver can release after short escrow -const releaseCondition = await new AndCondition([ +const capturePreActionCondition = await new AndCondition([ RECEIVER_CONDITION, // Only receiver escrowPeriod // Escrow period passed ]); @@ -278,12 +278,12 @@ const releaseCondition = await new AndCondition([ const config = { feeRecipient: arbiterAddress, // Arbiter earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator - authorizeCondition: PAYER_CONDITION, // Only payer authorizes - authorizeRecorder: escrowPeriod, // Record auth time - chargeCondition: RECEIVER_CONDITION, // Receiver can charge partials - chargeRecorder: '0x0000000000000000000000000000000000000000', - releaseCondition: releaseCondition, - releaseRecorder: '0x0000000000000000000000000000000000000000', + authorizePreActionCondition: PAYER_CONDITION, // Only payer authorizes + authorizePostActionHook: escrowPeriod, // Record auth time + chargePreActionCondition: RECEIVER_CONDITION, // Receiver can charge partials + chargePostActionHook: '0x0000000000000000000000000000000000000000', + capturePreActionCondition: capturePreActionCondition, + capturePostActionHook: '0x0000000000000000000000000000000000000000', voidPreActionCondition: arbiterCondition.address, voidPostActionHook: '0x0000000000000000000000000000000000000000', refundPreActionCondition: arbiterCondition.address, @@ -340,12 +340,12 @@ const arbiterCondition = await new StaticAddressCondition(arbiterAddress); const config = { feeRecipient: arbiterAddress, // Arbiter earns all fees feeCalculator: feeCalculatorAddress, // Operator fee calculator - authorizeCondition: arbiterCondition.address, // Arbiter creates payments - authorizeRecorder: '0x0000000000000000000000000000000000000000', - chargeCondition: arbiterCondition.address, // Arbiter charges - chargeRecorder: '0x0000000000000000000000000000000000000000', - releaseCondition: arbiterCondition.address, // Arbiter releases - releaseRecorder: '0x0000000000000000000000000000000000000000', + authorizePreActionCondition: arbiterCondition.address, // Arbiter creates payments + authorizePostActionHook: '0x0000000000000000000000000000000000000000', + chargePreActionCondition: arbiterCondition.address, // Arbiter charges + chargePostActionHook: '0x0000000000000000000000000000000000000000', + capturePreActionCondition: arbiterCondition.address, // Arbiter releases + capturePostActionHook: '0x0000000000000000000000000000000000000000', voidPreActionCondition: arbiterCondition.address, // Arbiter refunds voidPostActionHook: '0x0000000000000000000000000000000000000000', refundPreActionCondition: arbiterCondition.address, @@ -383,7 +383,7 @@ const escrowPeriod = await escrowPeriodFactory.deploy( ); // Receiver OR Arbiter can release -const releaseCondition = await new AndCondition([ +const capturePreActionCondition = await new AndCondition([ await new OrCondition([RECEIVER_CONDITION, arbiterCondition.address]), escrowPeriod // Escrow period passed ]); @@ -397,12 +397,12 @@ const refundCondition = await new OrCondition([ const config = { feeRecipient: arbiterAddress, // Arbiter earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator - authorizeCondition: ALWAYS_TRUE_CONDITION, - authorizeRecorder: escrowPeriod, // Record auth time - chargeCondition: '0x0000000000000000000000000000000000000000', - chargeRecorder: '0x0000000000000000000000000000000000000000', - releaseCondition: releaseCondition, - releaseRecorder: '0x0000000000000000000000000000000000000000', + authorizePreActionCondition: ALWAYS_TRUE_CONDITION, + authorizePostActionHook: escrowPeriod, // Record auth time + chargePreActionCondition: '0x0000000000000000000000000000000000000000', + chargePostActionHook: '0x0000000000000000000000000000000000000000', + capturePreActionCondition: capturePreActionCondition, + capturePostActionHook: '0x0000000000000000000000000000000000000000', voidPreActionCondition: refundCondition, // Receiver OR Arbiter voidPostActionHook: '0x0000000000000000000000000000000000000000', refundPreActionCondition: RECEIVER_CONDITION, // Only receiver after capture @@ -454,12 +454,12 @@ const providerCondition = await new StaticAddressCondition(serviceProviderAddres const config = { feeRecipient: serviceProviderAddress, // Service provider earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator - authorizeCondition: PAYER_CONDITION, // Payer sets up subscription - authorizeRecorder: '0x0000000000000000000000000000000000000000', - chargeCondition: providerCondition.address, // Provider charges monthly - chargeRecorder: '0x0000000000000000000000000000000000000000', - releaseCondition: providerCondition.address,// Provider releases - releaseRecorder: '0x0000000000000000000000000000000000000000', + authorizePreActionCondition: PAYER_CONDITION, // Payer sets up subscription + authorizePostActionHook: '0x0000000000000000000000000000000000000000', + chargePreActionCondition: providerCondition.address, // Provider charges monthly + chargePostActionHook: '0x0000000000000000000000000000000000000000', + capturePreActionCondition: providerCondition.address,// Provider releases + capturePostActionHook: '0x0000000000000000000000000000000000000000', voidPreActionCondition: '0x0000000000000000000000000000000000000000', // No refunds (no arbiter) voidPostActionHook: '0x0000000000000000000000000000000000000000', refundPreActionCondition: '0x0000000000000000000000000000000000000000', @@ -526,12 +526,12 @@ const daoCondition = await new StaticAddressCondition(DAO_MULTISIG_ADDRESS); const config = { feeRecipient: DAO_MULTISIG_ADDRESS, // DAO treasury earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator - authorizeCondition: daoCondition.address, // DAO authorizes grants - authorizeRecorder: '0x0000000000000000000000000000000000000000', - chargeCondition: '0x0000000000000000000000000000000000000000', - chargeRecorder: '0x0000000000000000000000000000000000000000', - releaseCondition: daoCondition.address, // DAO must approve releases - releaseRecorder: '0x0000000000000000000000000000000000000000', + authorizePreActionCondition: daoCondition.address, // DAO authorizes grants + authorizePostActionHook: '0x0000000000000000000000000000000000000000', + chargePreActionCondition: '0x0000000000000000000000000000000000000000', + chargePostActionHook: '0x0000000000000000000000000000000000000000', + capturePreActionCondition: daoCondition.address, // DAO must approve releases + capturePostActionHook: '0x0000000000000000000000000000000000000000', voidPreActionCondition: daoCondition.address, // DAO can refund if needed voidPostActionHook: '0x0000000000000000000000000000000000000000', refundPreActionCondition: '0x0000000000000000000000000000000000000000', // No refunds @@ -595,15 +595,15 @@ const timeProportionalCondition = await new TimeProportionalCondition(); const config = { feeRecipient: PLATFORM_ADDRESS, // Platform earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator - authorizeCondition: PAYER_CONDITION, // Payer authorizes stream - authorizeRecorder: '0x0000000000000000000000000000000000000000', - chargeCondition: new AndCondition([ + authorizePreActionCondition: PAYER_CONDITION, // Payer authorizes stream + authorizePostActionHook: '0x0000000000000000000000000000000000000000', + chargePreActionCondition: new AndCondition([ RECEIVER_CONDITION, timeProportionalCondition // Can only charge proportional to time ]), - chargeRecorder: '0x0000000000000000000000000000000000000000', - releaseCondition: RECEIVER_CONDITION, // Receiver releases remaining - releaseRecorder: '0x0000000000000000000000000000000000000000', + chargePostActionHook: '0x0000000000000000000000000000000000000000', + capturePreActionCondition: RECEIVER_CONDITION, // Receiver releases remaining + capturePostActionHook: '0x0000000000000000000000000000000000000000', voidPreActionCondition: PAYER_CONDITION, // Payer can cancel stream voidPostActionHook: '0x0000000000000000000000000000000000000000', refundPreActionCondition: '0x0000000000000000000000000000000000000000', // No refunds after charged @@ -667,12 +667,12 @@ const platformCondition = await new StaticAddressCondition(PLATFORM_ADDRESS); const config = { feeRecipient: PLATFORM_ADDRESS, // Platform earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator - authorizeCondition: PAYER_CONDITION, // Payer creates invoice payment - authorizeRecorder: '0x0000000000000000000000000000000000000000', - chargeCondition: RECEIVER_CONDITION, // Receiver charges on delivery - chargeRecorder: '0x0000000000000000000000000000000000000000', - releaseCondition: RECEIVER_CONDITION, // Receiver releases on payment terms - releaseRecorder: '0x0000000000000000000000000000000000000000', + authorizePreActionCondition: PAYER_CONDITION, // Payer creates invoice payment + authorizePostActionHook: '0x0000000000000000000000000000000000000000', + chargePreActionCondition: RECEIVER_CONDITION, // Receiver charges on delivery + chargePostActionHook: '0x0000000000000000000000000000000000000000', + capturePreActionCondition: RECEIVER_CONDITION, // Receiver releases on payment terms + capturePostActionHook: '0x0000000000000000000000000000000000000000', voidPreActionCondition: RECEIVER_CONDITION,// Receiver can refund (invoice error) voidPostActionHook: '0x0000000000000000000000000000000000000000', refundPreActionCondition: '0x0000000000000000000000000000000000000000', // No post-delivery refunds diff --git a/contracts/factories.mdx b/contracts/factories.mdx index 871b925..98907e1 100644 --- a/contracts/factories.mdx +++ b/contracts/factories.mdx @@ -46,7 +46,7 @@ Deploys PaymentOperator instances with deterministic addresses. ### Contract Address -All factories use unified CREATE3 addresses (same on every chain). +All factories use universal CREATE2 addresses (same on every chain). **PaymentOperatorFactory:** `0x4D9BC2Ba2D0d9AFb6B63E3afBbfC95143E6E8Da9` @@ -56,12 +56,12 @@ All factories use unified CREATE3 addresses (same on every chain). struct OperatorConfig { address feeRecipient; // Who receives operator fees address feeCalculator; // Operator fee calculator (IFeeCalculator) - address authorizeCondition; - address authorizeRecorder; - address chargeCondition; - address chargeRecorder; - address releaseCondition; - address releaseRecorder; + address authorizePreActionCondition; + address authorizePostActionHook; + address chargePreActionCondition; + address chargePostActionHook; + address capturePreActionCondition; + address capturePostActionHook; address voidPreActionCondition; address voidPostActionHook; address refundPreActionCondition; @@ -79,7 +79,7 @@ function deployOperator( **Parameters (in config):** - `feeRecipient` - Who receives operator fees (arbiter, service provider, treasury, etc.) -- `authorizeCondition` through `refundPostActionHook` - 10-slot configuration +- `authorizePreActionCondition` through `refundPostActionHook` - 10-slot configuration **Note:** `maxFeeBps` and `protocolFeePct` are set at factory level (shared across all operators) @@ -99,7 +99,7 @@ function computeAddress( ```typescript const config = { feeRecipient: arbiterAddress, - authorizeCondition: ALWAYS_TRUE_CONDITION, + authorizePreActionCondition: ALWAYS_TRUE_CONDITION, // ... rest of config }; @@ -143,21 +143,21 @@ const arbiterConditionHash = await staticAddressConditionFactory.write.deploy([a const arbiterConditionAddress = /* get from receipt */; // Deploy release condition: arbiter AND escrow period passed -const releaseConditionHash = await andConditionFactory.write.deploy([ +const capturePreActionConditionHash = await andConditionFactory.write.deploy([ [arbiterConditionAddress, escrowPeriodAddress] ]); -const releaseConditionAddress = /* get from receipt */; +const capturePreActionConditionAddress = /* get from receipt */; // Define configuration const config = { feeRecipient: arbiterAddress, // Arbiter earns fees feeCalculator: feeCalculatorAddress, - authorizeCondition: ALWAYS_TRUE_CONDITION, - authorizeRecorder: escrowPeriodAddress, - chargeCondition: zeroAddress, // Default allow - chargeRecorder: zeroAddress, // No recording - releaseCondition: releaseConditionAddress, - releaseRecorder: zeroAddress, + authorizePreActionCondition: ALWAYS_TRUE_CONDITION, + authorizePostActionHook: escrowPeriodAddress, + chargePreActionCondition: zeroAddress, // Default allow + chargePostActionHook: zeroAddress, // No recording + capturePreActionCondition: capturePreActionConditionAddress, + capturePostActionHook: zeroAddress, voidPreActionCondition: arbiterConditionAddress, voidPostActionHook: zeroAddress, refundPreActionCondition: arbiterConditionAddress, @@ -180,12 +180,12 @@ const providerCondition = await new StaticAddressCondition(serviceProviderAddres const config = { feeRecipient: serviceProviderAddress, // Provider earns fees - authorizeCondition: PAYER_CONDITION, - authorizeRecorder: zeroAddress, - chargeCondition: providerCondition.address, - chargeRecorder: zeroAddress, - releaseCondition: providerCondition.address, - releaseRecorder: zeroAddress, + authorizePreActionCondition: PAYER_CONDITION, + authorizePostActionHook: zeroAddress, + chargePreActionCondition: providerCondition.address, + chargePostActionHook: zeroAddress, + capturePreActionCondition: providerCondition.address, + capturePostActionHook: zeroAddress, voidPreActionCondition: zeroAddress, // No refunds voidPostActionHook: zeroAddress, refundPreActionCondition: zeroAddress, @@ -243,7 +243,7 @@ flowchart LR ``` -Use the SAME `EscrowPeriod` address for both `AUTHORIZE_RECORDER` and `RELEASE_CONDITION` slots on the operator. For freeze functionality, deploy a separate `Freeze` condition and compose via `AndCondition([escrowPeriod, freeze])`. +Use the SAME `EscrowPeriod` address for both `AUTHORIZE_POST_ACTION_HOOK` and `CAPTURE_PRE_ACTION_CONDITION` slots on the operator. For freeze functionality, deploy a separate `Freeze` condition and compose via `AndCondition([escrowPeriod, freeze])`. ### Example Deployment @@ -270,11 +270,11 @@ console.log("EscrowPeriod:", escrowPeriodAddress); // Use SAME address for both recorder and condition const config = { - authorizeCondition: ALWAYS_TRUE_CONDITION, - authorizeRecorder: escrowPeriodAddress, // Record auth time + authorizePreActionCondition: ALWAYS_TRUE_CONDITION, + authorizePostActionHook: escrowPeriodAddress, // Record auth time // ... - releaseCondition: escrowPeriodAddress, // Check escrow passed - releaseRecorder: zeroAddress, // No additional recording needed + capturePreActionCondition: escrowPeriodAddress, // Check escrow passed + capturePostActionHook: zeroAddress, // No additional recording needed // ... }; ``` @@ -336,14 +336,14 @@ const freeze = await freezeFactory.write.deploy([ ]); // Step 3: Compose with EscrowPeriod for release condition -const releaseCondition = await andConditionFactory.write.deploy([ +const capturePreActionCondition = await andConditionFactory.write.deploy([ [escrowPeriod, freeze] ]); // Use in operator config const config = { // ... - releaseCondition: releaseCondition, + capturePreActionCondition: capturePreActionCondition, // ... }; ``` @@ -491,12 +491,12 @@ Each factory uses different salt strategies: bytes32 key = keccak256(abi.encodePacked( config.feeRecipient, config.feeCalculator, - config.authorizeCondition, - config.authorizeRecorder, - config.chargeCondition, - config.chargeRecorder, - config.releaseCondition, - config.releaseRecorder, + config.authorizePreActionCondition, + config.authorizePostActionHook, + config.chargePreActionCondition, + config.chargePostActionHook, + config.capturePreActionCondition, + config.capturePostActionHook, config.voidPreActionCondition, config.voidPostActionHook, config.refundPreActionCondition, @@ -518,37 +518,7 @@ bytes32 salt = keccak256(abi.encodePacked("freeze", key)); ### Cross-Chain Addresses -Same configuration on different chains = same address: - -```typescript -import { createPublicClient } from 'viem'; -import { baseSepolia, optimismSepolia } from 'viem/chains'; - -// Deploy on Base Sepolia -const baseFactory = getContract({ - address: FACTORY_ADDRESS, - abi: PaymentOperatorFactory.abi, - client: baseWalletClient -}); - -const baseHash = await baseFactory.write.deployOperator([config]); -const baseReceipt = await baseWalletClient.waitForTransactionReceipt({ hash: baseHash }); -const addressBaseSepolia = baseReceipt.logs[0].address; - -// Deploy on Optimism Sepolia with identical params -const opFactory = getContract({ - address: FACTORY_ADDRESS, - abi: PaymentOperatorFactory.abi, - client: opWalletClient -}); - -const opHash = await opFactory.write.deployOperator([config]); -const opReceipt = await opWalletClient.waitForTransactionReceipt({ hash: opHash }); -const addressOptimismSepolia = opReceipt.logs[0].address; - -// Addresses are identical! -console.assert(addressBaseSepolia === addressOptimismSepolia); -``` +Because the factory uses CREATE2, the same configuration produces the same operator address on any chain where the factory itself lives at the canonical address. As supported chains expand beyond Base, an operator deployed with identical config will land at the same address on each new chain without the integrator needing per-chain bookkeeping. This enables: - Consistent addressing across chains @@ -577,14 +547,14 @@ Don't deploy new PayerCondition/ReceiverCondition - use existing singletons: ```typescript // ✅ Good: Reuse singleton const config = { - authorizeCondition: PAYER_CONDITION, // Pre-deployed singleton + authorizePreActionCondition: PAYER_CONDITION, // Pre-deployed singleton // ... }; // ❌ Bad: Deploy new instance const payerCondition = await new PayerCondition(); const config = { - authorizeCondition: payerCondition.address, // Wastes gas + authorizePreActionCondition: payerCondition.address, // Wastes gas // ... }; ``` diff --git a/contracts/overview.mdx b/contracts/overview.mdx index b861a5d..50e5bd0 100644 --- a/contracts/overview.mdx +++ b/contracts/overview.mdx @@ -107,7 +107,7 @@ x402r extends commerce-payments with flexible payment capabilities: ### 5. Factory Pattern -**PaymentOperatorFactory** - Deploys operators with deterministic CREATE3 addresses +**PaymentOperatorFactory** - Deploys operators with deterministic CREATE2 addresses **EscrowPeriodFactory** - Deploys EscrowPeriod contracts @@ -115,7 +115,7 @@ x402r extends commerce-payments with flexible payment capabilities: Plus factories for: StaticFeeCalculator, StaticAddressCondition, AndCondition, OrCondition, NotCondition, RecorderCombinator. -All factories use **unified CREATE3 addresses**: same address on every supported chain. +All factories use **universal CREATE2 addresses**: same address on every supported chain. **Benefits:** - Predictable addresses for off-chain address generation @@ -133,9 +133,9 @@ All factories use **unified CREATE3 addresses**: same address on every supported | **Dispute Resolution** | Not built-in | Arbiter workflow via conditions, signatures, and evidence | | **Authorization** | Operator-based only | Pluggable conditions (access, time, signature, combinators) | | **Freeze Mechanism** | Not available | Configurable freeze during escrow period | -| **Deployment** | Direct deployment | Factory pattern with unified CREATE3 (same address every chain) | +| **Deployment** | Direct deployment | Factory pattern with universal CREATE2 (same address every chain) | | **Fees** | Not enforced | Additive protocol + operator fees with 7-day timelock | -| **Multi-chain** | Per-chain deployment | Unified CREATE3 addresses across 11 chains | +| **Multi-chain** | Per-chain deployment | Universal CREATE2 addresses on supported chains | ## Use Cases diff --git a/contracts/payment-operator.mdx b/contracts/payment-operator.mdx index 9504373..5e7dbb9 100644 --- a/contracts/payment-operator.mdx +++ b/contracts/payment-operator.mdx @@ -36,9 +36,9 @@ mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees; ## 10-Slot Configuration -1. **AUTHORIZE_CONDITION** - Who can authorize payments -2. **CHARGE_CONDITION** - Who can charge partial amounts -3. **RELEASE_CONDITION** - Who can release from escrow +1. **AUTHORIZE_PRE_ACTION_CONDITION** - Who can authorize payments +2. **CHARGE_PRE_ACTION_CONDITION** - Who can charge partial amounts +3. **CAPTURE_PRE_ACTION_CONDITION** - Who can release from escrow 4. **VOID_PRE_ACTION_CONDITION** - Who can refund during escrow 5. **REFUND_PRE_ACTION_CONDITION** - Who can refund after release @@ -46,9 +46,9 @@ mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees; -1. **AUTHORIZE_RECORDER** - Record authorization (e.g., timestamp) -2. **CHARGE_RECORDER** - Record charge event -3. **RELEASE_RECORDER** - Record release +1. **AUTHORIZE_POST_ACTION_HOOK** - Record authorization (e.g., timestamp) +2. **CHARGE_POST_ACTION_HOOK** - Record charge event +3. **CAPTURE_POST_ACTION_HOOK** - Record release 4. **VOID_POST_ACTION_HOOK** - Record void 5. **REFUND_POST_ACTION_HOOK** - Record refund @@ -77,14 +77,14 @@ function authorize( - `collectorData` - Data to pass to the token collector **Flow:** -1. Check `AUTHORIZE_CONDITION` (if set) +1. Check `AUTHORIZE_PRE_ACTION_CONDITION` (if set) 2. Validate fee bounds compatibility 3. Store fees at authorization time (prevents protocol fee changes from breaking capture) 4. Call `escrow.authorize()` -5. Call `AUTHORIZE_RECORDER` (if set) -6. Emit `AuthorizationCreated` +5. Call `AUTHORIZE_POST_ACTION_HOOK` (if set) +6. Emit `AuthorizeExecuted` -**Access:** Controlled by `AUTHORIZE_CONDITION` (default: anyone) +**Access:** Controlled by `AUTHORIZE_PRE_ACTION_CONDITION` (default: anyone) **Authorization Expiry:** The `PaymentInfo` struct includes an `authorizationExpiry` field (from base commerce-payments). Set this to `type(uint48).max` for no expiry, or specify a timestamp to allow the payer to reclaim funds after expiry. This is useful for subscription-based payments where you want to limit the authorization window. @@ -110,25 +110,25 @@ function charge( - `collectorData` - Data to pass to the token collector **Flow:** -1. Check `CHARGE_CONDITION` (if set) +1. Check `CHARGE_PRE_ACTION_CONDITION` (if set) 2. Validate fee bounds compatibility 3. Call `escrow.charge()` - funds go directly to receiver 4. Accumulate protocol fees for later distribution -5. Call `CHARGE_RECORDER` (if set) +5. Call `CHARGE_POST_ACTION_HOOK` (if set) 6. Emit `ChargeExecuted` -**Access:** Controlled by `CHARGE_CONDITION` (default: anyone) +**Access:** Controlled by `CHARGE_PRE_ACTION_CONDITION` (default: anyone) Unlike `authorize()`, funds go directly to receiver without escrow hold. Refunds are only possible via `refund()`. -### release() +### capture() Releases funds from escrow to receiver (capture). ```solidity -function release( +function capture( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, uint256 amount ) external nonReentrant @@ -139,14 +139,14 @@ function release( - `amount` - Amount to release **Flow:** -1. Check `RELEASE_CONDITION` (if set) +1. Check `CAPTURE_PRE_ACTION_CONDITION` (if set) 2. Use fees stored at authorization time 3. Call `escrow.capture()` 4. Accumulate protocol fees for later distribution -5. Call `RELEASE_RECORDER` (if set) -6. Emit `ReleaseExecuted` +5. Call `CAPTURE_POST_ACTION_HOOK` (if set) +6. Emit `CaptureExecuted` -**Access:** Controlled by `RELEASE_CONDITION` +**Access:** Controlled by `CAPTURE_PRE_ACTION_CONDITION` **Marketplace example:** Receiver OR StaticAddressCondition(arbiter) + escrow passed diff --git a/contracts/periphery/auth-capture-escrow.mdx b/contracts/periphery/auth-capture-escrow.mdx index ac9dc4c..5d896e0 100644 --- a/contracts/periphery/auth-capture-escrow.mdx +++ b/contracts/periphery/auth-capture-escrow.mdx @@ -1,114 +1,138 @@ --- title: "Commerce Payments" -description: "AuthCaptureEscrow and ERC3009PaymentCollector, the base layer from commerce-payments" +description: "AuthCaptureEscrow and the canonical token collectors that form the authCapture base layer" icon: "vault" --- -x402r builds on the canonical [Commerce Payments Protocol](https://github.com/base/commerce-payments) (no fork). Two contracts from this stack form the base layer: **AuthCaptureEscrow** (holds funds) and **ERC3009PaymentCollector** (collects funds via signed authorizations). Both are deployed at universal CREATE2 addresses. +x402r builds on the canonical [Commerce Payments Protocol](https://github.com/base/commerce-payments) (no fork). Three contracts from this stack form the base layer: + +- **AuthCaptureEscrow**: singleton escrow that holds funds and gates lifecycle actions on the `captureAuthorizer` (committed on-chain as `PaymentInfo.operator`). +- **ERC3009PaymentCollector**: collects funds via signed ERC-3009 `receiveWithAuthorization`. +- **Permit2PaymentCollector**: collects funds via Uniswap Permit2 `permitTransferFrom`. + +All three are deployed at universal CREATE2 addresses (same address on every supported chain). + +| Contract | Canonical address | +|---|---| +| `AuthCaptureEscrow` | `0xF8211868187974a7Fb9d99b8fFB171AD70665Dc6` | +| `ERC3009PaymentCollector` | `0x7561DC178D9aD5bc5fb103C01f448A510d2A36D0` | +| `Permit2PaymentCollector` | `0xD8490609d2da0ee626b0e676941b225cbc1A8C08` | ## AuthCaptureEscrow Core escrow contract for holding ERC-20 tokens during the payment lifecycle. -- **Type:** Singleton (one per network) -- **Access:** Operator-based (only registered operators can manage payments) -- **Address:** `0xe050bB89eD43BB02d71343063824614A7fb80B77` (all chains) - ### Payment State Machine ```mermaid stateDiagram-v2 [*] --> NonExistent NonExistent --> InEscrow: authorize() - InEscrow --> Released: release() + InEscrow --> Captured: capture() InEscrow --> Settled: void() - Released --> Settled: reclaim() / refund() + Captured --> Settled: reclaim() / refund() Settled --> [*] note right of InEscrow - Funds locked in escrow - Payer can reclaim after expiry + Funds locked in escrow. + Payer can reclaim after captureDeadline. end note - note right of Released - Funds transferred to receiver - Can still refund + note right of Captured + Funds transferred to receiver. + Can still refund within refundDeadline. end note note right of Settled - Terminal state - No further actions possible + Terminal state. + No further actions possible. end note ``` -### Key Methods +### Key methods #### authorize() -Locks tokens in escrow. Called by operator. +Pulls funds into escrow via the token collector. Called by the `captureAuthorizer` (typically the facilitator EOA, or a smart contract acting as captureAuthorizer). ```solidity function authorize( - bytes32 paymentId, - address payer, - address receiver, + PaymentInfo calldata paymentInfo, uint256 amount, - address token, - address operator -) external onlyOperator + address tokenCollector, + bytes calldata collectorData +) external ``` -**Requires:** Payer has approved escrow contract for `amount` of `token` - - -The base escrow contract uses individual parameters (paymentId, payer, receiver, etc.) while the PaymentOperator wraps them in a `PaymentInfo` struct. The operator translates between the two formats internally. - +`tokenCollector` is `ERC3009PaymentCollector` or `Permit2PaymentCollector` depending on `assetTransferMethod` in the scheme `extra`. `collectorData` carries the raw ERC-3009 signature or the ABI-encoded Permit2 signature. -#### release() +#### charge() -Releases tokens to receiver. Called by operator. +Single-shot atomic settlement: pulls funds and transfers directly to the receiver, no escrow hold. ```solidity -function release( - bytes32 paymentId -) external onlyOperator returns (uint256 amount) +function charge( + PaymentInfo calldata paymentInfo, + uint256 amount, + address tokenCollector, + bytes calldata collectorData +) external ``` -**State change:** `InEscrow` -> `Released` +#### capture() + +Releases escrowed funds to the receiver, minus fees. + +```solidity +function capture( + PaymentInfo calldata paymentInfo, + uint256 amount, + uint16 feeBps, + address feeReceiver +) external +``` #### void() -Returns tokens to payer (full refund). Called by operator. +Returns all escrowed funds to the payer. Full-only: `void()` empties the authorization in one transaction. ```solidity -function void( - bytes32 paymentId -) external onlyOperator +function void(PaymentInfo calldata paymentInfo) external ``` -**State change:** `InEscrow` -> `Settled` - #### reclaim() -Takes tokens back from receiver to give to payer. Called by operator. +Permissionless: the payer can pull funds back out of escrow after `captureDeadline` if the captureAuthorizer never captured. ```solidity -function reclaim( - bytes32 paymentId, - address from, - uint256 amount -) external onlyOperator +function reclaim(PaymentInfo calldata paymentInfo) external ``` -**State change:** `Released` -> `Settled` +#### refund() -**Requires:** Receiver has approved escrow for `amount` +Returns funds to the payer after capture, sourced via a token collector (typically pulled from the merchant's balance). -### Security Features +```solidity +function refund( + PaymentInfo calldata paymentInfo, + uint256 amount, + address tokenCollector, + bytes calldata collectorData +) external +``` + +### Access control + +Lifecycle actions (`authorize`, `charge`, `capture`, `void`, `refund`) check `msg.sender` against `PaymentInfo.operator` (the captureAuthorizer). `reclaim` is permissionless after `captureDeadline`. + +There is no escrow-wide "operator whitelist", access is per-payment via the signed `PaymentInfo`. + +### Security features -- **Operator whitelist** - Only registered operators can manage payments -- **Reentrancy protection** - All state changes protected -- **Event logging** - Complete audit trail +- **Replay prevention**: each payment has a unique nonce derived from `(chainId, escrowAddress, paymentInfoHash)`, consumed on-chain at settlement +- **Fee bounds enforcement**: `minFeeBps` / `maxFeeBps` / `feeReceiver` in `PaymentInfo` are signed by the client; the escrow rejects out-of-bounds captures/charges +- **Expiry ordering**: contract enforces `preApprovalExpiry <= authorizationExpiry <= refundExpiry` +- **Reentrancy protection** on all state-changing entry points --- @@ -116,35 +140,42 @@ function reclaim( Collects ERC-20 tokens into escrow using the client's off-chain ERC-3009 signature. The payer never submits a transaction. -- **Type:** Singleton (one per network) -- **Address:** `0xcE66Ab399EDA513BD12760b6427C87D6602344a7` (all chains) +### How it works -### How It Works - -The operator calls the token collector during `authorize()` or `charge()`, passing the client's signature as `collectorData`. The collector executes `receiveWithAuthorization` (ERC-3009) to pull tokens from the payer into escrow. +The escrow calls the token collector during `authorize()` or `charge()`, passing the client's signature as `collectorData`. The collector executes `receiveWithAuthorization` (ERC-3009) to pull tokens from the payer. ### Features -- **ERC-3009 `receiveWithAuthorization()`** - Gasless token transfers via signed messages -- **EIP-6492 support** - Handles smart wallet clients with deployment bytecode in signatures -- **Nonce-based replay protection** - Each authorization can only be used once -- **Deadline-based expiry** - `validBefore` timestamp prevents stale authorizations +- **ERC-3009 `receiveWithAuthorization()`**: gasless token transfers via signed messages +- **EIP-6492 support**: handles smart wallet clients with deployment bytecode in signatures (via `ERC6492SignatureHandler`) +- **Nonce-based replay protection**: each authorization can only be used once; the nonce is the payer-agnostic `PaymentInfo` hash +- **Deadline-based expiry**: `validBefore` (typically `now + maxTimeoutSeconds`) prevents stale authorizations -### ERC-3009 Signature +### ERC-3009 signature The client signs an EIP-712 typed data message with primary type `ReceiveWithAuthorization`: ```typescript const authorization = { from: payerAddress, // Who is paying - to: tokenCollectorAddress, // ERC3009PaymentCollector + to: tokenCollectorAddress, // ERC3009PaymentCollector (canonical) value: amount, // Amount in token decimals validAfter: 0, // Earliest valid time (0 = immediately) validBefore: deadline, // Latest valid time - nonce: derivedNonce // Deterministic nonce from payment params -}; + nonce: derivedNonce, // Payer-agnostic PaymentInfo hash +} ``` -The escrow scheme uses `receiveWithAuthorization` (not `transferWithAuthorization`). The token collector is the `to` address, which then routes tokens to the escrow contract. +The authCapture scheme uses `receiveWithAuthorization` (not `transferWithAuthorization`). The token collector is the `to` address, which then routes tokens into the escrow. + +--- + +## Permit2PaymentCollector + +Collects ERC-20 tokens via Uniswap Permit2 `permitTransferFrom`. Used when `assetTransferMethod === "permit2"` in the scheme `extra`. Any ERC-20 the payer has approved Permit2 for becomes spendable through this collector. + +The client signs a Permit2 `PermitTransferFrom`; the merchant address is bound through the deterministic nonce, so no separate witness struct is needed. + +See the [authCapture Scheme Specification](/x402-integration/auth-capture-scheme) for the full Permit2 wire format. diff --git a/contracts/periphery/overview.mdx b/contracts/periphery/overview.mdx index 3974084..9ba8b11 100644 --- a/contracts/periphery/overview.mdx +++ b/contracts/periphery/overview.mdx @@ -19,7 +19,7 @@ Periphery contracts support the [PaymentOperator](/contracts/payment-operator) b ## Contract Addresses -All periphery contracts use **unified CREATE3 addresses**: the same address on every supported chain. +All periphery contracts use **universal CREATE2 addresses**: the same address on every supported chain. | Contract | Address | |----------|---------| diff --git a/contracts/recorders/combinator.mdx b/contracts/recorders/combinator.mdx index 66f177d..0258777 100644 --- a/contracts/recorders/combinator.mdx +++ b/contracts/recorders/combinator.mdx @@ -17,13 +17,13 @@ const comboAddress = await recorderCombinatorFactory.write.deploy([ [escrowPeriodAddress, paymentIndexRecorderAddress] // Records auth time + payment index ]); -config.authorizeRecorder = comboAddress; +config.authorizePostActionHook = comboAddress; ``` ## Behavior - Recorders are called in the order provided -- **If any recorder reverts, all revert** — the entire recording is atomic +- **If any recorder reverts, all revert**: the entire recording is atomic - Each recorder receives the same `paymentInfo`, `amount`, and `caller` parameters ## Limits diff --git a/contracts/recorders/overview.mdx b/contracts/recorders/overview.mdx index 3d641e9..8456209 100644 --- a/contracts/recorders/overview.mdx +++ b/contracts/recorders/overview.mdx @@ -10,9 +10,9 @@ Recorders are pluggable contracts that update state **after** an action successf | Slot | Records After | |------|---------------| -| `AUTHORIZE_RECORDER` | Authorization (e.g., timestamp) | -| `CHARGE_RECORDER` | Charge event | -| `RELEASE_RECORDER` | Release from escrow | +| `AUTHORIZE_POST_ACTION_HOOK` | Authorization (e.g., timestamp) | +| `CHARGE_POST_ACTION_HOOK` | Charge event | +| `CAPTURE_POST_ACTION_HOOK` | Release from escrow | | `VOID_POST_ACTION_HOOK` | Void | | `REFUND_POST_ACTION_HOOK` | Refund (after capture) | @@ -49,7 +49,7 @@ Not every payment needs on-chain recorders. Choose based on your use case: ### Events Only (~0 Extra Gas) -The operator already emits events (`AuthorizationCreated`, `ReleaseExecuted`, etc.) for every action. If you only need payment history for analytics or display, skip recorders entirely and index events off-chain. +The operator already emits events (`AuthorizeExecuted`, `CaptureExecuted`, etc.) for every action. If you only need payment history for analytics or display, skip recorders entirely and index events off-chain. **Best for:** Micropayments, high-volume payments where gas overhead matters, simple UIs. @@ -75,12 +75,12 @@ Use recorders when you need **on-chain reads**: other contracts or conditions th |------|----------|---------------| | Payment history for UI | Events only | `address(0)` | | Rich queries, analytics | Events + Subgraph | `address(0)` | -| Time-locked releases | On-chain | [EscrowPeriod](/contracts/conditions/escrow-period) on `AUTHORIZE_RECORDER` | +| Time-locked releases | On-chain | [EscrowPeriod](/contracts/conditions/escrow-period) on `AUTHORIZE_POST_ACTION_HOOK` | | On-chain payment index | On-chain | [PaymentIndexRecorder](/contracts/recorders/payment-index) | | Multiple data points | On-chain | [RecorderCombinator](/contracts/recorders/combinator) | -For most configurations, you only need a recorder on the `AUTHORIZE_RECORDER` slot (for [EscrowPeriod](/contracts/conditions/escrow-period)). Leave other recorder slots as `address(0)`. +For most configurations, you only need a recorder on the `AUTHORIZE_POST_ACTION_HOOK` slot (for [EscrowPeriod](/contracts/conditions/escrow-period)). Leave other recorder slots as `address(0)`. ## Next Steps diff --git a/index.mdx b/index.mdx index d02c959..33b333a 100644 --- a/index.mdx +++ b/index.mdx @@ -10,9 +10,9 @@ icon: "house" Standard x402 payments are immediate and irreversible. x402r adds: -- **Escrow deposits** — Funds held in smart contracts until conditions are met -- **Refund windows** — Configurable time periods for buyers to request refunds -- **Dispute resolution** — Arbiter system for handling contested transactions +- **Escrow deposits**: Funds held in smart contracts until conditions are met +- **Refund windows**: Configurable time periods for buyers to request refunds +- **Dispute resolution**: Arbiter system for handling contested transactions ## How It Works @@ -81,21 +81,15 @@ x402r consists of these core components: | **EscrowPeriod & Freeze** | Time-based release and freeze policies for buyer protection | | **RefundRequest** | Handles refund request lifecycle and approvals | -All protocol contracts use unified CREATE3 addresses — same address on every supported chain. +All protocol contracts use universal CREATE2 addresses, same address on every supported chain. ## Supported Networks +Today, the supported chains in `@x402r/core` are **Base** and **Base Sepolia**. Additional EVMs will be added as canonical `base/commerce-payments@v1.0.0` coverage extends. + | Network | Chain ID | Status | -|---------|----------|--------| +|---|---|---| | Base | 8453 | Supported | -| Ethereum | 1 | Supported | -| Polygon | 137 | Supported | -| Arbitrum One | 42161 | Supported | -| Optimism | 10 | Supported | -| Celo | 42220 | Supported | -| Avalanche C-Chain | 43114 | Supported | -| Monad | 143 | Supported | -| Linea | 59144 | Supported | | Base Sepolia | 84532 | Testnet | | Ethereum Sepolia | 11155111 | Testnet | diff --git a/sdk/arbiter/subscriptions.mdx b/sdk/arbiter/subscriptions.mdx index ff64876..0a96770 100644 --- a/sdk/arbiter/subscriptions.mdx +++ b/sdk/arbiter/subscriptions.mdx @@ -27,7 +27,7 @@ This is a no-op if `refundRequestAddress` was not provided in the client config. ## watch.onPayment -Watch for payment lifecycle events: `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted`. +Watch for payment lifecycle events: `AuthorizeExecuted`, `ChargeExecuted`, and `CaptureExecuted`. ```typescript const unwatch = arbiter.watch.onPayment((logs) => { @@ -72,7 +72,7 @@ unwatch() | Method | Events Watched | Contract | |--------|---------------|----------| | `watch.onRefundRequest` | All RefundRequest ABI events | RefundRequest | -| `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | +| `watch.onPayment` | `AuthorizeExecuted`, `ChargeExecuted`, `CaptureExecuted` | PaymentOperator | | `watch.onRefundExecuted` | `VoidExecuted`, `RefundExecuted` | PaymentOperator | | `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | diff --git a/sdk/client/payment-queries.mdx b/sdk/client/payment-queries.mdx index f79975d..f44caf3 100644 --- a/sdk/client/payment-queries.mdx +++ b/sdk/client/payment-queries.mdx @@ -72,7 +72,7 @@ Retrieve all slot addresses from the PaymentOperator contract. ```typescript const config = await client.operator.getConfig() -console.log('Release condition:', config.releaseCondition) +console.log('Release condition:', config.capturePreActionCondition) console.log('Fee calculator:', config.feeCalculator) console.log('Fee recipient:', config.feeRecipient) ``` diff --git a/sdk/client/subscriptions.mdx b/sdk/client/subscriptions.mdx index fc00f4c..a92e35e 100644 --- a/sdk/client/subscriptions.mdx +++ b/sdk/client/subscriptions.mdx @@ -10,7 +10,7 @@ The `watch` group is always available on the client (no optional address require ## watch.onPayment -Watch for payment lifecycle events: `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted` on the PaymentOperator contract. +Watch for payment lifecycle events: `AuthorizeExecuted`, `ChargeExecuted`, and `CaptureExecuted` on the PaymentOperator contract. ```typescript const unwatch = client.watch.onPayment((logs) => { @@ -18,13 +18,13 @@ const unwatch = client.watch.onPayment((logs) => { console.log('Payment event:', log.eventName) switch (log.eventName) { - case 'AuthorizationCreated': + case 'AuthorizeExecuted': console.log('New payment authorized') break case 'ChargeExecuted': console.log('Payment charged') break - case 'ReleaseExecuted': + case 'CaptureExecuted': console.log('Funds released to merchant') break } @@ -82,7 +82,7 @@ unwatch() | Method | Events Watched | Contract | |--------|---------------|----------| -| `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | +| `watch.onPayment` | `AuthorizeExecuted`, `ChargeExecuted`, `CaptureExecuted` | PaymentOperator | | `watch.onRefundRequest` | All RefundRequest ABI events | RefundRequest | | `watch.onRefundExecuted` | `VoidExecuted`, `RefundExecuted` | PaymentOperator | | `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | diff --git a/sdk/delivery-arbiter.mdx b/sdk/delivery-arbiter.mdx index 5ffd059..ce788f7 100644 --- a/sdk/delivery-arbiter.mdx +++ b/sdk/delivery-arbiter.mdx @@ -73,7 +73,7 @@ app.post('/verify', async (req, res) => { if (passed) { const amounts = await arbiter.payment.getAmounts(paymentInfo) - await arbiter.payment.release(paymentInfo, amounts.capturableAmount) + await arbiter.payment.capture(paymentInfo, amounts.capturableAmount) res.json({ verdict: 'PASS' }) } else { // Arbiter can refund immediately without waiting for escrow expiry. diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index 64f7af8..3b7136f 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -147,7 +147,7 @@ console.log('Existing (reused):', result.summary.existingCount); ## Deployment result - Redeploying with the same parameters is idempotent (CREATE3). It detects existing contracts and skips them. Check `summary` for what was new vs reused. + Redeploying with the same parameters is idempotent (CREATE2). It detects existing contracts and skips them. Check `summary` for what was new vs reused. ```typescript @@ -170,7 +170,7 @@ interface MarketplaceOperatorDeployment { ``` -Because all contracts use CREATE3, redeploying with the same parameters is idempotent, it will detect existing contracts and skip them. The `summary` tells you what was new vs reused. +Because all contracts use CREATE2, redeploying with the same parameters is idempotent, it will detect existing contracts and skip them. The `summary` tells you what was new vs reused. ## Preview addresses (no deploy) @@ -183,10 +183,10 @@ Because all contracts use CREATE3, redeploying with the same parameters is idemp | Slot | Contract | Purpose | |------|----------|---------| - | `AUTHORIZE_CONDITION` | UsdcTvlLimit | Safety limit on authorization | - | `AUTHORIZE_RECORDER` | EscrowPeriod | Records authorization timestamp | - | `CHARGE_CONDITION` | (none) | No restrictions on charge | - | `RELEASE_CONDITION` | EscrowPeriod | Blocks release during escrow period | + | `AUTHORIZE_PRE_ACTION_CONDITION` | UsdcTvlLimit | Safety limit on authorization | + | `AUTHORIZE_POST_ACTION_HOOK` | EscrowPeriod | Records authorization timestamp | + | `CHARGE_PRE_ACTION_CONDITION` | (none) | No restrictions on charge | + | `CAPTURE_PRE_ACTION_CONDITION` | EscrowPeriod | Blocks release during escrow period | | `VOID_PRE_ACTION_CONDITION` | OR(Receiver, Arbiter) | Receiver or arbiter can approve | | `REFUND_PRE_ACTION_CONDITION` | Receiver | Only receiver after escrow | | `FEE_CALCULATOR` | StaticFeeCalculator | Fixed percentage fee | @@ -214,10 +214,10 @@ The deployed marketplace operator has the following slot configuration: | Slot | Contract | Purpose | |------|----------|---------| -| `AUTHORIZE_CONDITION` | UsdcTvlLimit | Safety limit on authorization | -| `AUTHORIZE_RECORDER` | EscrowPeriod | Records authorization timestamp | -| `CHARGE_CONDITION` | (none) | No restrictions on charge | -| `RELEASE_CONDITION` | EscrowPeriod (or AND(EscrowPeriod, Freeze) if freeze enabled) | Blocks release during escrow period | +| `AUTHORIZE_PRE_ACTION_CONDITION` | UsdcTvlLimit | Safety limit on authorization | +| `AUTHORIZE_POST_ACTION_HOOK` | EscrowPeriod | Records authorization timestamp | +| `CHARGE_PRE_ACTION_CONDITION` | (none) | No restrictions on charge | +| `CAPTURE_PRE_ACTION_CONDITION` | EscrowPeriod (or AND(EscrowPeriod, Freeze) if freeze enabled) | Blocks release during escrow period | | `VOID_PRE_ACTION_CONDITION` | OR(Receiver, Arbiter) | Receiver or arbiter can approve | | `VOID_POST_ACTION_HOOK` | RefundRequest | Tracks refund request state | | `REFUND_PRE_ACTION_CONDITION` | Receiver | Only receiver after escrow | @@ -236,8 +236,8 @@ This preset has **no freeze, no fees, and no RefundRequest** contracts, making i | Slot | Contract | Purpose | |------|----------|---------| -| `RELEASE_CONDITION` | StaticAddressCondition(arbiter) | Only the arbiter can release | -| `AUTHORIZE_RECORDER` | EscrowPeriod | Records authorization timestamp | +| `CAPTURE_PRE_ACTION_CONDITION` | StaticAddressCondition(arbiter) | Only the arbiter can release | +| `AUTHORIZE_POST_ACTION_HOOK` | EscrowPeriod | Records authorization timestamp | | `VOID_PRE_ACTION_CONDITION` | EscrowPeriod | Anyone can refund after escrow window expires | | `REFUND_PRE_ACTION_CONDITION` | Receiver | Receiver can refund | @@ -329,23 +329,14 @@ console.log('Arbiter condition:', preview.arbiterConditionAddress); ## Network support -Deployment is supported on all configured networks: +Today, deployment is supported on the chains in `@x402r/core`'s `x402rChains`: | Network | Chain ID | EIP-155 ID | -|---------|----------|------------| -| Base Sepolia | 84532 | `eip155:84532` | +|---|---|---| | Base | 8453 | `eip155:8453` | -| Ethereum | 1 | `eip155:1` | -| Ethereum Sepolia | 11155111 | `eip155:11155111` | -| Arbitrum | 42161 | `eip155:42161` | -| Arbitrum Sepolia | 421614 | `eip155:421614` | -| Optimism | 10 | `eip155:10` | -| Polygon | 137 | `eip155:137` | -| Celo | 42220 | `eip155:42220` | -| Avalanche | 43114 | `eip155:43114` | -| Linea | 59144 | `eip155:59144` | -| Monad | 143 | `eip155:143` | -| SKALE Base | 1187947933 | `eip155:1187947933` | +| Base Sepolia | 84532 | `eip155:84532` | + +Additional EVMs will be added as canonical `base/commerce-payments@v1.0.0` coverage extends. Deployment requires gas fees. Ensure your wallet has ETH on the target network. On Base Sepolia, you can get testnet ETH from [Base network faucets](https://docs.base.org/base-chain/tools/network-faucets). @@ -387,8 +378,8 @@ const deployment = await deployDeliveryProtectionOperator( console.log('Operator:', deployment.operatorAddress) console.log('EscrowPeriod:', deployment.escrowPeriodAddress) console.log('ArbiterCondition:', deployment.arbiterConditionAddress) -console.log('ReleaseCondition:', deployment.releaseConditionAddress) -console.log('AuthorizeRecorder:', deployment.authorizeRecorderAddress) +console.log('ReleaseCondition:', deployment.capturePreActionConditionAddress) +console.log('AuthorizeRecorder:', deployment.authorizePostActionHookAddress) ``` | Option | Type | Description | @@ -408,9 +399,9 @@ console.log('AuthorizeRecorder:', deployment.authorizeRecorderAddress) operatorAddress: Address escrowPeriodAddress: Address arbiterConditionAddress: Address - releaseConditionAddress: Address // OrCondition([arbiter, payer]) + capturePreActionConditionAddress: Address // OrCondition([arbiter, payer]) voidPreActionConditionAddress: Address // OrCondition([escrowPeriod, receiver, arbiter]) - authorizeRecorderAddress: Address // RecorderCombinator([escrowPeriod, paymentIndexRecorder]) + authorizePostActionHookAddress: Address // RecorderCombinator([escrowPeriod, paymentIndexRecorder]) paymentIndexRecorderAddress: Address operatorConfig: OperatorConfig deployments: DeployResult[] @@ -424,7 +415,7 @@ console.log('AuthorizeRecorder:', deployment.authorizeRecorderAddress) Deploys 6 contracts by default: EscrowPeriod, StaticAddressCondition(arbiter), OrCondition(release), OrCondition(refund), RecorderCombinator, and the Operator. If you pass `paymentIndexRecorderAddress: zeroAddress`, the RecorderCombinator is skipped (5 contracts). - Redeploying with the same parameters is idempotent (CREATE3). It detects existing contracts and skips them. + Redeploying with the same parameters is idempotent (CREATE2). It detects existing contracts and skips them. @@ -442,15 +433,15 @@ console.log('AuthorizeRecorder:', deployment.authorizeRecorderAddress) console.log('Operator will be at:', preview.operatorAddress) console.log('EscrowPeriod will be at:', preview.escrowPeriodAddress) - console.log('AuthorizeRecorder will be at:', preview.authorizeRecorderAddress) + console.log('AuthorizeRecorder will be at:', preview.authorizePostActionHookAddress) ``` | Slot | Contract | Purpose | |------|----------|---------| - | `RELEASE_CONDITION` | OrCondition([SAC(arbiter), PayerCondition]) | Arbiter or satisfied payer can release | - | `AUTHORIZE_RECORDER` | RecorderCombinator([EscrowPeriod, PaymentIndexRecorder]) | Records authorization time and indexes payments on-chain | + | `CAPTURE_PRE_ACTION_CONDITION` | OrCondition([SAC(arbiter), PayerCondition]) | Arbiter or satisfied payer can release | + | `AUTHORIZE_POST_ACTION_HOOK` | RecorderCombinator([EscrowPeriod, PaymentIndexRecorder]) | Records authorization time and indexes payments on-chain | | `VOID_PRE_ACTION_CONDITION` | OrCondition([EscrowPeriod, ReceiverCondition, SAC(arbiter)]) | Escrow expiry, receiver voluntary refund, or arbiter immediate refund | | `REFUND_PRE_ACTION_CONDITION` | ReceiverCondition | Only receiver after escrow | diff --git a/sdk/examples.mdx b/sdk/examples.mdx index 7efad9d..2ceb64b 100644 --- a/sdk/examples.mdx +++ b/sdk/examples.mdx @@ -16,10 +16,10 @@ The [x402r-sdk repo](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples Deploy a complete marketplace operator with escrow, freeze, and arbiter support. - Express merchant server using `EscrowServerScheme` and `HTTPFacilitatorClient` to accept escrow payments via x402 middleware. + Express merchant server using `AuthCaptureServerScheme` and `HTTPFacilitatorClient` to accept escrow payments via x402 middleware. - Hono merchant server using `EscrowServerScheme` and `HTTPFacilitatorClient` to accept escrow payments via x402 middleware. + Hono merchant server using `AuthCaptureServerScheme` and `HTTPFacilitatorClient` to accept escrow payments via x402 middleware. CLI tool for merchants to release payments, approve/deny refunds, and query escrow state. @@ -100,7 +100,7 @@ See [Deploy an operator](/sdk/deploy-operator) for the full guide. | [`payer/submit-evidence.ts`](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/payer/submit-evidence.ts) | Submit an IPFS evidence CID for a dispute | | [`payer/freeze-payment.ts`](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/payer/freeze-payment.ts) | Freeze a payment to block release during investigation | -Demonstrates minimal merchant servers (Express and Hono variants) that use `EscrowServerScheme` and `HTTPFacilitatorClient` via x402's standard middleware: +Demonstrates minimal merchant servers (Express and Hono variants) that use `AuthCaptureServerScheme` and `HTTPFacilitatorClient` via x402's standard middleware: 1. Returns 402 with inline escrow payment options 2. Delegates payment verification to the facilitator via `HTTPFacilitatorClient` diff --git a/sdk/facilitator/getting-started.mdx b/sdk/facilitator/getting-started.mdx index 9063130..26f0f08 100644 --- a/sdk/facilitator/getting-started.mdx +++ b/sdk/facilitator/getting-started.mdx @@ -48,7 +48,7 @@ Never commit private keys to source control. Use environment variables or a secr import { x402Facilitator } from "@x402/core/facilitator"; import { PaymentPayload, PaymentRequirements } from "@x402/core/types"; import { toFacilitatorEvmSigner } from "@x402/evm"; - import { registerEscrowScheme } from "@x402r/evm/escrow/facilitator"; + import { registerEscrowScheme } from "@x402r/evm/authCapture/facilitator"; import { createWalletClient, http, publicActions } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { baseSepolia } from "viem/chains"; diff --git a/sdk/helpers/erc8004.mdx b/sdk/helpers/erc8004.mdx index 59be3b6..2372e2c 100644 --- a/sdk/helpers/erc8004.mdx +++ b/sdk/helpers/erc8004.mdx @@ -50,7 +50,7 @@ Parses agent registrations from the upstream x402 reputation extension (`extensi Returns an empty array if the reputation extension is absent or malformed. -The upstream x402 reputation extension is not yet merged. The shape of `extensions["reputation"]` may change. See [x402-foundation/x402#931](https://github.com/x402-foundation/x402/issues/931). +The upstream x402 reputation extension is not yet merged. The shape of `extensions["reputation"]` may change. ```typescript @@ -150,9 +150,6 @@ function handlePayment(paymentRequired: { extensions?: Record } ## Next steps - - Add escrow config to x402 payment options. - Client factory with identity.check() for combined verification. diff --git a/sdk/helpers/forward-to-arbiter.mdx b/sdk/helpers/forward-to-arbiter.mdx index deb2dcc..02cb6d9 100644 --- a/sdk/helpers/forward-to-arbiter.mdx +++ b/sdk/helpers/forward-to-arbiter.mdx @@ -16,7 +16,7 @@ The `forwardToArbiter()` function creates an `onAfterSettle` hook that forwards import { forwardToArbiter } from '@x402r/helpers'; const resourceServer = new x402ResourceServer(facilitatorClient) - .register(networkId, new EscrowServerScheme()) + .register(networkId, new AuthCaptureServerScheme()) .onAfterSettle( forwardToArbiter('http://arbiter:3001') ); @@ -75,7 +75,7 @@ When a commerce settlement succeeds, the hook POSTs the following JSON to `{arbi ``` -Arbiters that need `paymentInfo` for `release()` can read it directly from `paymentPayload.payload.paymentInfo` — no extra RPC call needed. +Arbiters that need `paymentInfo` for `capture()` can read it directly from `paymentPayload.payload.paymentInfo`, no extra RPC call needed. ## Error handling @@ -86,7 +86,7 @@ By default, fetch errors are logged with `console.warn`. You can override this w import { forwardToArbiter } from '@x402r/helpers'; const resourceServer = new x402ResourceServer(facilitatorClient) - .register(networkId, new EscrowServerScheme()) + .register(networkId, new AuthCaptureServerScheme()) .onAfterSettle( forwardToArbiter('http://arbiter:3001', { onError: (err) => sentry.captureException(err), @@ -123,14 +123,11 @@ import { } from '@x402r/helpers'; ``` -These are the same CREATE3 addresses available from `@x402r/core`. You can import from either package depending on which you already have installed. +These are the same CREATE2 addresses available from `@x402r/core`. You can import from either package depending on which you already have installed. ## Next steps - - Configure escrow options and fee bounds. - Build an arbiter that processes forwarded settlements. diff --git a/sdk/limitations.mdx b/sdk/limitations.mdx index 55f3197..937ee46 100644 --- a/sdk/limitations.mdx +++ b/sdk/limitations.mdx @@ -38,7 +38,7 @@ const request = await client.refund?.getByKey(paymentInfoHash) ### Event Log Scanning Limits -The event-based query provider scans `AuthorizationCreated` and `ChargeExecuted` events using `eth_getLogs`. Base Sepolia RPCs typically limit responses to 10,000 blocks. Configure `eventFromBlock` in your client config to set the scan start: +The event-based query provider scans `AuthorizeExecuted` and `ChargeExecuted` events using `eth_getLogs`. Base Sepolia RPCs typically limit responses to 10,000 blocks. Configure `eventFromBlock` in your client config to set the scan start: ```typescript // Pass eventFromBlock when creating the client to limit scan range @@ -55,7 +55,7 @@ const payments = await client.query?.getPayerPayments(payerAddress) ### No Express/Hono Middleware -The `forwardToArbiter()` hook in `@x402r/helpers` is framework-agnostic. There is no dedicated Express or Hono middleware — configure escrow payment options inline and use `forwardToArbiter()` as an `onAfterSettle` hook. +The `forwardToArbiter()` hook in `@x402r/helpers` is framework-agnostic. There is no dedicated Express or Hono middleware, configure escrow payment options inline and use `forwardToArbiter()` as an `onAfterSettle` hook. ## Getting Updates diff --git a/sdk/merchant.mdx b/sdk/merchant.mdx index 4787aac..ee89724 100644 --- a/sdk/merchant.mdx +++ b/sdk/merchant.mdx @@ -77,7 +77,7 @@ console.log('In escrow:', inEscrow) // true ```typescript -const releaseTx = await merchant.payment.release(paymentInfo, 1_000_000n) +const releaseTx = await merchant.payment.capture(paymentInfo, 1_000_000n) console.log('Released:', releaseTx) // Verify diff --git a/sdk/merchant/getting-started.mdx b/sdk/merchant/getting-started.mdx index 67b7676..cb0f57f 100644 --- a/sdk/merchant/getting-started.mdx +++ b/sdk/merchant/getting-started.mdx @@ -46,7 +46,7 @@ The full source code for this example is available on [GitHub](https://github.co import "dotenv/config"; import express from "express"; import { paymentMiddleware, x402ResourceServer } from "@x402/express"; - import { EscrowServerScheme } from "@x402r/evm/escrow/server"; + import { AuthCaptureServerScheme } from "@x402r/evm/authCapture/server"; import { getChainConfig } from "@x402r/core"; import { HTTPFacilitatorClient } from "@x402/core/server"; @@ -65,25 +65,33 @@ The full source code for this example is available on [GitHub](https://github.co const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); const networkId = "eip155:84532"; - const { authCaptureEscrow } = getChainConfig(networkId); const app = express(); + const now = Math.floor(Date.now() / 1000); + app.use( paymentMiddleware( { "GET /weather": { accepts: [ { - scheme: "escrow", + scheme: "authCapture", price: "$0.01", network: networkId, payTo: address, + maxTimeoutSeconds: 60, extra: { - escrowAddress: authCaptureEscrow, - operatorAddress, - feeReceiver: operatorAddress, + name: "USDC", + version: "2", + captureAuthorizer: operatorAddress, + captureDeadline: now + 60 * 60, // capture within 1 hour + refundDeadline: now + 24 * 60 * 60, // refund window 24 hours + feeRecipient: operatorAddress, + minFeeBps: 0, maxFeeBps: 500, + // assetTransferMethod defaults to "eip3009" + // autoCapture defaults to false (two-phase) }, }, ], @@ -93,7 +101,7 @@ The full source code for this example is available on [GitHub](https://github.co }, new x402ResourceServer(facilitatorClient).register( networkId, - new EscrowServerScheme() as never, + new AuthCaptureServerScheme() as never, ), ), ); @@ -126,20 +134,25 @@ The full source code for this example is available on [GitHub](https://github.co curl http://localhost:4021/weather ``` - Without a valid payment header, the server responds with HTTP 402 and the escrow payment requirements: + Without a valid payment header, the server responds with HTTP 402 and the authCapture payment requirements: ```json { "x402Version": 2, "accepts": [{ - "scheme": "escrow", + "scheme": "authCapture", "price": "$0.01", "network": "eip155:84532", "payTo": "0x...", + "maxTimeoutSeconds": 60, "extra": { - "escrowAddress": "0x...", - "operatorAddress": "0x...", - "feeReceiver": "0x...", + "name": "USDC", + "version": "2", + "captureAuthorizer": "0x...", + "captureDeadline": 1740758554, + "refundDeadline": 1741276954, + "feeRecipient": "0x...", + "minFeeBps": 0, "maxFeeBps": 500 } }] @@ -150,8 +163,8 @@ The full source code for this example is available on [GitHub](https://github.co ## How it works -- **Escrow `extra` config** specifies the escrow contract address, operator address, fee receiver, and maximum fee bounds directly on the payment option. -- **`EscrowServerScheme`** registers the escrow payment scheme with the x402 resource server so it can validate escrow-backed payments. +- **`extra` config** declares the captureAuthorizer, capture/refund deadlines, fee recipient, and fee bounds. The canonical `AuthCaptureEscrow` and token collector addresses are universal CREATE2 deploys, so they do not need to be repeated per-route. +- **`AuthCaptureServerScheme`** registers the authCapture payment scheme with the x402 resource server so it can validate authCapture-backed payments. - **`paymentMiddleware`** intercepts requests, checks for a valid payment header, and returns 402 if no payment is provided. - **`HTTPFacilitatorClient`** connects to the facilitator service that verifies and settles payments on-chain. diff --git a/sdk/merchant/payment-operations.mdx b/sdk/merchant/payment-operations.mdx index ade1088..ea50ee6 100644 --- a/sdk/merchant/payment-operations.mdx +++ b/sdk/merchant/payment-operations.mdx @@ -8,13 +8,13 @@ The merchant client provides methods for managing the full payment lifecycle thr ## Payment operations -### payment.release +### payment.capture Transfer escrowed funds to the receiver (merchant). The `amount` parameter is required. ```typescript // Release 10 USDC (6 decimals) from escrow -const tx = await merchant.payment.release(paymentInfo, 10_000_000n) +const tx = await merchant.payment.capture(paymentInfo, 10_000_000n) console.log('Released:', tx) ``` @@ -22,7 +22,7 @@ For partial releases, specify a smaller amount. The remaining funds stay in escr ```typescript // Release 3 USDC of a 10 USDC escrow -const tx = await merchant.payment.release(paymentInfo, 3_000_000n) +const tx = await merchant.payment.capture(paymentInfo, 3_000_000n) // Check what remains const amounts = await merchant.payment.getAmounts(paymentInfo) @@ -134,7 +134,7 @@ const config = await merchant.operator.getConfig() console.log('Fee recipient:', config.feeRecipient) console.log('Fee calculator:', config.feeCalculator) -console.log('Release condition:', config.releaseCondition) +console.log('Release condition:', config.capturePreActionCondition) ``` ### operator.getFeeAddresses @@ -172,7 +172,7 @@ flowchart TD C -->|No| E[Nothing to release] D -->|No| F[Safe to release] D -->|Yes| G{Approve refund?} - F --> H["payment.release(paymentInfo, amount)"] + F --> H["payment.capture(paymentInfo, amount)"] G -->|Yes| I["payment.voidPayment(paymentInfo)"] G -->|No| J[Deny request, then release] J --> H diff --git a/sdk/merchant/quickstart.mdx b/sdk/merchant/quickstart.mdx index 30f9425..f1b0692 100644 --- a/sdk/merchant/quickstart.mdx +++ b/sdk/merchant/quickstart.mdx @@ -51,11 +51,11 @@ const merchant = createMerchantClient({ ## Release funds from escrow -Use `payment.release()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is required. +Use `payment.capture()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is required. ```typescript // Release 10 USDC (6 decimals) from escrow -const tx = await merchant.payment.release(paymentInfo, 10_000_000n) +const tx = await merchant.payment.capture(paymentInfo, 10_000_000n) console.log('Released:', tx) ``` @@ -63,7 +63,7 @@ For partial releases, specify a smaller amount. The remaining funds stay in escr ```typescript // Release 3 USDC of a 10 USDC escrow -const tx = await merchant.payment.release(paymentInfo, 3_000_000n) +const tx = await merchant.payment.capture(paymentInfo, 3_000_000n) console.log('Partial release:', tx) // Check what remains @@ -137,7 +137,7 @@ console.log('Capturable:', amounts.capturableAmount) console.log('Refundable:', amounts.refundableAmount) if (amounts.capturableAmount > 0n) { - await merchant.payment.release(paymentInfo, amounts.capturableAmount) + await merchant.payment.capture(paymentInfo, amounts.capturableAmount) } ``` @@ -159,7 +159,7 @@ const config = await merchant.operator.getConfig() console.log('Fee recipient:', config.feeRecipient) console.log('Fee calculator:', config.feeCalculator) -console.log('Release condition:', config.releaseCondition) +console.log('Release condition:', config.capturePreActionCondition) ``` ### operator.getFeeAddresses @@ -197,7 +197,7 @@ flowchart TD C -->|No| E[Nothing to release] D -->|No| F[Safe to release] D -->|Yes| G{Approve refund?} - F --> H["payment.release(paymentInfo, amount)"] + F --> H["payment.capture(paymentInfo, amount)"] G -->|Yes| I["payment.voidPayment(paymentInfo)"] G -->|No| J[Deny request, then release] J --> H diff --git a/sdk/merchant/subscriptions.mdx b/sdk/merchant/subscriptions.mdx index 38fbbce..81b8692 100644 --- a/sdk/merchant/subscriptions.mdx +++ b/sdk/merchant/subscriptions.mdx @@ -8,7 +8,7 @@ The merchant client provides real-time event subscriptions through the `watch` a ## watch.onPayment -Watch for payment lifecycle events: `AuthorizationCreated`, `ChargeExecuted`, and `ReleaseExecuted` on the PaymentOperator contract. +Watch for payment lifecycle events: `AuthorizeExecuted`, `ChargeExecuted`, and `CaptureExecuted` on the PaymentOperator contract. ```typescript const unwatch = merchant.watch.onPayment((logs) => { @@ -28,7 +28,7 @@ let totalReleased = 0n const unwatch = merchant.watch.onPayment((logs) => { for (const log of logs) { - if (log.eventName === 'ReleaseExecuted') { + if (log.eventName === 'CaptureExecuted') { const amount = log.args?.amount ?? 0n totalReleased += amount console.log('Release: +', amount, 'Total:', totalReleased) @@ -106,7 +106,7 @@ unwatch() | Method | Events Watched | Contract | |--------|---------------|----------| -| `watch.onPayment` | `AuthorizationCreated`, `ChargeExecuted`, `ReleaseExecuted` | PaymentOperator | +| `watch.onPayment` | `AuthorizeExecuted`, `ChargeExecuted`, `CaptureExecuted` | PaymentOperator | | `watch.onRefundRequest` | All RefundRequest ABI events | RefundRequest | | `watch.onRefundExecuted` | `VoidExecuted`, `RefundExecuted` | PaymentOperator | | `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | diff --git a/sdk/overview.mdx b/sdk/overview.mdx index 21b58de..28b2765 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -85,44 +85,29 @@ bunx @x402r/cli pay [options] Deploy an operator, accept a payment, release funds from escrow. - High-level SDK with role-scoped clients (`createPayerClient`, `createMerchantClient`, `createArbiterClient`) and the generic `createX402r` factory. + High-level client with role-scoped factories (`createPayerClient`, `createMerchantClient`, `createArbiterClient`) and the generic `createX402r`. - - SDK for merchants to release payments, charge, and handle refunds. + + Capture funds from escrow, charge directly, void, and process refunds using `createMerchantClient`. - - SDK for arbiters to resolve disputes and manage refund decisions. + + Resolve disputes and act as a captureAuthorizer using `createArbiterClient`. - - Framework-agnostic helpers: `refundable()` for escrow configuration, `forwardToArbiter()` for arbiter integration, and re-exported address constants. - - - One-shot payments from the command line or scripts. + + Wallet-agnostic one-shot payments from the command line or scripts. ## Network support -All v3 contracts are deployed to the same address on every chain via CREATE3. +All x402r-authored contracts use universal CREATE2 addresses: when a chain is supported, the address is the same as on every other supported chain. + +Today, the supported chains in `@x402r/core` are **Base** and **Base Sepolia**. Additional EVMs will be added as canonical `base/commerce-payments@v1.0.0` coverage extends. | Chain | Chain ID | USDC | -|-------|----------|------| -| Base Sepolia | `84532` | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` | -| Ethereum Sepolia | `11155111` | `0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238` | -| Arbitrum Sepolia | `421614` | `0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d` | -| Ethereum | `1` | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | +|---|---|---| | Base | `8453` | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | -| Polygon | `137` | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` | -| Arbitrum One | `42161` | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` | -| Optimism | `10` | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` | -| Celo | `42220` | `0xcebA9300f2b948710d2653dD7B07f33A8B32118C` | -| Avalanche C-Chain | `43114` | `0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E` | -| Monad | `143` | `0x754704Bc059F8C67012fEd69BC8A327a5aafb603` | -| Linea | `59144` | `0x176211869cA2b568f2A7D4EE941E073a821EE1ff` | - - -SKALE Base (`1187947933`) was removed from the registry. Its Shanghai EVM is incompatible with the Cancun-locked canonical bytecode used by the commerce-payments v1 primitives. - +| Base Sepolia | `84532` | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` | ### commerce-payments v1 primitives @@ -152,8 +137,4 @@ commercePaymentsAddresses.erc3009PaymentCollector; commercePaymentsAddresses.permit2PaymentCollector; ``` -The primitives are live on Base, Optimism, Arbitrum One, Polygon, Celo, Avalanche C-Chain, Linea, Base Sepolia, Ethereum Sepolia, and Arbitrum Sepolia. Ethereum mainnet and Monad are listed in the registry pending an imminent deploy. - - -The legacy CREATE3 exports (`authCaptureEscrow`, `tokenCollector`, `factories`, `conditions`, `recorders`) remain unchanged. They will be retired in the SDK v2 migration that ships the x402r-authored contracts at CREATE2 addresses. - +The primitives are exposed to SDK consumers on the chains listed in `@x402r/core`'s `x402rChains` (Base + Base Sepolia today). The CREATE2 addresses themselves have been prepositioned via CreateX on additional chains and will be enabled in the registry as canonical `base/commerce-payments@v1.0.0` coverage extends. diff --git a/x402-integration/comparison.mdx b/x402-integration/comparison.mdx index cd356b8..0a4f808 100644 --- a/x402-integration/comparison.mdx +++ b/x402-integration/comparison.mdx @@ -276,18 +276,22 @@ You can support **both schemes** and let clients choose: "payTo": "0xReceiver..." }, { - "scheme": "escrow", + "scheme": "authCapture", "network": "eip155:8453", "amount": "10000000", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "payTo": "0xReceiver...", + "maxTimeoutSeconds": 60, "extra": { "name": "USDC", "version": "2", - "escrowAddress": "0xe050bB89eD43BB02d71343063824614A7fb80B77", - "operatorAddress": "0xOperator...", - "tokenCollector": "0xcE66Ab399EDA513BD12760b6427C87D6602344a7", - "settlementMethod": "authorize" + "captureAuthorizer": "0xCaptureAuthorizer...", + "captureDeadline": 1740758554, + "refundDeadline": 1741276954, + "feeRecipient": "0xFeeRecipient...", + "minFeeBps": 0, + "maxFeeBps": 500, + "autoCapture": false } } ] @@ -302,16 +306,16 @@ You can support **both schemes** and let clients choose: ## Migration Strategy -### From exact to escrow +### From exact to authCapture -Existing `exact` integrations can add `escrow` support: +Existing `exact` integrations can add `authCapture` support: -1. Deploy operator contract -2. Add `escrow` to `accepts` array +1. Pick a captureAuthorizer (facilitator EOA or arbiter contract) +2. Add an `authCapture` entry to `accepts` array 3. Keep `exact` as fallback 4. Clients upgrade when ready -### From escrow to exact +### From authCapture to exact If escrow proves unnecessary: From b8d298552c47ee57d3e5120ea0d40b8dd157e18c Mon Sep 17 00:00:00 2001 From: A1igator Date: Mon, 11 May 2026 21:40:21 -0700 Subject: [PATCH 23/37] docs(deploy-operator): fix mangled MDX structure breaking CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Mintlify consolidation merge left this file with three structural breaks that blocked `mintlify broken-links` CI: 1. Lines 148-209 (former "## Deployment result" section): had stray and closing tags with no matching openers, an unterminated code block, and TypeScript orphaned outside any fence. Replaced with a clean "Deployment result" + "Preview addresses" pair. 2. Lines 211-227 ("## Marketplace operator slot configuration"): duplicated content that was supposed to be inside an accordion. Kept as a standalone table (no surrounding accordion needed). 3. Lines 229-328: duplicate "## Delivery protection operator" section that re-covered the same material as the well-formed "## Delivery Protection Operator" section below it. Removed the broken first copy; kept the cleaner version. Verified locally with `npx mintlify@latest broken-links` — passes. Co-Authored-By: Claude Opus 4.7 (1M context) --- sdk/deploy-operator.mdx | 173 ++++++---------------------------------- 1 file changed, 25 insertions(+), 148 deletions(-) diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index 3b7136f..d7f48bb 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -147,65 +147,43 @@ console.log('Existing (reused):', result.summary.existingCount); ## Deployment result - Redeploying with the same parameters is idempotent (CREATE2). It detects existing contracts and skips them. Check `summary` for what was new vs reused. - - ```typescript interface MarketplaceOperatorDeployment { - operatorAddress: Address; // The PaymentOperator - escrowPeriodAddress: Address; // EscrowPeriod recorder/condition - freezeAddress: Address | null; // Freeze condition (null if disabled) - refundRequestAddress: Address; // RefundRequest contract - refundRequestEvidenceAddress: Address; // RefundRequestEvidence contract - voidPreActionConditionAddress: Address; // OR(Receiver, Arbiter) - feeCalculatorAddress: Address | null; // null if no fee - operatorConfig: OperatorConfig; // Full operator slot configuration - deployments: DeployResult[]; // Per-contract deploy details + operatorAddress: Address // The PaymentOperator + escrowPeriodAddress: Address // EscrowPeriod recorder/condition + freezeAddress: Address | null // Freeze condition (null if disabled) + refundRequestAddress: Address // RefundRequest contract + refundRequestEvidenceAddress: Address // RefundRequestEvidence contract + voidPreActionConditionAddress: Address // OR(Receiver, Arbiter) + feeCalculatorAddress: Address | null // null if no fee + operatorConfig: OperatorConfig // Full operator slot configuration + deployments: DeployResult[] // Per-contract deploy details summary: { - newCount: number; // Newly deployed contracts - existingCount: number; // Reused existing contracts - txHashes: `0x${string}`[]; // All deployment tx hashes - }; + newCount: number // Newly deployed contracts + existingCount: number // Reused existing contracts + txHashes: `0x${string}`[] // All deployment tx hashes + } } ``` -Because all contracts use CREATE2, redeploying with the same parameters is idempotent, it will detect existing contracts and skip them. The `summary` tells you what was new vs reused. +Because all contracts use CREATE2, redeploying with the same parameters is idempotent: existing contracts are detected and skipped. The `summary` tells you what was new vs reused. ## Preview addresses (no deploy) - console.log('Operator will be at:', preview.operatorAddress) - console.log('EscrowPeriod will be at:', preview.escrowPeriodAddress) - ``` - - - - | Slot | Contract | Purpose | - |------|----------|---------| - | `AUTHORIZE_PRE_ACTION_CONDITION` | UsdcTvlLimit | Safety limit on authorization | - | `AUTHORIZE_POST_ACTION_HOOK` | EscrowPeriod | Records authorization timestamp | - | `CHARGE_PRE_ACTION_CONDITION` | (none) | No restrictions on charge | - | `CAPTURE_PRE_ACTION_CONDITION` | EscrowPeriod | Blocks release during escrow period | - | `VOID_PRE_ACTION_CONDITION` | OR(Receiver, Arbiter) | Receiver or arbiter can approve | - | `REFUND_PRE_ACTION_CONDITION` | Receiver | Only receiver after escrow | - | `FEE_CALCULATOR` | StaticFeeCalculator | Fixed percentage fee | - | `FEE_RECIPIENT` | Your address | Receives fees | - - +```typescript +import { previewMarketplaceOperator } from '@x402r/core' -const preview = await previewMarketplaceOperator( - publicClient, - { - chainId: 84532, - feeRecipient: '0xYourAddress...', - arbiter: '0xArbiterAddress...', - escrowPeriodSeconds: 604800n, - } -); +const preview = await previewMarketplaceOperator(publicClient, { + chainId: 84532, + feeRecipient: '0xYourAddress...', + arbiter: '0xArbiterAddress...', + escrowPeriodSeconds: 604800n, +}) -console.log('Operator will be at:', preview.operatorAddress); -console.log('EscrowPeriod will be at:', preview.escrowPeriodAddress); +console.log('Operator will be at:', preview.operatorAddress) +console.log('EscrowPeriod will be at:', preview.escrowPeriodAddress) ``` ## Marketplace operator slot configuration @@ -213,7 +191,7 @@ console.log('EscrowPeriod will be at:', preview.escrowPeriodAddress); The deployed marketplace operator has the following slot configuration: | Slot | Contract | Purpose | -|------|----------|---------| +|---|---|---| | `AUTHORIZE_PRE_ACTION_CONDITION` | UsdcTvlLimit | Safety limit on authorization | | `AUTHORIZE_POST_ACTION_HOOK` | EscrowPeriod | Records authorization timestamp | | `CHARGE_PRE_ACTION_CONDITION` | (none) | No restrictions on charge | @@ -226,107 +204,6 @@ The deployed marketplace operator has the following slot configuration: --- -## Delivery protection operator - -The delivery protection preset is a simpler operator designed for garbage detection and content verification use cases. An arbiter verifies that content was delivered correctly and releases funds. If the arbiter does nothing, the escrow window expires and anyone can trigger a refund. - -This preset has **no freeze, no fees, and no RefundRequest** contracts, making it cheaper to deploy. - -### Condition layout - -| Slot | Contract | Purpose | -|------|----------|---------| -| `CAPTURE_PRE_ACTION_CONDITION` | StaticAddressCondition(arbiter) | Only the arbiter can release | -| `AUTHORIZE_POST_ACTION_HOOK` | EscrowPeriod | Records authorization timestamp | -| `VOID_PRE_ACTION_CONDITION` | EscrowPeriod | Anyone can refund after escrow window expires | -| `REFUND_PRE_ACTION_CONDITION` | Receiver | Receiver can refund | - -### Deploy a delivery protection operator - -```typescript -import { createPublicClient, createWalletClient, http } from 'viem'; -import { baseSepolia } from 'viem/chains'; -import { privateKeyToAccount } from 'viem/accounts'; -import { deployDeliveryProtectionOperator } from '@x402r/core/deploy'; - -const publicClient = createPublicClient({ - chain: baseSepolia, - transport: http(), -}); - -const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); -const walletClient = createWalletClient({ - account, - chain: baseSepolia, - transport: http(), -}); - -const result = await deployDeliveryProtectionOperator( - walletClient, - publicClient, - { - chainId: 84532, // Base Sepolia - arbiter: '0xArbiterAddress...', // Delivery verifier - feeRecipient: account.address, // Required (non-zero), future-proofs for fees - escrowPeriodSeconds: 604800n, // 7 days - } -); - -console.log('Operator:', result.operatorAddress); -console.log('EscrowPeriod:', result.escrowPeriodAddress); -console.log('Arbiter condition:', result.arbiterConditionAddress); -``` - - -The `feeRecipient` is required by the factory even though no fee calculator is set. This future-proofs the operator so you can add a fee calculator later without redeploying. - - -### Configuration options - -| Option | Type | Description | -|--------|------|-------------| -| `chainId` | `number` | Target chain ID (e.g., `84532` for Base Sepolia) | -| `arbiter` | `Address` | Arbiter address that verifies delivery | -| `feeRecipient` | `Address` | Fee recipient (required, non-zero) | -| `escrowPeriodSeconds` | `bigint` | Escrow waiting period (e.g., `604800n` for 7 days) | -| `authorizedCodehash` | `Hex` | Optional codehash restriction. Default: `bytes32(0)` (no restriction) | - -### Deployment result - -```typescript -interface DeliveryProtectionOperatorDeployment { - operatorAddress: Address; // The PaymentOperator - escrowPeriodAddress: Address; // EscrowPeriod recorder/condition - arbiterConditionAddress: Address; // StaticAddressCondition for arbiter - operatorConfig: OperatorConfig; // Full operator slot configuration - deployments: DeployResult[]; // Per-contract deploy details - summary: { - newCount: number; // Newly deployed contracts - existingCount: number; // Reused existing contracts - txHashes: `0x${string}`[]; // All deployment tx hashes - }; -} -``` - -### Preview addresses (no deploy) - -```typescript -import { previewDeliveryProtectionOperator } from '@x402r/core/deploy'; - -const preview = await previewDeliveryProtectionOperator( - publicClient, - { - chainId: 84532, - arbiter: '0xArbiterAddress...', - feeRecipient: '0xYourAddress...', - escrowPeriodSeconds: 604800n, - } -); - -console.log('Operator will be at:', preview.operatorAddress); -console.log('Arbiter condition:', preview.arbiterConditionAddress); -``` - ## Network support Today, deployment is supported on the chains in `@x402r/core`'s `x402rChains`: From c8a5fc6d63a2e5750a5a3034c8126b88f89921d2 Mon Sep 17 00:00:00 2001 From: A1igator Date: Mon, 11 May 2026 23:19:11 -0700 Subject: [PATCH 24/37] docs: address PR #35 review (API alignment + structure + style) Addresses the audit at PR #35 review (vraspar). Organized by category: API alignment vs x402r-sdk packages/core + packages/sdk: - Canonical addresses rotated to PR #122 (x402r-canonical-v1.0.1 salt namespace). 30+ address replacements across sdk/overview, contracts/periphery/{auth-capture-escrow,overview, receiver-refund-collector,refund-request-evidence}, contracts/factories. - feeRecipient -> feeReceiver in SDK/contract contexts (option names, slot names FEE_RECIPIENT -> FEE_RECEIVER, OperatorConfig struct field, log keys). Wire-format spec correctly keeps feeRecipient (spec-level field renames to feeReceiver in PaymentInfo canonical struct). - Deploy-result JS field names shortened to match SDK exports: voidPreActionConditionAddress -> voidConditionAddress capturePreActionConditionAddress -> captureConditionAddress authorizePostActionHookAddress -> authorizeHookAddress paymentIndexRecorderAddress -> paymentIndexRecorderHookAddress - OperatorSlots JS interface uses short form (config.captureCondition, config.voidHook, etc.) - solidity slot CONSTANTS keep PRE/POST suffix. - registerEscrowScheme -> registerAuthCaptureEvmScheme. - PaymentOperatorABI from '@x402r/core/abis' -> paymentOperatorAbi from '@x402r/core' (lowercase + root subpath). - allowArbiterRefund default false (was true in docs). - PaymentState enum import dropped from sdk/concepts.mdx, sdk/client/payment-queries.mdx, sdk/merchant/{payment-operations, quickstart}.mdx. getPaymentState() returns a tuple, not an enum; numeric state comments replaced. - getFeeAddresses return shape: operatorFeeCalculator, protocolFeeCalculator, operatorFeeRecipient, protocolFeeRecipient (not feeCalculator/feeRecipient). - calculateFees return shape: protocolFeeAmount, operatorFeeAmount, totalFeeAmount + bps fields (not operatorFee/protocolFee/totalFee). - forwardToArbiter page: scheme guard "commerce" -> "authCapture"; payload shape corrected (no nested paymentInfo - the wire payload carries the signature + salt; PaymentInfo is reconstructed); dropped non-existent re-exports (arbiterRegistry, usdcTvlLimit) from the helpers import list; added the actual @x402r/evm wire-format type re-exports that helpers does ship. - delivery-arbiter.mdx: parseForwardedPayload doesn't exist; rewrote /verify handler to reconstruct PaymentInfo from the wire payload. Also arbiter.refund.voidPayment -> arbiter.payment.voidPayment (voidPayment lives on PaymentActions, not RefundActions). - RefundRequest contract page: rewrote signatures - no nonce arg, no updateStatus entrypoint; arbiter uses deny()/refuse(); approval flips automatically via VOID_POST_ACTION_HOOK / REFUND_POST_ACTION_HOOK when the refund is executed through the operator. Each payment has one request keyed by paymentInfoHash. Other fixes: - sdk/examples.mdx: rewrote the unclosed-fence/orphan-content mess. Same MDX-corruption class as the earlier deploy-operator fix. - sdk/concepts.mdx, sdk/limitations.mdx, index.mdx: stale "release" prose (table cells, mermaid labels, role descriptions) renamed to "capture" / "void" to match the actual operator surface. - index.mdx Supported Networks table: dropped Ethereum Sepolia (only Base + Base Sepolia are in x402rChains). - AI-slop sweep against the project's hard-ban list: "flexible refund capabilities", "out of the box" (x2), "strong foundation", "under the hood", "built-in mechanism / batch method", "everything merchants need", "enables `reclaim()`" - all rewritten. Deferred (per reviewer's "non-blocking" tag): - Navigation orphans (~22 files not in docs.json). Needs maintainer decision on add-to-nav vs delete - left as-is for now. - Split deploy-operator.mdx into marketplace + delivery sibling pages. - Numbered H3s -> conversion in delivery-arbiter / delivery- merchant. - Typed Returns/Parameters tables across sdk/merchant/* and sdk/create-client.mdx. - Templated "X client provides methods for Y" intro at 15 sites. Verified locally: npx mintlify@latest broken-links passes. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/architecture.mdx | 6 +- contracts/audits.mdx | 2 +- contracts/conditions/combinators.mdx | 8 +- contracts/conditions/custom.mdx | 2 +- contracts/examples.mdx | 30 ++--- contracts/factories.mdx | 52 ++++---- contracts/fees.mdx | 28 ++--- contracts/overview.mdx | 2 +- contracts/payment-operator.mdx | 10 +- contracts/periphery/auth-capture-escrow.mdx | 6 +- contracts/periphery/overview.mdx | 34 ++--- .../periphery/receiver-refund-collector.mdx | 2 +- .../periphery/refund-request-evidence.mdx | 4 +- contracts/periphery/refund-request.mdx | 118 ++++++++---------- contracts/recorders/combinator.mdx | 4 +- index.mdx | 5 +- sdk/arbiter/batch-operations.mdx | 2 +- sdk/cli.mdx | 2 +- sdk/client/payment-queries.mdx | 33 ++--- sdk/client/quickstart.mdx | 2 +- sdk/client/subscriptions.mdx | 2 +- sdk/concepts.mdx | 41 +++--- sdk/create-client.mdx | 4 +- sdk/delivery-arbiter.mdx | 18 +-- sdk/deploy-operator.mdx | 38 +++--- sdk/examples.mdx | 102 +++++---------- sdk/facilitator/getting-started.mdx | 8 +- sdk/helpers/forward-to-arbiter.mdx | 79 +++++++----- sdk/limitations.mdx | 2 +- sdk/merchant/payment-operations.mdx | 35 +++--- sdk/merchant/quickstart.mdx | 35 +++--- sdk/overview.mdx | 6 +- x402-integration/auth-capture-scheme.mdx | 4 +- 33 files changed, 350 insertions(+), 376 deletions(-) diff --git a/contracts/architecture.mdx b/contracts/architecture.mdx index 86646d0..4c0e001 100644 --- a/contracts/architecture.mdx +++ b/contracts/architecture.mdx @@ -206,13 +206,13 @@ Fees are **additive**: `totalFee = protocolFee + operatorFee` For a 1000 USDC payment with 3 bps protocol fee + 2 bps operator fee: - **Protocol Fee:** 0.30 USDC (3 bps) → `protocolFeeRecipient` on ProtocolFeeConfig -- **Operator Fee:** 0.20 USDC (2 bps) → `FEE_RECIPIENT` on operator +- **Operator Fee:** 0.20 USDC (2 bps) → `FEE_RECEIVER` on operator - **Total Fee:** 0.50 USDC (5 bps) - **Receiver Gets:** 999.50 USDC Fees accumulate in the operator and are distributed via `distributeFees(token)`. -**FEE_RECIPIENT** can be: +**FEE_RECEIVER** can be: - Arbiter address (marketplace with disputes) - Service provider address (subscriptions, APIs) - Platform treasury (platform-controlled) @@ -297,7 +297,7 @@ event RefundExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, address indexed // Fee distribution event FeesDistributed(address indexed token, uint256 protocolAmount, uint256 arbiterAmount); -event OperatorDeployed(address indexed operator, address indexed feeRecipient, address indexed capturePreActionCondition); +event OperatorDeployed(address indexed operator, address indexed deployer, address indexed feeReceiver); // Freeze state (Freeze contract events) event PaymentFrozen(bytes32 indexed paymentInfoHash, uint40 frozenAt); diff --git a/contracts/audits.mdx b/contracts/audits.mdx index 0eb9e8b..1b09ad3 100644 --- a/contracts/audits.mdx +++ b/contracts/audits.mdx @@ -34,7 +34,7 @@ These audits cover the core escrow lifecycle: authorize, capture, void, reclaim, ### What This Means -- The audited escrow layer provides a strong foundation, fund custody, token transfers, and payment state transitions have been professionally reviewed +- The audited escrow layer covers fund custody, token transfers, and payment state transitions - The condition/recorder plugin system is stateless or minimal-state by design, reducing attack surface diff --git a/contracts/conditions/combinators.mdx b/contracts/conditions/combinators.mdx index 5f00507..eb43b01 100644 --- a/contracts/conditions/combinators.mdx +++ b/contracts/conditions/combinators.mdx @@ -19,7 +19,7 @@ const comboAddress = await andConditionFactory.write.deploy([ ]); // Use in operator config -config.capturePreActionCondition = comboAddress; +config.captureCondition = comboAddress; ``` **Example:** Release requires receiver AND escrow period passed. @@ -34,7 +34,7 @@ const comboAddress = await orConditionFactory.write.deploy([ [RECEIVER_CONDITION, ARBITER_CONDITION] ]); -config.capturePreActionCondition = comboAddress; +config.captureCondition = comboAddress; ``` **Example:** Either receiver or arbiter can release. @@ -47,7 +47,7 @@ Inverts a condition (`!A`). // Anyone EXCEPT payer can call const comboAddress = await notConditionFactory.write.deploy([PAYER_CONDITION]); -config.capturePreActionCondition = comboAddress; +config.captureCondition = comboAddress; ``` **Example:** Prevent payer from releasing their own payment. @@ -66,7 +66,7 @@ const capturePreActionCondition = await andConditionFactory.write.deploy([ [receiverOrArbiter, ESCROW_PERIOD_ADDRESS] ]); -config.capturePreActionCondition = capturePreActionCondition; +config.captureCondition = capturePreActionCondition; ``` **Logic Tree:** diff --git a/contracts/conditions/custom.mdx b/contracts/conditions/custom.mdx index 9d1ef80..2225473 100644 --- a/contracts/conditions/custom.mdx +++ b/contracts/conditions/custom.mdx @@ -51,7 +51,7 @@ Usage, deploy via a factory and use in operator config: const businessHours = await timeOfDayConditionFactory.write.deploy([9, 17]); // Use in operator config -config.capturePreActionCondition = businessHours; +config.captureCondition = businessHours; ``` ## Security Checklist diff --git a/contracts/examples.mdx b/contracts/examples.mdx index 5ee17c1..58fa188 100644 --- a/contracts/examples.mdx +++ b/contracts/examples.mdx @@ -73,7 +73,7 @@ The configuration examples below use simplified pseudo-code (e.g., `new StaticAd ```typescript const config = { - feeRecipient: arbiterAddress, // Arbiter earns fees for dispute resolution + feeReceiver: arbiterAddress, // Arbiter earns fees for dispute resolution feeCalculator: feeCalculatorAddress, // Operator fee calculator authorizePreActionCondition: ALWAYS_TRUE_CONDITION, authorizePostActionHook: escrowPeriod, // Same address for recording auth time @@ -135,7 +135,7 @@ sequenceDiagram const arbiterCondition = await new StaticAddressCondition(arbiterAddress); const config = { - feeRecipient: arbiterAddress, // Arbiter earns fees + feeReceiver: arbiterAddress, // Arbiter earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator authorizePreActionCondition: '0x0000000000000000000000000000000000000000', // Not used (charge handles auth) authorizePostActionHook: '0x0000000000000000000000000000000000000000', @@ -201,7 +201,7 @@ const capturePreActionCondition = await new AndCondition([ ]); const config = { - feeRecipient: arbiterAddress, // Arbiter earns fees + feeReceiver: arbiterAddress, // Arbiter earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator authorizePreActionCondition: ALWAYS_TRUE_CONDITION, authorizePostActionHook: escrowPeriod, // Same address for recording auth time @@ -276,7 +276,7 @@ const capturePreActionCondition = await new AndCondition([ ]); const config = { - feeRecipient: arbiterAddress, // Arbiter earns fees + feeReceiver: arbiterAddress, // Arbiter earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator authorizePreActionCondition: PAYER_CONDITION, // Only payer authorizes authorizePostActionHook: escrowPeriod, // Record auth time @@ -338,7 +338,7 @@ sequenceDiagram const arbiterCondition = await new StaticAddressCondition(arbiterAddress); const config = { - feeRecipient: arbiterAddress, // Arbiter earns all fees + feeReceiver: arbiterAddress, // Arbiter earns all fees feeCalculator: feeCalculatorAddress, // Operator fee calculator authorizePreActionCondition: arbiterCondition.address, // Arbiter creates payments authorizePostActionHook: '0x0000000000000000000000000000000000000000', @@ -395,7 +395,7 @@ const refundCondition = await new OrCondition([ ]); const config = { - feeRecipient: arbiterAddress, // Arbiter earns fees + feeReceiver: arbiterAddress, // Arbiter earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator authorizePreActionCondition: ALWAYS_TRUE_CONDITION, authorizePostActionHook: escrowPeriod, // Record auth time @@ -452,7 +452,7 @@ const providerCondition = await new StaticAddressCondition(serviceProviderAddres // Receiver can charge immediately (no escrow) const config = { - feeRecipient: serviceProviderAddress, // Service provider earns fees + feeReceiver: serviceProviderAddress, // Service provider earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator authorizePreActionCondition: PAYER_CONDITION, // Payer sets up subscription authorizePostActionHook: '0x0000000000000000000000000000000000000000', @@ -524,7 +524,7 @@ sequenceDiagram const daoCondition = await new StaticAddressCondition(DAO_MULTISIG_ADDRESS); const config = { - feeRecipient: DAO_MULTISIG_ADDRESS, // DAO treasury earns fees + feeReceiver: DAO_MULTISIG_ADDRESS, // DAO treasury earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator authorizePreActionCondition: daoCondition.address, // DAO authorizes grants authorizePostActionHook: '0x0000000000000000000000000000000000000000', @@ -588,12 +588,12 @@ sequenceDiagram // Deploy condition for platform address const platformCondition = await new StaticAddressCondition(PLATFORM_ADDRESS); -// Time-proportional charge condition (custom, not provided out of the box, +// Time-proportional charge condition (custom, not provided shipped with the SDK, // this is a hypothetical custom condition you would implement yourself) const timeProportionalCondition = await new TimeProportionalCondition(); const config = { - feeRecipient: PLATFORM_ADDRESS, // Platform earns fees + feeReceiver: PLATFORM_ADDRESS, // Platform earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator authorizePreActionCondition: PAYER_CONDITION, // Payer authorizes stream authorizePostActionHook: '0x0000000000000000000000000000000000000000', @@ -665,7 +665,7 @@ sequenceDiagram const platformCondition = await new StaticAddressCondition(PLATFORM_ADDRESS); const config = { - feeRecipient: PLATFORM_ADDRESS, // Platform earns fees + feeReceiver: PLATFORM_ADDRESS, // Platform earns fees feeCalculator: feeCalculatorAddress, // Operator fee calculator authorizePreActionCondition: PAYER_CONDITION, // Payer creates invoice payment authorizePostActionHook: '0x0000000000000000000000000000000000000000', @@ -754,7 +754,7 @@ Before deploying, verify: ```typescript import { createTestClient, http, parseUnits, keccak256, toHex } from 'viem'; import { baseSepolia } from 'viem/chains'; -import { PaymentOperatorABI } from '@x402r/core/abis'; +import { paymentOperatorAbi } from '@x402r/core'; // Deploy on Base Sepolia first const operatorAddress = await factory.write.deployOperator([config]); @@ -768,7 +768,7 @@ const testClient = createTestClient({ // 1. Authorize await walletClient.writeContract({ address: operatorAddress, - abi: PaymentOperatorABI, + abi: paymentOperatorAbi, functionName: 'authorize', args: [paymentInfo, parseUnits('100', 6), tokenCollectorAddress, collectorData], }); @@ -778,7 +778,7 @@ await walletClient.writeContract({ try { await walletClient.writeContract({ address: operatorAddress, - abi: PaymentOperatorABI, + abi: paymentOperatorAbi, functionName: 'release', args: [paymentInfo, parseUnits('100', 6)], }); @@ -793,7 +793,7 @@ await testClient.mine({ blocks: 1 }); // 4. Release after escrow await walletClient.writeContract({ address: operatorAddress, - abi: PaymentOperatorABI, + abi: paymentOperatorAbi, functionName: 'release', args: [paymentInfo, parseUnits('100', 6)], }); diff --git a/contracts/factories.mdx b/contracts/factories.mdx index 98907e1..4eac432 100644 --- a/contracts/factories.mdx +++ b/contracts/factories.mdx @@ -48,13 +48,13 @@ Deploys PaymentOperator instances with deterministic addresses. All factories use universal CREATE2 addresses (same on every chain). -**PaymentOperatorFactory:** `0x4D9BC2Ba2D0d9AFb6B63E3afBbfC95143E6E8Da9` +**PaymentOperatorFactory:** `0xa0d4734842df1690a5B33Cb21828c946e39D55a2` ### Configuration Structure ```solidity struct OperatorConfig { - address feeRecipient; // Who receives operator fees + address feeReceiver; // Who receives operator fees address feeCalculator; // Operator fee calculator (IFeeCalculator) address authorizePreActionCondition; address authorizePostActionHook; @@ -78,7 +78,7 @@ function deployOperator( ``` **Parameters (in config):** -- `feeRecipient` - Who receives operator fees (arbiter, service provider, treasury, etc.) +- `feeReceiver` - Who receives operator fees (arbiter, service provider, treasury, etc.) - `authorizePreActionCondition` through `refundPostActionHook` - 10-slot configuration **Note:** `maxFeeBps` and `protocolFeePct` are set at factory level (shared across all operators) @@ -98,7 +98,7 @@ function computeAddress( **Usage:** ```typescript const config = { - feeRecipient: arbiterAddress, + feeReceiver: arbiterAddress, authorizePreActionCondition: ALWAYS_TRUE_CONDITION, // ... rest of config }; @@ -121,7 +121,7 @@ assert(deployedAddress === predictedAddress); import { createWalletClient, http, getContract, zeroAddress } from 'viem'; import { base } from 'viem/chains'; import { privateKeyToAccount } from 'viem/accounts'; -import { PaymentOperatorABI } from '@x402r/core/abis'; +import { paymentOperatorAbi } from '@x402r/core'; const FACTORY_ADDRESS = '0x...'; // Replace with actual factory address @@ -134,7 +134,7 @@ const walletClient = createWalletClient({ const factory = getContract({ address: FACTORY_ADDRESS, - abi: PaymentOperatorABI, + abi: paymentOperatorAbi, client: walletClient }); @@ -146,17 +146,17 @@ const arbiterConditionAddress = /* get from receipt */; const capturePreActionConditionHash = await andConditionFactory.write.deploy([ [arbiterConditionAddress, escrowPeriodAddress] ]); -const capturePreActionConditionAddress = /* get from receipt */; +const captureConditionAddress = /* get from receipt */; // Define configuration const config = { - feeRecipient: arbiterAddress, // Arbiter earns fees + feeReceiver: arbiterAddress, // Arbiter earns fees feeCalculator: feeCalculatorAddress, authorizePreActionCondition: ALWAYS_TRUE_CONDITION, authorizePostActionHook: escrowPeriodAddress, chargePreActionCondition: zeroAddress, // Default allow chargePostActionHook: zeroAddress, // No recording - capturePreActionCondition: capturePreActionConditionAddress, + capturePreActionCondition: captureConditionAddress, capturePostActionHook: zeroAddress, voidPreActionCondition: arbiterConditionAddress, voidPostActionHook: zeroAddress, @@ -179,7 +179,7 @@ console.log("Deployed marketplace operator at:", operatorAddress); const providerCondition = await new StaticAddressCondition(serviceProviderAddress); const config = { - feeRecipient: serviceProviderAddress, // Provider earns fees + feeReceiver: serviceProviderAddress, // Provider earns fees authorizePreActionCondition: PAYER_CONDITION, authorizePostActionHook: zeroAddress, chargePreActionCondition: providerCondition.address, @@ -208,7 +208,7 @@ Deploys `EscrowPeriod` contracts - combined recorder and condition for time-base ### Contract Address -**EscrowPeriodFactory:** `0x15DB06aADEB3a39D47756Bf864a173cc48bafe24` +**EscrowPeriodFactory:** `0xe72D2014ebC48F1d92521e8629574918E8030548` ### Deployment Method @@ -297,7 +297,7 @@ Deploys `Freeze` condition contracts that block release when a payment is frozen ### Contract Address -**FreezeFactory:** `0xdf129EFFE040c3403aca597c0F0bb704859a78Fd` +**FreezeFactory:** `0xeC092cf1215DB44af0Abe87c1157E304FEa5d0Eb` ### Deployment Method @@ -354,9 +354,9 @@ Use these pre-deployed condition contracts: | Condition | Address (all chains) | Description | |-----------|---------------------|-------------| -| PayerCondition | `0x33F5F1154A02d0839266EFd23Fd3b85a3505bB4B` | Only payer can call | -| ReceiverCondition | `0xF41974A853940Ff4c18d46B6565f973c1180E171` | Only receiver can call | -| AlwaysTrueCondition | `0xb295df7E7f786fd84D614AB26b1f2e86026C3483` | Anyone can call | +| PayerCondition | `0x586486394C38A2a7d36B16a3FDaF366cd202d823` | Only payer can call | +| ReceiverCondition | `0x321651df4593DA57C413579c5b611D1A90168a3A` | Only receiver can call | +| AlwaysTrueCondition | `0x2ef2A6162aEF9Df1022ff51c011af94D99AB4904` | Anyone can call | ### Example Deployments @@ -489,18 +489,18 @@ Each factory uses different salt strategies: **PaymentOperatorFactory:** ```solidity bytes32 key = keccak256(abi.encodePacked( - config.feeRecipient, + config.feeReceiver, config.feeCalculator, - config.authorizePreActionCondition, - config.authorizePostActionHook, - config.chargePreActionCondition, - config.chargePostActionHook, - config.capturePreActionCondition, - config.capturePostActionHook, - config.voidPreActionCondition, - config.voidPostActionHook, - config.refundPreActionCondition, - config.refundPostActionHook + config.authorizeCondition, + config.authorizeHook, + config.chargeCondition, + config.chargeHook, + config.captureCondition, + config.captureHook, + config.voidCondition, + config.voidHook, + config.refundCondition, + config.refundHook )); ``` diff --git a/contracts/fees.mdx b/contracts/fees.mdx index 5e08280..80af4c3 100644 --- a/contracts/fees.mdx +++ b/contracts/fees.mdx @@ -26,7 +26,7 @@ flowchart TD ACC --> DIST["distributeFees()"] DIST --> PR["protocolFeeRecipient (on ProtocolFeeConfig)"] - DIST --> FR["FEE_RECIPIENT + DIST --> FR["FEE_RECEIVER (on Operator)"] ``` @@ -35,7 +35,7 @@ flowchart TD | Layer | Configured By | Mutability | Recipient | |-------|--------------|------------|-----------| | **Protocol Fee** | `ProtocolFeeConfig` (shared) | Swappable calculator with 7-day timelock | `protocolFeeRecipient` on ProtocolFeeConfig | -| **Operator Fee** | `FEE_CALCULATOR` (per-operator) | Immutable — set at deploy time | `FEE_RECIPIENT` on operator | +| **Operator Fee** | `FEE_CALCULATOR` (per-operator) | Immutable: set at deploy time | `FEE_RECEIVER` on operator | ### Example Calculation @@ -44,7 +44,7 @@ For a 1000 USDC payment with 50 bps protocol fee + 250 bps operator fee: | Component | Rate | Amount | Goes To | |-----------|------|--------|---------| | Protocol Fee | 50 bps (0.5%) | 5.00 USDC | `protocolFeeRecipient` | -| Operator Fee | 250 bps (2.5%) | 25.00 USDC | `FEE_RECIPIENT` | +| Operator Fee | 250 bps (2.5%) | 25.00 USDC | `FEE_RECEIVER` | | **Total Fee** | **300 bps (3%)** | **30.00 USDC** | | | **Receiver Gets** | | **970.00 USDC** | Payment receiver | @@ -67,11 +67,11 @@ interface IFeeCalculator { } ``` -This enables flexible fee models — static rates, volume-based tiers, per-token pricing, or any custom logic. +This enables flexible fee models, static rates, volume-based tiers, per-token pricing, or any custom logic. ## StaticFeeCalculator -The simplest implementation — returns a fixed basis points value for every payment: +The simplest implementation, returns a fixed basis points value for every payment: ```solidity contract StaticFeeCalculator is IFeeCalculator { @@ -117,11 +117,11 @@ mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees; **Flow:** 1. `authorize()` calculates fees and stores them in `authorizedFees[hash]` -2. `release()` uses the stored fees — not the current calculator rates +2. `release()` uses the stored fees, not the current calculator rates 3. Protocol fee timelocks can't break already-authorized payments -`charge()` calculates fees inline since it authorizes and captures atomically — there's no gap where fees could change. +`charge()` calculates fees inline since it authorizes and captures atomically, there's no gap where fees could change. ## Fee Bounds Validation @@ -145,7 +145,7 @@ Fees accumulate in the operator contract and are distributed via `distributeFees // Anyone can call to distribute fees for a token operator.distributeFees(usdcAddress); // Protocol share → protocolFeeRecipient -// Operator share → FEE_RECIPIENT +// Operator share → FEE_RECEIVER ``` **How it works:** @@ -153,11 +153,11 @@ operator.distributeFees(usdcAddress); 2. Protocol share = `accumulatedProtocolFees[token]` (tracked per-token) 3. Operator share = remaining balance 4. Transfer protocol share to `protocolFeeRecipient` -5. Transfer operator share to `FEE_RECIPIENT` +5. Transfer operator share to `FEE_RECEIVER` 6. Reset accumulated tracking to 0 -`distributeFees()` is permissionless — anyone can trigger distribution. This prevents fees from being stuck in the operator. +`distributeFees()` is permissionless, anyone can trigger distribution. This prevents fees from being stuck in the operator. ## ProtocolFeeConfig @@ -201,18 +201,18 @@ await protocolFeeConfig.executeRecipient(); ``` -Operator fees are **immutable** — set at deploy time via `IFeeCalculator` and `FEE_RECIPIENT`. Only protocol fees can be changed (with 7-day timelock). Already-authorized payments use locked fee rates regardless. +Operator fees are **immutable**: set at deploy time via `IFeeCalculator` and `FEE_RECEIVER`. Only protocol fees can be changed (with 7-day timelock). Already-authorized payments use locked fee rates regardless. ### Disabling Protocol Fees Set the protocol fee calculator to `address(0)` to disable protocol fees entirely. The operator will calculate 0 bps for the protocol layer. -## FEE_RECIPIENT Roles +## FEE_RECEIVER Roles -The operator's `FEE_RECIPIENT` varies by use case: +The operator's `FEE_RECEIVER` varies by use case: -| Use Case | FEE_RECIPIENT | Description | +| Use Case | FEE_RECEIVER | Description | |----------|---------------|-------------| | Marketplace | Arbiter address | Arbiter earns fees for dispute resolution | | Subscription | Service provider | Provider earns fees for service delivery | diff --git a/contracts/overview.mdx b/contracts/overview.mdx index 50e5bd0..0cfd604 100644 --- a/contracts/overview.mdx +++ b/contracts/overview.mdx @@ -6,7 +6,7 @@ icon: "circle-info" ## What is x402r? -x402r is a smart contract extension for HTTP-native refundable payments. It builds on the canonical [Commerce Payments Protocol](https://github.com/base/commerce-payments) to add dispute resolution, escrow periods, and flexible refund capabilities. +x402r is a smart contract extension for HTTP-native refundable payments. It builds on the canonical [Commerce Payments Protocol](https://github.com/base/commerce-payments) to add dispute resolution, escrow periods, and refund capabilities. ## Architecture Layers diff --git a/contracts/payment-operator.mdx b/contracts/payment-operator.mdx index 5e7dbb9..e570f0d 100644 --- a/contracts/payment-operator.mdx +++ b/contracts/payment-operator.mdx @@ -18,7 +18,7 @@ The main payment operator contract with pluggable conditions for flexible author ```solidity address public immutable ESCROW; // AuthCaptureEscrow address -address public immutable FEE_RECIPIENT; // Operator fee recipient +address public immutable FEE_RECEIVER; // Operator fee recipient ProtocolFeeConfig public immutable PROTOCOL_FEE_CONFIG; // Shared protocol fee config IFeeCalculator public immutable FEE_CALCULATOR; // Operator fee calculator ``` @@ -235,7 +235,7 @@ ProtocolFeeConfig public immutable PROTOCOL_FEE_CONFIG; IFeeCalculator public immutable FEE_CALCULATOR; // Fee recipients -address public immutable FEE_RECIPIENT; // Operator fee recipient +address public immutable FEE_RECEIVER; // Operator fee recipient // Protocol fee recipient is on ProtocolFeeConfig // Fee tracking for accurate distribution @@ -246,7 +246,7 @@ mapping(address token => uint256) public accumulatedProtocolFees; For a 1000 USDC payment: - **Protocol Fee:** 3 bps (0.03%) = 0.30 USDC -> goes to `protocolFeeRecipient` -- **Operator Fee:** 2 bps (0.02%) = 0.20 USDC -> goes to `FEE_RECIPIENT` +- **Operator Fee:** 2 bps (0.02%) = 0.20 USDC -> goes to `FEE_RECEIVER` - **Total Fee:** 5 bps (0.05%) = 0.50 USDC - **Receiver Gets:** 999.50 USDC @@ -261,7 +261,7 @@ struct AuthorizedFees { mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees; ``` -**FEE_RECIPIENT** can be: +**FEE_RECEIVER** can be: - Arbiter (marketplace with disputes) - Service Provider (subscriptions) - Platform Treasury (platform-controlled) @@ -275,7 +275,7 @@ Fees accumulate in the operator contract and are distributed via `distributeFees // Anyone can call to distribute fees for a token operator.distributeFees(usdcAddress); // Protocol share -> protocolFeeRecipient -// Operator share -> FEE_RECIPIENT +// Operator share -> FEE_RECEIVER ``` ### Protocol Fee Changes (7-day Timelock) diff --git a/contracts/periphery/auth-capture-escrow.mdx b/contracts/periphery/auth-capture-escrow.mdx index 5d896e0..7a641f3 100644 --- a/contracts/periphery/auth-capture-escrow.mdx +++ b/contracts/periphery/auth-capture-escrow.mdx @@ -14,9 +14,9 @@ All three are deployed at universal CREATE2 addresses (same address on every sup | Contract | Canonical address | |---|---| -| `AuthCaptureEscrow` | `0xF8211868187974a7Fb9d99b8fFB171AD70665Dc6` | -| `ERC3009PaymentCollector` | `0x7561DC178D9aD5bc5fb103C01f448A510d2A36D0` | -| `Permit2PaymentCollector` | `0xD8490609d2da0ee626b0e676941b225cbc1A8C08` | +| `AuthCaptureEscrow` | `0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff` | +| `ERC3009PaymentCollector` | `0x0E3dF9510de65469C4518D7843919c0b8C7A7757` | +| `Permit2PaymentCollector` | `0x992476B9Ee81d52a5BdA0622C333938D0Af0aB26` | ## AuthCaptureEscrow diff --git a/contracts/periphery/overview.mdx b/contracts/periphery/overview.mdx index 9ba8b11..527139d 100644 --- a/contracts/periphery/overview.mdx +++ b/contracts/periphery/overview.mdx @@ -23,33 +23,33 @@ All periphery contracts use **universal CREATE2 addresses**: the same address on | Contract | Address | |----------|---------| -| AuthCaptureEscrow | `0xe050bB89eD43BB02d71343063824614A7fb80B77` | -| ERC3009PaymentCollector | `0xcE66Ab399EDA513BD12760b6427C87D6602344a7` | -| ProtocolFeeConfig | `0x7e868A42a458fa2443b6259419aA6A8a161E08c8` | -| ReceiverRefundCollector | `0xE5500a38BE45a6C598420fbd7867ac85EC451A07` | -| RefundRequestEvidence | `0xF97aAB816b7cbe53025454ad05b03cf5C361F1BA` | +| AuthCaptureEscrow | `0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff` | +| ERC3009PaymentCollector | `0x0E3dF9510de65469C4518D7843919c0b8C7A7757` | +| ProtocolFeeConfig | `0xBe2d24614F339a1eB103A399F93AA2a39Ca815Bc` | +| ReceiverRefundCollector | `0x88C9826dFA17Ad9d3a726015C667dD995394D341` | +| RefundRequestEvidence | `0x4089A5A853e9eF35f504B842795fB272dF69c739` | ### Factories | Factory | Address | |---------|---------| -| PaymentOperatorFactory | `0x4D9BC2Ba2D0d9AFb6B63E3afBbfC95143E6E8Da9` | -| EscrowPeriodFactory | `0x15DB06aADEB3a39D47756Bf864a173cc48bafe24` | -| FreezeFactory | `0xdf129EFFE040c3403aca597c0F0bb704859a78Fd` | -| StaticFeeCalculatorFactory | `0x6CDdBdB46e2d7Caae31A6b213B59a1412d7f16Ac` | -| StaticAddressConditionFactory | `0xfB09350b200fda7dDd06565F5296A0CA625311d5` | -| AndConditionFactory | `0x5a1F3b6d030D25a2B86aAE469Ae1216ef3be308D` | -| OrConditionFactory | `0x101B2fac8cdC6348E541A0ef087275dA62AA13A0` | -| NotConditionFactory | `0x1D58f97843579356863d3393ebe24feEd76ceefF` | -| RecorderCombinatorFactory | `0xACf2b5e21CFc14135C9cD43ebE96a481F184C1A1` | +| PaymentOperatorFactory | `0xa0d4734842df1690a5B33Cb21828c946e39D55a2` | +| EscrowPeriodFactory | `0xe72D2014ebC48F1d92521e8629574918E8030548` | +| FreezeFactory | `0xeC092cf1215DB44af0Abe87c1157E304FEa5d0Eb` | +| StaticFeeCalculatorFactory | `0x97F99AB01F86b480f751B7b81166Dbe1F113e6C3` | +| StaticAddressConditionFactory | `0x77B379390750E1d3F802cC220926694D2454903E` | +| AndConditionFactory | `0x2B07d750C639b65a26e43F1FDCE404b21DCf16D9` | +| OrConditionFactory | `0x0519a37c0A996DD5F1e81e07b4aD3B24C257BC90` | +| NotConditionFactory | `0xb9c3223D059C3cAbD482bB54f3d7cD52DE70A9ae` | +| RecorderCombinatorFactory | `0x30B5373FD791D2d7b28C3B8020EB68b032f3f960` | ### Condition Singletons | Condition | Address | |-----------|---------| -| PayerCondition | `0x33F5F1154A02d0839266EFd23Fd3b85a3505bB4B` | -| ReceiverCondition | `0xF41974A853940Ff4c18d46B6565f973c1180E171` | -| AlwaysTrueCondition | `0xb295df7E7f786fd84D614AB26b1f2e86026C3483` | +| PayerCondition | `0x586486394C38A2a7d36B16a3FDaF366cd202d823` | +| ReceiverCondition | `0x321651df4593DA57C413579c5b611D1A90168a3A` | +| AlwaysTrueCondition | `0x2ef2A6162aEF9Df1022ff51c011af94D99AB4904` | All addresses are available programmatically via `@x402r/core`'s `getChainConfig(chainId)`. See [SDK Overview](/sdk/overview) for details. diff --git a/contracts/periphery/receiver-refund-collector.mdx b/contracts/periphery/receiver-refund-collector.mdx index e2422d2..b6b8988 100644 --- a/contracts/periphery/receiver-refund-collector.mdx +++ b/contracts/periphery/receiver-refund-collector.mdx @@ -8,7 +8,7 @@ icon: "arrow-rotate-left" - **Type:** Singleton (one per network) - **Purpose:** Collect tokens from the receiver to refund the payer after escrow release -- **Address:** `0xE5500a38BE45a6C598420fbd7867ac85EC451A07` (all chains) +- **Address:** `0x88C9826dFA17Ad9d3a726015C667dD995394D341` (all chains) ## Features diff --git a/contracts/periphery/refund-request-evidence.mdx b/contracts/periphery/refund-request-evidence.mdx index b799de7..ba70164 100644 --- a/contracts/periphery/refund-request-evidence.mdx +++ b/contracts/periphery/refund-request-evidence.mdx @@ -8,7 +8,7 @@ icon: "file-lines" - **Type:** Singleton (one per network) - **Purpose:** Store evidence for refund disputes on-chain -- **Address:** `0xF97aAB816b7cbe53025454ad05b03cf5C361F1BA` (all chains) +- **Address:** `0x4089A5A853e9eF35f504B842795fB272dF69c739` (all chains) ## Features @@ -21,4 +21,4 @@ icon: "file-lines" When a refund is disputed, parties submit evidence (documents, screenshots, logs) to IPFS and record the CID on-chain. The arbiter reviews evidence off-chain and submits an EIP-712 approval signature that anyone can relay. -This keeps dispute resolution costs low — the arbiter never needs to submit an on-chain transaction. +This keeps dispute resolution costs low, the arbiter never needs to submit an on-chain transaction. diff --git a/contracts/periphery/refund-request.mdx b/contracts/periphery/refund-request.mdx index 8c81763..f79df80 100644 --- a/contracts/periphery/refund-request.mdx +++ b/contracts/periphery/refund-request.mdx @@ -44,113 +44,101 @@ icon: "rotate-left" -## Request Status States +## Request status states + +Each payment supports one refund request, keyed by `paymentInfoHash`. Requesting again is only allowed after the prior request was cancelled. ```mermaid stateDiagram-v2 - [*] --> Pending - Pending --> Approved: Arbiter approves - Pending --> Denied: Arbiter denies - Pending --> Cancelled: Requester cancels - Approved --> [*]: Arbiter executes refund via operator + [*] --> Pending: payer requestRefund() + Pending --> Approved: arbiter (via operator hook on capture/void/refund) + Pending --> Denied: arbiter deny() + Pending --> Refused: arbiter refuse() + Pending --> Cancelled: payer cancelRefundRequest() + Cancelled --> Pending: payer requestRefund() again note right of Approved - Arbiter must call - operator.void() - or operator.refund() + Status flips to Approved automatically + when the arbiter executes the refund + via operator.void() / operator.refund() + (wired through VOID_POST_ACTION_HOOK). end note ``` -## Key Methods +## Key methods ### requestRefund() -Creates a new refund request. +Creates a refund request for this payment. Only the payer can call. ```solidity function requestRefund( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, - uint120 amount, - uint256 nonce + uint120 amount ) external ``` **Parameters:** -- `paymentInfo` - Payment info struct -- `amount` - Amount being requested for refund -- `nonce` - Record index (from PaymentIndexRecorder) identifying which charge/action +- `paymentInfo`: PaymentInfo struct +- `amount`: amount being requested for refund (uint120) -**Access Control:** Only the payer who made the authorization can request +**Reverts** if a non-cancelled request already exists, if the payment is unknown to the canonical escrow, or if `amount == 0`. - -Each refund request is keyed by `(paymentInfoHash, nonce)` where nonce is the record index. This allows multiple refund requests per payment (one per charge/action). - - -### updateStatus() +### cancelRefundRequest() -Approve or deny a refund request. +Payer cancels their own pending request, freeing the slot to request again. ```solidity -function updateStatus( - AuthCaptureEscrow.PaymentInfo calldata paymentInfo, - uint256 nonce, - RequestStatus newStatus -) external +function cancelRefundRequest(AuthCaptureEscrow.PaymentInfo calldata paymentInfo) external ``` -**Parameters:** -- `paymentInfo` - Payment info struct -- `nonce` - Record index identifying which refund request -- `newStatus` - The new status (`Approved` or `Denied`) +**Access:** only the payer. -**Access:** Receiver can always approve/deny. While in escrow, anyone passing the operator's `VOID_PRE_ACTION_CONDITION` can also approve/deny. +### deny() -**Valid transitions:** -- `Pending` -> `Approved` -- `Pending` -> `Denied` +Arbiter rejects the claim after reviewing evidence. Terminal state. -### cancelRefundRequest() +```solidity +function deny(AuthCaptureEscrow.PaymentInfo calldata paymentInfo) external +``` -Payer cancels their own request. +### refuse() + +Arbiter refuses to consider the request (spam, out of jurisdiction, invalid). Terminal state. ```solidity -function cancelRefundRequest( - AuthCaptureEscrow.PaymentInfo calldata paymentInfo, - uint256 nonce -) external +function refuse(AuthCaptureEscrow.PaymentInfo calldata paymentInfo) external ``` -**Parameters:** -- `paymentInfo` - Payment info struct -- `nonce` - Record index identifying which refund request +### Approval -**Access:** Only the payer who created the request +There is no `updateStatus` / explicit `approve` entrypoint. Approval happens automatically when the arbiter executes the refund through the operator (`operator.void()` or `operator.refund()`), which fires `VOID_POST_ACTION_HOOK` / `REFUND_POST_ACTION_HOOK` and flips status to `Approved`. -**Valid transition:** -- `Pending` -> `Cancelled` +### Query helpers -## Usage Example +- `getRefundRequest(paymentInfo)`: full `RefundRequestData` for this payment +- `hasRefundRequest(paymentInfo)`: boolean +- `getRefundRequestStatus(paymentInfo)`: just the `RequestStatus` +- `getPayerRefundRequests(payer, offset, count)` / `getReceiverRefundRequests(...)` / `getOperatorRefundRequests(...)`: paginated index lookups + +## Usage example ```typescript -// 1. Payer requests refund (nonce 0 = first action on this payment) -await refundRequest.requestRefund(paymentInfo, requestedAmount, 0); +// 1. Payer files refund request +await refundRequest.write.requestRefund([paymentInfo, requestedAmount]) // Status: Pending -// 2. Receiver (or arbiter) reviews and approves -await refundRequest.updateStatus( - paymentInfo, - 0, // nonce - RequestStatus.Approved -); -// Status: Approved - -// 3. Execute refund via operator (separate transaction) -// For pre-capture: operator.void() empties the auth (full only). -// For post-capture: operator.refund(paymentInfo, amount, tokenCollector, collectorData). -await operator.void(paymentInfo); -// Funds returned to payer +// 2a. Arbiter denies (terminal): +await refundRequest.write.deny([paymentInfo]) + +// 2b. Arbiter approves by executing the refund via the operator. +// The post-action hook auto-flips the request to Approved. +// Before capture: +await operator.write.void([paymentInfo, '0x']) +// After capture: +await operator.write.refund([paymentInfo, amount, tokenCollector, collectorData]) ``` -RefundRequest is **advisory only**. Approval does not automatically execute refunds - the authorized party must call the operator's refund function. +RefundRequest is the request-tracking layer. Executing the refund is always a separate call into the operator/escrow. diff --git a/contracts/recorders/combinator.mdx b/contracts/recorders/combinator.mdx index 0258777..690f8b3 100644 --- a/contracts/recorders/combinator.mdx +++ b/contracts/recorders/combinator.mdx @@ -14,10 +14,10 @@ Deploy via RecorderCombinatorFactory: ```typescript const comboAddress = await recorderCombinatorFactory.write.deploy([ - [escrowPeriodAddress, paymentIndexRecorderAddress] // Records auth time + payment index + [escrowPeriodAddress, paymentIndexRecorderHookAddress] // Records auth time + payment index ]); -config.authorizePostActionHook = comboAddress; +config.authorizeHook = comboAddress; ``` ## Behavior diff --git a/index.mdx b/index.mdx index 33b333a..6bdfb80 100644 --- a/index.mdx +++ b/index.mdx @@ -75,10 +75,10 @@ x402r consists of these core components: | Component | Purpose | |-----------|---------| -| **PaymentOperator** | Manages payment authorization, release, charge, and refunds with pluggable conditions | +| **PaymentOperator** | Manages payment authorization, capture, charge, void, and refunds with pluggable conditions | | **AuthCaptureEscrow** | Holds ERC-20 tokens during the payment lifecycle (from commerce-payments) | | **Conditions & Recorders** | Pluggable authorization checks (before action) and state updates (after action) | -| **EscrowPeriod & Freeze** | Time-based release and freeze policies for buyer protection | +| **EscrowPeriod & Freeze** | Time-based capture and freeze policies for buyer protection | | **RefundRequest** | Handles refund request lifecycle and approvals | All protocol contracts use universal CREATE2 addresses, same address on every supported chain. @@ -91,7 +91,6 @@ Today, the supported chains in `@x402r/core` are **Base** and **Base Sepolia**. |---|---|---| | Base | 8453 | Supported | | Base Sepolia | 84532 | Testnet | -| Ethereum Sepolia | 11155111 | Testnet | ## Resources diff --git a/sdk/arbiter/batch-operations.mdx b/sdk/arbiter/batch-operations.mdx index a57ae37..93bcece 100644 --- a/sdk/arbiter/batch-operations.mdx +++ b/sdk/arbiter/batch-operations.mdx @@ -4,7 +4,7 @@ description: "Process multiple refund decisions efficiently as an arbiter" icon: "layer-group" --- -The arbiter client can process multiple refund requests by iterating over cases and calling refund/payment methods individually. There is no built-in batch method, but you can build batch workflows on top of the action groups. +The arbiter client can process multiple refund requests by iterating over cases and calling refund/payment methods individually. There is no batch method, but you can build batch workflows on top of the action groups. Each refund decision is a separate on-chain transaction. If one fails mid-batch, previously processed items are not rolled back. Design your error handling accordingly. diff --git a/sdk/cli.mdx b/sdk/cli.mdx index faaa55e..ac0e6be 100644 --- a/sdk/cli.mdx +++ b/sdk/cli.mdx @@ -187,7 +187,7 @@ The `@x402r/cli` package exports: ### Supported chains -The CLI auto-detects the chain from the 402 response's `accepts[].network` field. Any EVM chain known to `viem/chains` works out of the box (Base, Base Sepolia, Ethereum, Arbitrum, Optimism, and others). For unknown chain IDs, pass `--rpc ` to provide an RPC endpoint. +The CLI auto-detects the chain from the 402 response's `accepts[].network` field. Any EVM chain known to `viem/chains` is supported (Base, Base Sepolia, Ethereum, Arbitrum, Optimism, and others). For unknown chain IDs, pass `--rpc ` to provide an RPC endpoint. ## Next steps diff --git a/sdk/client/payment-queries.mdx b/sdk/client/payment-queries.mdx index f44caf3..0e2c1c1 100644 --- a/sdk/client/payment-queries.mdx +++ b/sdk/client/payment-queries.mdx @@ -8,17 +8,18 @@ The payer client provides methods for querying payment state and history across ## payment.getState -Derive the lifecycle state of a payment from the operator contract. +Returns the lifecycle position of a payment as a tuple. ```typescript -const state = await client.payment.getState(paymentInfo) - -// Returns a PaymentState number: -// 0 = NonExistent - Payment has never been authorized -// 1 = InEscrow - Funds locked, capturableAmount > 0 -// 2 = Released - Funds released to receiver -// 3 = Settled - Payment fully settled -// 4 = Expired - Authorization expired +const [hasCollectedPayment, capturableAmount, refundableAmount] = + await client.payment.getState(paymentInfo) + +// Interpret: +// !hasCollectedPayment → not yet authorized +// hasCollectedPayment && capturableAmount > 0n → in escrow, still capturable +// hasCollectedPayment && capturableAmount === 0n && refundableAmount > 0n +// → captured, still refundable +// refundableAmount === 0n → fully settled ``` ## payment.getAmounts @@ -35,7 +36,7 @@ console.log('Refundable:', amounts.refundableAmount) ## query.getPayerPayments -List all payments where a given address is the payer. Requires `paymentIndexRecorderAddress` in the client config. +List all payments where a given address is the payer. Requires `paymentIndexRecorderHookAddress` in the client config. ```typescript const payments = await client.query?.getPayerPayments(payerAddress) @@ -72,9 +73,9 @@ Retrieve all slot addresses from the PaymentOperator contract. ```typescript const config = await client.operator.getConfig() -console.log('Release condition:', config.capturePreActionCondition) +console.log('Release condition:', config.captureCondition) console.log('Fee calculator:', config.feeCalculator) -console.log('Fee recipient:', config.feeRecipient) +console.log('Fee recipient:', config.feeReceiver) ``` ## operator.calculateFees @@ -84,10 +85,10 @@ Calculate the fee breakdown for a given payment amount. ```typescript const fees = await client.operator.calculateFees(paymentInfo, 1_000_000n) -console.log('Operator fee:', fees.operatorFee) -console.log('Protocol fee:', fees.protocolFee) -console.log('Total fee:', fees.totalFee) -console.log('Net amount:', fees.netAmount) +console.log('Operator fee:', fees.operatorFeeAmount) +console.log('Protocol fee:', fees.protocolFeeAmount) +console.log('Total fee:', fees.totalFeeAmount) +console.log('Net amount:', fees.netAmount) ``` ## Next steps diff --git a/sdk/client/quickstart.mdx b/sdk/client/quickstart.mdx index 724a52a..a9dfe77 100644 --- a/sdk/client/quickstart.mdx +++ b/sdk/client/quickstart.mdx @@ -42,7 +42,7 @@ The payer client provides these action groups: - **`freeze`**: `freeze`, `unfreeze`, `isFrozen` - **`watch`**: `onPayment`, `onRefundRequest`, `onRefundExecuted`, `onFeeDistribution` - **`operator`**: `getConfig`, `getFeeAddresses`, `calculateFees` and more -- **`query`**: `getPayerPayments`, `getReceiverPayments`, `getPayment` (requires `paymentIndexRecorderAddress`) +- **`query`**: `getPayerPayments`, `getReceiverPayments`, `getPayment` (requires `paymentIndexRecorderHookAddress`) Optional groups (`escrow`, `refund`, `evidence`, `freeze`, `query`) are `undefined` if you do not pass the corresponding contract address. Use optional chaining when calling them. diff --git a/sdk/client/subscriptions.mdx b/sdk/client/subscriptions.mdx index a92e35e..1460753 100644 --- a/sdk/client/subscriptions.mdx +++ b/sdk/client/subscriptions.mdx @@ -4,7 +4,7 @@ description: "Subscribe to real-time payment, refund, and fee events" icon: "bell" --- -The `watch` action group provides real-time event subscriptions using viem's `watchContractEvent` under the hood. Each method returns an unsubscribe function you should call when you no longer need the watcher. +The `watch` action group provides real-time event subscriptions using viem's `watchContractEvent` underneath. Each method returns an unsubscribe function you should call when you no longer need the watcher. The `watch` group is always available on the client (no optional address required). diff --git a/sdk/concepts.mdx b/sdk/concepts.mdx index 64acace..31c1f93 100644 --- a/sdk/concepts.mdx +++ b/sdk/concepts.mdx @@ -6,27 +6,24 @@ icon: "lightbulb" ## Payment States -Every payment in X402r goes through a defined lifecycle represented by the `PaymentState` enum: +`getPaymentState()` returns a tuple, not an enum. There is no exported `PaymentState` symbol; derive lifecycle position from the returned values. ```typescript -import { PaymentState } from '@x402r/core'; - -// PaymentState values: -PaymentState.NonExistent // 0 - Payment doesn't exist -PaymentState.InEscrow // 1 - Funds held in escrow -PaymentState.Released // 2 - Funds released to merchant -PaymentState.Settled // 3 - Payment fully settled -PaymentState.Expired // 4 - Payment expired +// getPaymentState() return type: +// readonly [hasCollectedPayment: boolean, capturableAmount: bigint, refundableAmount: bigint] +const [collected, capturable, refundable] = await client.payment.getState(paymentInfo) ``` +Conceptually, every payment moves through this lifecycle: + ```mermaid stateDiagram-v2 [*] --> InEscrow: authorize() - InEscrow --> Released: release() + InEscrow --> Captured: capture() InEscrow --> Settled: voidPayment() - InEscrow --> Expired: escrow period passes - Released --> Settled: settle() - Expired --> [*] + InEscrow --> Reclaimable: captureDeadline passes + Captured --> Settled: refund() + Reclaimable --> Settled: reclaim() Settled --> [*] ``` @@ -57,11 +54,11 @@ Use `computePaymentInfoHash()` from `@x402r/sdk` to compute the unique hash of a ## Escrow Period -The **EscrowPeriod** contract tracks when a payment was authorized and enforces a configurable waiting period before funds can be released. During the escrow period: +The **EscrowPeriod** contract tracks when a payment was authorized and enforces a configurable waiting period before funds can be captured. During the escrow period: - **Payers** can request refunds or freeze the payment -- **Merchants** can refund but cannot release -- **After the period**: merchants can release funds to themselves +- **Merchants** can void (return funds) but cannot capture +- **After the period**: merchants can capture funds ```typescript import { createPayerClient } from '@x402r/sdk' @@ -136,7 +133,7 @@ const status = await client.refund.getStatus(paymentInfo); ## Freeze / Unfreeze -The **Freeze** contract allows payers to freeze a payment during the escrow period, preventing release until the freeze expires or is lifted: +The **Freeze** contract allows payers to freeze a payment during the escrow period, preventing capture until the freeze expires or is lifted: ```typescript // Payer freezes payment (requires payer authorization) @@ -150,15 +147,15 @@ const frozen = await client.freeze?.isFrozen(paymentInfo) ``` -Freezing a payment does not automatically escalate to an arbiter. It pauses the release condition to allow time for dispute resolution. +Freezing a payment does not automatically escalate to an arbiter. It pauses the capture condition to allow time for dispute resolution. ## Roles and Permissions | Role | Can Do | -|------|--------| +|---|---| | **Payer** | Request refunds, freeze payments, cancel requests, query escrow state | -| **Merchant** | Release payments, charge, void (auto-approves requests), deny refunds | +| **Merchant** | Capture payments, charge, void (auto-approves requests), deny refunds | | **Arbiter** | Deny/refuse disputed refunds, void, review evidence | ## Contract Architecture @@ -168,9 +165,9 @@ flowchart TB subgraph PO[PaymentOperator] direction LR auth[authorize] - rel[release] + cap[capture] chg[charge] - ref[voidPayment] + v[void] refp[refund] end diff --git a/sdk/create-client.mdx b/sdk/create-client.mdx index 38c92ff..473f399 100644 --- a/sdk/create-client.mdx +++ b/sdk/create-client.mdx @@ -57,7 +57,7 @@ Type narrowing is a DX convenience, not a security boundary. On-chain [condition | `refundRequestAddress` | `Address` | No | Activates `refund` group | | `refundRequestEvidenceAddress` | `Address` | No | Activates `evidence` group (requires `refundRequestAddress`) | | `freezeAddress` | `Address` | No | Activates `freeze` group | -| `paymentIndexRecorderAddress` | `Address` | No | Activates `query` group | +| `paymentIndexRecorderHookAddress` | `Address` | No | Activates `query` group | | `paymentStore` | `PaymentStore` | No | Pluggable storage for payment lookups | | `eventFromBlock` | `bigint` | No | Starting block for event-based payment lookups | @@ -72,7 +72,7 @@ Type narrowing is a DX convenience, not a security boundary. On-chain [condition | `refund` | 14 | `refundRequestAddress` | | `evidence` | 4 | `refundRequestEvidenceAddress` | | `freeze` | 3 | `freezeAddress` | -| `query` | 3 | `paymentIndexRecorderAddress` | +| `query` | 3 | `paymentIndexRecorderHookAddress` | Groups without their required address are `undefined` on the client. Use optional chaining: diff --git a/sdk/delivery-arbiter.mdx b/sdk/delivery-arbiter.mdx index ce788f7..7bcc26f 100644 --- a/sdk/delivery-arbiter.mdx +++ b/sdk/delivery-arbiter.mdx @@ -53,20 +53,25 @@ const arbiter = createArbiterClient({ ### 3. Handle the Verify Endpoint -The merchant's `forwardToArbiter()` hook POSTs to `/verify`. Use `parseForwardedPayload()` to extract typed `PaymentInfo` with BigInt fields restored: +The merchant's `forwardToArbiter()` hook POSTs `{ responseBody, transaction, paymentPayload }` to `/verify`. The payload does not contain `PaymentInfo` directly. Reconstruct it from `paymentPayload.accepted.extra` + `paymentPayload.payload.salt` + the recovered `payer` + the top-level requirements (`payTo`, `asset`, `amount`), or run the payload back through your own facilitator's verify step to recover it. ```typescript -import { parseForwardedPayload } from '@x402r/helpers' +import { isAuthCapturePayload } from '@x402r/helpers' import express from 'express' const app = express() app.use(express.json()) app.post('/verify', async (req, res) => { - const { responseBody, paymentInfo, network, transaction } = - parseForwardedPayload(req.body) + const { responseBody, transaction, paymentPayload } = req.body - const chainId = fromNetworkId(network) // "eip155:84532" -> 84532 + if (!isAuthCapturePayload(paymentPayload?.payload)) { + res.status(400).json({ error: 'unsupported_payload' }) + return + } + + // Reconstruct PaymentInfo from the wire payload (see forwardToArbiter docs). + const paymentInfo = reconstructPaymentInfo(paymentPayload) // Your evaluation logic const passed = await evaluate(responseBody) @@ -77,8 +82,7 @@ app.post('/verify', async (req, res) => { res.json({ verdict: 'PASS' }) } else { // Arbiter can refund immediately without waiting for escrow expiry. - const amounts = await arbiter.payment.getAmounts(paymentInfo) - await arbiter.refund.voidPayment(paymentInfo) + await arbiter.payment.voidPayment(paymentInfo) res.json({ verdict: 'FAIL' }) } }) diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index d7f48bb..57bcc7f 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -59,7 +59,7 @@ A complete marketplace operator deployment includes: |----------|---------|-------------| | `PRIVATE_KEY` |, | Deployer wallet (required) | | `ARBITER` | deployer address | Dispute resolver | - | `FEE_RECIPIENT` | deployer address | Receives operator fees | + | `FEE_RECEIVER` | deployer address | Receives operator fees | | `ESCROW_PERIOD` | `604800` (7 days) | Escrow period in seconds | | `FREEZE_DURATION` | `259200` (3 days) | Freeze duration in seconds | | `FEE_BPS` | `100` (1%) | Operator fee in basis points | @@ -118,7 +118,7 @@ const result = await deployMarketplaceOperator( publicClient, { chainId: 84532, // Base Sepolia - feeRecipient: account.address, // receives operator fees + feeReceiver: account.address, // receives operator fees arbiter: '0xArbiterAddress...', // dispute resolver escrowPeriodSeconds: 604800n, // 7 days freezeDurationSeconds: 259200n, // 3 days max freeze @@ -138,7 +138,7 @@ console.log('Existing (reused):', result.summary.existingCount); | Option | Type | Description | |--------|------|-------------| | `chainId` | `number` | Target chain ID (e.g., `84532` for Base Sepolia) | -| `feeRecipient` | `Address` | Address that receives operator fees | +| `feeReceiver` | `Address` | Address that receives operator fees | | `arbiter` | `Address` | Arbiter address for dispute resolution | | `escrowPeriodSeconds` | `bigint` | Escrow waiting period (e.g., `604800n` for 7 days) | | `freezeDurationSeconds` | `bigint` | How long freezes last. Default: `0n` (permanent until unfrozen) | @@ -154,7 +154,7 @@ interface MarketplaceOperatorDeployment { freezeAddress: Address | null // Freeze condition (null if disabled) refundRequestAddress: Address // RefundRequest contract refundRequestEvidenceAddress: Address // RefundRequestEvidence contract - voidPreActionConditionAddress: Address // OR(Receiver, Arbiter) + voidConditionAddress: Address // OR(Receiver, Arbiter) feeCalculatorAddress: Address | null // null if no fee operatorConfig: OperatorConfig // Full operator slot configuration deployments: DeployResult[] // Per-contract deploy details @@ -177,7 +177,7 @@ import { previewMarketplaceOperator } from '@x402r/core' const preview = await previewMarketplaceOperator(publicClient, { chainId: 84532, - feeRecipient: '0xYourAddress...', + feeReceiver: '0xYourAddress...', arbiter: '0xArbiterAddress...', escrowPeriodSeconds: 604800n, }) @@ -200,7 +200,7 @@ The deployed marketplace operator has the following slot configuration: | `VOID_POST_ACTION_HOOK` | RefundRequest | Tracks refund request state | | `REFUND_PRE_ACTION_CONDITION` | Receiver | Only receiver after escrow | | `FEE_CALCULATOR` | StaticFeeCalculator | Fixed percentage fee (if configured) | -| `FEE_RECIPIENT` | Your address | Receives fees | +| `FEE_RECEIVER` | Your address | Receives fees | --- @@ -247,7 +247,7 @@ const deployment = await deployDeliveryProtectionOperator( { chainId: 84532, arbiter: '0xArbiterServiceAddress', - feeRecipient: account.address, + feeReceiver: account.address, escrowPeriodSeconds: 300n, // 5 minutes }, ) @@ -255,19 +255,19 @@ const deployment = await deployDeliveryProtectionOperator( console.log('Operator:', deployment.operatorAddress) console.log('EscrowPeriod:', deployment.escrowPeriodAddress) console.log('ArbiterCondition:', deployment.arbiterConditionAddress) -console.log('ReleaseCondition:', deployment.capturePreActionConditionAddress) -console.log('AuthorizeRecorder:', deployment.authorizePostActionHookAddress) +console.log('ReleaseCondition:', deployment.captureConditionAddress) +console.log('AuthorizeRecorder:', deployment.authorizeHookAddress) ``` | Option | Type | Description | |--------|------|-------------| | `chainId` | `number` | Target chain | | `arbiter` | `Address` | Arbiter address for release and refund decisions | -| `feeRecipient` | `Address` | Receives protocol fees | +| `feeReceiver` | `Address` | Receives protocol fees | | `escrowPeriodSeconds` | `bigint` | Verification window before auto-refund | | `authorizedCodehash` | `Hex` | Override the default `recorderCombinatorCodehash`. Optional | -| `paymentIndexRecorderAddress` | `Address` | Override the default PaymentIndexRecorder. Pass `zeroAddress` to skip on-chain payment indexing. Optional | -| `allowArbiterRefund` | `boolean` | Allow arbiter to refund immediately during escrow. Default: `true` | +| `paymentIndexRecorderHookAddress` | `Address` | Override the default PaymentIndexRecorder. Pass `zeroAddress` to skip on-chain payment indexing. Optional | +| `allowArbiterRefund` | `boolean` | Allow arbiter to refund immediately during escrow. Default: `false` | @@ -276,10 +276,10 @@ console.log('AuthorizeRecorder:', deployment.authorizePostActionHookAddress) operatorAddress: Address escrowPeriodAddress: Address arbiterConditionAddress: Address - capturePreActionConditionAddress: Address // OrCondition([arbiter, payer]) - voidPreActionConditionAddress: Address // OrCondition([escrowPeriod, receiver, arbiter]) - authorizePostActionHookAddress: Address // RecorderCombinator([escrowPeriod, paymentIndexRecorder]) - paymentIndexRecorderAddress: Address + captureConditionAddress: Address // OrCondition([arbiter, payer]) + voidConditionAddress: Address // OrCondition([escrowPeriod, receiver, arbiter]) + authorizeHookAddress: Address // RecorderCombinator([escrowPeriod, paymentIndexRecorder]) + paymentIndexRecorderHookAddress: Address operatorConfig: OperatorConfig deployments: DeployResult[] summary: { @@ -290,7 +290,7 @@ console.log('AuthorizeRecorder:', deployment.authorizePostActionHookAddress) } ``` - Deploys 6 contracts by default: EscrowPeriod, StaticAddressCondition(arbiter), OrCondition(release), OrCondition(refund), RecorderCombinator, and the Operator. If you pass `paymentIndexRecorderAddress: zeroAddress`, the RecorderCombinator is skipped (5 contracts). + Deploys 6 contracts by default: EscrowPeriod, StaticAddressCondition(arbiter), OrCondition(release), OrCondition(refund), RecorderCombinator, and the Operator. If you pass `paymentIndexRecorderHookAddress: zeroAddress`, the RecorderCombinator is skipped (5 contracts). Redeploying with the same parameters is idempotent (CREATE2). It detects existing contracts and skips them. @@ -304,13 +304,13 @@ console.log('AuthorizeRecorder:', deployment.authorizePostActionHookAddress) const preview = await previewDeliveryProtectionOperator(publicClient, { chainId: 84532, arbiter: '0xArbiterServiceAddress', - feeRecipient: account.address, + feeReceiver: account.address, escrowPeriodSeconds: 300n, }) console.log('Operator will be at:', preview.operatorAddress) console.log('EscrowPeriod will be at:', preview.escrowPeriodAddress) - console.log('AuthorizeRecorder will be at:', preview.authorizePostActionHookAddress) + console.log('AuthorizeRecorder will be at:', preview.authorizeHookAddress) ``` diff --git a/sdk/examples.mdx b/sdk/examples.mdx index 2ceb64b..4741440 100644 --- a/sdk/examples.mdx +++ b/sdk/examples.mdx @@ -4,122 +4,84 @@ description: "Runnable examples for every SDK operation." icon: "code" --- -The [x402r-sdk repo](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples) includes runnable examples for every role. Each example starts a local Anvil fork, deploys contracts, and runs. No wallet or testnet funds needed. +The [x402r-sdk repo](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples) ships runnable examples for every role. Each starts a local Anvil fork, deploys contracts, and runs end-to-end; no wallet or testnet funds needed. ## Examples - Operator-agnostic HTTP service implementing x402's facilitator protocol for escrow payments. Handles signature verification and on-chain settlement. + Operator-agnostic HTTP service implementing x402's facilitator protocol. Handles signature verification and on-chain settlement for `authCapture`. - Deploy a complete marketplace operator with escrow, freeze, and arbiter support. + Deploy a complete marketplace or delivery-protection operator via `deployMarketplaceOperator()` / `deployDeliveryProtectionOperator()`. - Express merchant server using `AuthCaptureServerScheme` and `HTTPFacilitatorClient` to accept escrow payments via x402 middleware. + Express merchant server using `AuthCaptureServerScheme` and `HTTPFacilitatorClient` to accept refundable payments via x402 middleware. - Hono merchant server using `AuthCaptureServerScheme` and `HTTPFacilitatorClient` to accept escrow payments via x402 middleware. + Hono merchant server using `AuthCaptureServerScheme` and `HTTPFacilitatorClient` to accept refundable payments via x402 middleware. - CLI tool for merchants to release payments, approve/deny refunds, and query escrow state. + CLI tool for merchants to capture funds, void / deny / approve refunds, and query escrow state. CLI tool for payers to `pay`, `preview-fee`, request refunds, freeze payments, and check status. - CLI tool for arbiters to review cases, make decisions, and manage registry. + CLI tool for arbiters to review cases, deny / refuse / execute refunds, and manage registry. - Shared utilities used by the CLI examples: `parsePaymentInfo`, `shortAddress`, `formatUSDC`. + Shared utilities for the CLI examples: `parsePaymentInfo`, `shortAddress`, `formatUSDC`. -## Running Examples +## Running examples -All examples require a private key with Base Sepolia ETH and USDC. See [Base network faucets](https://docs.base.org/base-chain/tools/network-faucets) for testnet tokens. +Mainnet runs require funded wallets on Base. For local runs, the Anvil-fork scripts seed accounts automatically. -The full payment flow requires the facilitator to be running before the merchant server: - -```bash + +```bash npm +git clone https://github.com/BackTrackCo/x402r-sdk.git +cd x402r-sdk +npm install && npm run build +``` +```bash pnpm git clone https://github.com/BackTrackCo/x402r-sdk.git cd x402r-sdk pnpm install && pnpm build ``` +```bash bun +git clone https://github.com/BackTrackCo/x402r-sdk.git +cd x402r-sdk +bun install && bun run build +``` + -Then run any example: +Then run a per-action example: ```bash -# Per-action examples pnpm example:payer:request-refund pnpm example:merchant:charge pnpm example:arbiter:approve-refund - -Deploys a complete marketplace operator using `deployMarketplaceOperator()`: - -```typescript -import { deployMarketplaceOperator } from '@x402r/core/deploy'; - -const result = await deployMarketplaceOperator( - walletClient, - publicClient, - { - chainId: 84532, // Base Sepolia - feeRecipient: account.address, - arbiter: arbiterAddress, - escrowPeriodSeconds: 604800n, // 7 days - operatorFeeBps: 100n, // 1% - } -); ``` -You can also deploy a simpler delivery protection operator for content verification use cases: - -```typescript -import { deployDeliveryProtectionOperator } from '@x402r/core/deploy'; - -const result = await deployDeliveryProtectionOperator( - walletClient, - publicClient, - { - chainId: 84532, - arbiter: arbiterAddress, - feeRecipient: account.address, - escrowPeriodSeconds: 604800n, - } -); -``` - -See [Deploy an operator](/sdk/deploy-operator) for the full guide. - -| Example | What it does | -|---------|-------------| -| [`payer/request-refund.ts`](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/payer/request-refund.ts) | Request a refund for a payment in escrow | -| [`payer/submit-evidence.ts`](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/payer/submit-evidence.ts) | Submit an IPFS evidence CID for a dispute | -| [`payer/freeze-payment.ts`](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/payer/freeze-payment.ts) | Freeze a payment to block release during investigation | - -Demonstrates minimal merchant servers (Express and Hono variants) that use `AuthCaptureServerScheme` and `HTTPFacilitatorClient` via x402's standard middleware: - -1. Returns 402 with inline escrow payment options -2. Delegates payment verification to the facilitator via `HTTPFacilitatorClient` -3. Delegates on-chain settlement to the facilitator after the handler runs -4. Returns weather data after successful payment +See the [examples directory README](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples) for the full list of scripts. -### Arbiter +## Next steps - - Deploy a PaymentOperator with escrow and freeze support. + + Walk through `deployMarketplaceOperator()` and `deployDeliveryProtectionOperator()`. - Forward escrow settlements to an arbiter service. + Forward `authCapture` settlements to an arbiter service. - - Understand the payment lifecycle and key concepts. + + The payment lifecycle and key terms. - Browse all examples on GitHub. + Browse every example. diff --git a/sdk/facilitator/getting-started.mdx b/sdk/facilitator/getting-started.mdx index 26f0f08..a58b442 100644 --- a/sdk/facilitator/getting-started.mdx +++ b/sdk/facilitator/getting-started.mdx @@ -40,7 +40,7 @@ Never commit private keys to source control. Use environment variables or a secr - Create `index.ts`. This is identical to a standard x402 facilitator, with two additions: the `@x402r/evm` import and the `registerEscrowScheme()` call. + Create `index.ts`. This is identical to a standard x402 facilitator, with two additions: the `@x402r/evm` import and the `registerAuthCaptureEvmScheme()` call. ```typescript import "dotenv/config"; @@ -48,7 +48,7 @@ Never commit private keys to source control. Use environment variables or a secr import { x402Facilitator } from "@x402/core/facilitator"; import { PaymentPayload, PaymentRequirements } from "@x402/core/types"; import { toFacilitatorEvmSigner } from "@x402/evm"; - import { registerEscrowScheme } from "@x402r/evm/authCapture/facilitator"; + import { registerAuthCaptureEvmScheme } from "@x402r/evm/authCapture/facilitator"; import { createWalletClient, http, publicActions } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { baseSepolia } from "viem/chains"; @@ -76,7 +76,7 @@ Never commit private keys to source control. Use environment variables or a secr const facilitator = new x402Facilitator(); // x402r: Register the escrow scheme to handle refundable payments - registerEscrowScheme(facilitator, { + registerAuthCaptureEvmScheme(facilitator, { signer: evmSigner, networks: "eip155:84532", }); @@ -127,7 +127,7 @@ Never commit private keys to source control. Use environment variables or a secr ## How it works - **`x402Facilitator`** is the core facilitator class from `@x402/core` that routes verify and settle requests to registered scheme handlers. -- **`registerEscrowScheme`** adds x402r escrow support to the facilitator, enabling it to verify escrow payment signatures and settle them on-chain. +- **`registerAuthCaptureEvmScheme`** adds x402r escrow support to the facilitator, enabling it to verify escrow payment signatures and settle them on-chain. - **`toFacilitatorEvmSigner`** adapts a viem wallet client into the signer interface the facilitator expects for on-chain interactions. - The three endpoints (`/verify`, `/settle`, `/supported`) match the interface that `HTTPFacilitatorClient` on the merchant side expects. diff --git a/sdk/helpers/forward-to-arbiter.mdx b/sdk/helpers/forward-to-arbiter.mdx index 02cb6d9..4d9c980 100644 --- a/sdk/helpers/forward-to-arbiter.mdx +++ b/sdk/helpers/forward-to-arbiter.mdx @@ -6,20 +6,20 @@ icon: "arrow-right" The `forwardToArbiter()` function creates an `onAfterSettle` hook that forwards the response body and payment payload to an arbiter service. It runs fire-and-forget so it never blocks the response to the client. -- Only fires for successful **commerce** scheme settlements +- Only fires for successful **`authCapture`** scheme settlements - POSTs `{ responseBody, transaction, paymentPayload }` to `{arbiterUrl}/verify` - Errors are silently caught so an unavailable arbiter cannot break the payment flow ## Usage ```typescript -import { forwardToArbiter } from '@x402r/helpers'; +import { forwardToArbiter } from '@x402r/helpers' const resourceServer = new x402ResourceServer(facilitatorClient) .register(networkId, new AuthCaptureServerScheme()) .onAfterSettle( - forwardToArbiter('http://arbiter:3001') - ); + forwardToArbiter('http://arbiter:3001'), + ) ``` ## Function signature @@ -27,14 +27,14 @@ const resourceServer = new x402ResourceServer(facilitatorClient) ```typescript function forwardToArbiter( arbiterUrl: string, - options?: ForwardToArbiterOptions + options?: ForwardToArbiterOptions, ): (context: SettleResultContext) => Promise ``` ### Parameters | Parameter | Type | Description | -|-----------|------|-------------| +|---|---|---| | `arbiterUrl` | `string` | Base URL of your arbiter service (e.g. `http://arbiter:3001`) | | `options` | `ForwardToArbiterOptions` | Optional configuration (see below) | @@ -43,39 +43,40 @@ function forwardToArbiter( ```typescript interface ForwardToArbiterOptions { /** Custom error handler. Defaults to `console.warn`. */ - onError?: (error: unknown) => void; + onError?: (error: unknown) => void } ``` ## Payload shape -When a commerce settlement succeeds, the hook POSTs the following JSON to `{arbiterUrl}/verify`: +When an `authCapture` settlement succeeds, the hook POSTs the following JSON to `{arbiterUrl}/verify`: ```typescript { - responseBody: string; // UTF-8 encoded response body - transaction: string; // Settlement transaction hash + responseBody: string // UTF-8 encoded response body + transaction: string // Settlement transaction hash paymentPayload: { - x402Version: number; + x402Version: number accepted: { - scheme: string; // e.g. "commerce" - network: string; // e.g. "eip155:84532" - // ...other accepted fields - }; - payload: { - paymentInfo: { - operator: string; - payer: string; - receiver: string; - // ...full PaymentInfo - }; - }; - }; + scheme: 'authCapture' + network: string // e.g. "eip155:84532" + amount: string + asset: `0x${string}` + payTo: `0x${string}` + maxTimeoutSeconds: number + extra: AuthCaptureExtra // captureAuthorizer, captureDeadline, refundDeadline, ... + } + payload: + | { authorization: { from, to, value, validAfter, validBefore, nonce }; signature; salt } // EIP-3009 + | { permit2Authorization: { from, permitted, spender, nonce, deadline }; signature; salt } // Permit2 + } } ``` +There is no nested `paymentInfo` in the payload. The arbiter reconstructs `PaymentInfo` from `accepted.extra` + `payload.salt` + the recovered `payer` + the top-level requirements (`payTo`, `asset`, `amount`), the same way the facilitator does at settlement. + -Arbiters that need `paymentInfo` for `capture()` can read it directly from `paymentPayload.payload.paymentInfo`, no extra RPC call needed. +The arbiter can also resolve the `PaymentInfo` indirectly: derive `paymentInfoHash` from the wire data, then read the on-chain `PaymentInfo` from the escrow if it stores it, or simply forward the wire payload back into your own facilitator to verify. ## Error handling @@ -83,15 +84,15 @@ Arbiters that need `paymentInfo` for `capture()` can read it directly from `paym By default, fetch errors are logged with `console.warn`. You can override this with a custom handler: ```typescript -import { forwardToArbiter } from '@x402r/helpers'; +import { forwardToArbiter } from '@x402r/helpers' const resourceServer = new x402ResourceServer(facilitatorClient) .register(networkId, new AuthCaptureServerScheme()) .onAfterSettle( forwardToArbiter('http://arbiter:3001', { onError: (err) => sentry.captureException(err), - }) - ); + }), + ) ``` Errors are wrapped in an `X402rError` with the arbiter URL and request details for easier debugging. @@ -101,7 +102,7 @@ Errors are wrapped in an `X402rError` with the arbiter URL and request details f The hook silently returns without making a request when: - The settlement was not successful (`context.result.success === false`) -- The scheme is not `commerce` (e.g. direct or other custom schemes) +- The scheme is not `authCapture` - No response body is available in the transport context ## Address re-exports @@ -113,17 +114,29 @@ import { authCaptureEscrow, tokenCollector, protocolFeeConfig, - arbiterRegistry, receiverRefundCollector, - usdcTvlLimit, factories, conditions, getChainConfig, supportedChainIds, -} from '@x402r/helpers'; +} from '@x402r/helpers' ``` -These are the same CREATE2 addresses available from `@x402r/core`. You can import from either package depending on which you already have installed. +Plus the `@x402r/evm` wire-format types and guards: + +```typescript +import { + type AuthCaptureExtra, + type AuthCapturePayload, + type Eip3009Payload, + type Permit2Payload, + type PaymentInfoStruct, + isAuthCaptureExtra, + isAuthCapturePayload, + isEip3009Payload, + isPermit2Payload, +} from '@x402r/helpers' +``` ## Next steps diff --git a/sdk/limitations.mdx b/sdk/limitations.mdx index 937ee46..fdf8cf3 100644 --- a/sdk/limitations.mdx +++ b/sdk/limitations.mdx @@ -46,7 +46,7 @@ const client = createPayerClient({ publicClient, walletClient, operatorAddress: '0x...', - paymentIndexRecorderAddress: '0x...', + paymentIndexRecorderHookAddress: '0x...', eventFromBlock: recentBlockNumber, }) diff --git a/sdk/merchant/payment-operations.mdx b/sdk/merchant/payment-operations.mdx index ea50ee6..89903ff 100644 --- a/sdk/merchant/payment-operations.mdx +++ b/sdk/merchant/payment-operations.mdx @@ -118,35 +118,37 @@ console.log('Refundable:', amounts.refundableAmount) ### payment.getState -Derive the lifecycle state of a payment from the operator contract. +Returns a tuple of the payment's lifecycle position. ```typescript -const state = await merchant.payment.getState(paymentInfo) -// 0 = NonExistent, 1 = InEscrow, 2 = Released, 3 = Settled, 4 = Expired +const [hasCollectedPayment, capturableAmount, refundableAmount] = + await merchant.payment.getState(paymentInfo) ``` ### operator.getConfig -Retrieve all slot addresses from the PaymentOperator contract, including conditions and recorders. +Retrieve all slot addresses from the PaymentOperator contract. ```typescript const config = await merchant.operator.getConfig() -console.log('Fee recipient:', config.feeRecipient) -console.log('Fee calculator:', config.feeCalculator) -console.log('Release condition:', config.capturePreActionCondition) +console.log('Fee receiver:', config.feeReceiver) +console.log('Fee calculator:', config.feeCalculator) +console.log('Capture condition:', config.captureCondition) ``` ### operator.getFeeAddresses -Get just the fee-related addresses. +Get the fee-related addresses. ```typescript const fees = await merchant.operator.getFeeAddresses() -console.log('Fee calculator:', fees.feeCalculator) -console.log('Protocol fee config:', fees.protocolFeeConfig) -console.log('Fee recipient:', fees.feeRecipient) +console.log('Operator fee calculator:', fees.operatorFeeCalculator) +console.log('Protocol fee config:', fees.protocolFeeConfig) +console.log('Protocol fee calculator:', fees.protocolFeeCalculator) +console.log('Operator fee recipient:', fees.operatorFeeRecipient) +console.log('Protocol fee recipient:', fees.protocolFeeRecipient) ``` ### operator.calculateFees @@ -156,10 +158,13 @@ Calculate the full fee breakdown for a payment amount. ```typescript const fees = await merchant.operator.calculateFees(paymentInfo, 1_000_000n) -console.log('Operator fee:', fees.operatorFee) -console.log('Protocol fee:', fees.protocolFee) -console.log('Total fee:', fees.totalFee) -console.log('Net amount:', fees.netAmount) +console.log('Operator fee bps:', fees.operatorFeeBps) +console.log('Protocol fee bps:', fees.protocolFeeBps) +console.log('Total fee bps:', fees.totalFeeBps) +console.log('Operator fee:', fees.operatorFeeAmount) +console.log('Protocol fee:', fees.protocolFeeAmount) +console.log('Total fee:', fees.totalFeeAmount) +console.log('Net amount:', fees.netAmount) ``` ## Release vs refund decision flow diff --git a/sdk/merchant/quickstart.mdx b/sdk/merchant/quickstart.mdx index f1b0692..6c84e4e 100644 --- a/sdk/merchant/quickstart.mdx +++ b/sdk/merchant/quickstart.mdx @@ -4,7 +4,7 @@ description: "Release funds, charge payments, process refunds, and query escrow icon: "rocket" --- -The `@x402r/sdk` package provides everything merchants need for the post-payment lifecycle: releasing escrowed funds, charging directly, processing refunds, and querying operator state. +The `@x402r/sdk` package covers the merchant's post-payment lifecycle: capturing escrowed funds, charging directly, processing refunds, and querying operator state. **Looking for server setup?** The [Merchant Server Quickstart](/sdk/merchant/getting-started) shows how to accept escrow payments via Express middleware. This page covers the `createMerchantClient` factory for managing payments after they arrive. @@ -143,11 +143,11 @@ if (amounts.capturableAmount > 0n) { ### payment.getState -Derive the lifecycle state of a payment from the operator contract. +Returns a tuple of the payment's lifecycle position. ```typescript -const state = await merchant.payment.getState(paymentInfo) -// 0 = NonExistent, 1 = InEscrow, 2 = Released, 3 = Settled, 4 = Expired +const [hasCollectedPayment, capturableAmount, refundableAmount] = + await merchant.payment.getState(paymentInfo) ``` ### operator.getConfig @@ -157,21 +157,23 @@ Retrieve all slot addresses from the PaymentOperator contract. ```typescript const config = await merchant.operator.getConfig() -console.log('Fee recipient:', config.feeRecipient) -console.log('Fee calculator:', config.feeCalculator) -console.log('Release condition:', config.capturePreActionCondition) +console.log('Fee receiver:', config.feeReceiver) +console.log('Fee calculator:', config.feeCalculator) +console.log('Capture condition:', config.captureCondition) ``` ### operator.getFeeAddresses -Get just the fee-related addresses. +Get the fee-related addresses. ```typescript const fees = await merchant.operator.getFeeAddresses() -console.log('Fee calculator:', fees.feeCalculator) -console.log('Protocol fee config:', fees.protocolFeeConfig) -console.log('Fee recipient:', fees.feeRecipient) +console.log('Operator fee calculator:', fees.operatorFeeCalculator) +console.log('Protocol fee config:', fees.protocolFeeConfig) +console.log('Protocol fee calculator:', fees.protocolFeeCalculator) +console.log('Operator fee recipient:', fees.operatorFeeRecipient) +console.log('Protocol fee recipient:', fees.protocolFeeRecipient) ``` ### operator.calculateFees @@ -181,10 +183,13 @@ Calculate the full fee breakdown for a payment amount. ```typescript const fees = await merchant.operator.calculateFees(paymentInfo, 1_000_000n) -console.log('Operator fee:', fees.operatorFee) -console.log('Protocol fee:', fees.protocolFee) -console.log('Total fee:', fees.totalFee) -console.log('Net amount:', fees.netAmount) +console.log('Operator fee bps:', fees.operatorFeeBps) +console.log('Protocol fee bps:', fees.protocolFeeBps) +console.log('Total fee bps:', fees.totalFeeBps) +console.log('Operator fee:', fees.operatorFeeAmount) +console.log('Protocol fee:', fees.protocolFeeAmount) +console.log('Total fee:', fees.totalFeeAmount) +console.log('Net amount:', fees.netAmount) ``` ## Release vs refund decision flow diff --git a/sdk/overview.mdx b/sdk/overview.mdx index 28b2765..d64ea17 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -115,9 +115,9 @@ The `base/commerce-payments@v1.0.0` primitives are redeployed at canonical CREAT | Contract | Address | |----------|---------| -| `AuthCaptureEscrow` | `0xF8211868187974a7Fb9d99b8fFB171AD70665Dc6` | -| `ERC3009PaymentCollector` | `0x7561DC178D9aD5bc5fb103C01f448A510d2A36D0` | -| `Permit2PaymentCollector` | `0xD8490609d2da0ee626b0e676941b225cbc1A8C08` | +| `AuthCaptureEscrow` | `0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff` | +| `ERC3009PaymentCollector` | `0x0E3dF9510de65469C4518D7843919c0b8C7A7757` | +| `Permit2PaymentCollector` | `0x992476B9Ee81d52a5BdA0622C333938D0Af0aB26` | Salt namespace: `commerce-payments::v1::`. diff --git a/x402-integration/auth-capture-scheme.mdx b/x402-integration/auth-capture-scheme.mdx index 482859d..e2a731d 100644 --- a/x402-integration/auth-capture-scheme.mdx +++ b/x402-integration/auth-capture-scheme.mdx @@ -8,7 +8,7 @@ icon: "file-contract" The **`authCapture` scheme** for x402 v2 uses the audited [Commerce Payments Protocol](https://github.com/base/commerce-payments) (`AuthCaptureEscrow` + token collectors) directly, no fork. The client signs a single signature (ERC-3009 or Permit2). The facilitator submits it, either locking funds in escrow for later capture (two-phase) or sending them directly to the receiver with refund capability (single-shot). -Unlike `exact`, which has no built-in mechanism for returning funds, `authCapture` supports returning funds to the client through void, refund, and reclaim. +Unlike `exact`, which has no mechanism for returning funds, `authCapture` supports returning funds to the client through void, refund, and reclaim. ## Settlement Paths @@ -338,7 +338,7 @@ 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()` | +| `authorizationExpiry` | `captureDeadline` | `capture()` | Blocks capture; allows `reclaim()` | | `refundExpiry` | `refundDeadline` | `refund()` | Blocks refund requests | ## Safety Guarantees From 0ce04f3107da775502a9cd8ae0c2877f4deeefa5 Mon Sep 17 00:00:00 2001 From: A1igator Date: Tue, 12 May 2026 00:04:33 -0700 Subject: [PATCH 25/37] docs: reframe plugins as Hooks + Conditions, expose nav orphans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reframes the plugin taxonomy to match the actual x402r-contracts source: the operator slot model is Conditions (pre-action gate, ICondition) and Hooks (post-action callback, IHook). There is no IRecorder interface in source; the "Recorders" group was a stale name for what are really Hooks. Architecture changes: - contracts/recorders/* → contracts/hooks/* (entire directory) - RecorderCombinator → HookCombinator (canonical contract name) - AuthorizationTimeRecorder → AuthorizationTimeRecorderHook (full source name kept) - PaymentIndexRecorder → PaymentIndexRecorderHook - IRecorder interface → IHook (canonical) - "Recorders" nav group → "Hooks" - *_RECORDER slot constants → *_POST_ACTION_HOOK (matches PaymentOperator.sol) - camelCase config struct: refundRecorder → refundHook, etc. RefundRequest reframed: RefundRequest extends BaseHook in source — it is a hook plugin, not a periphery contract. Moved the page from contracts/periphery/refund-request to contracts/hooks/refund-request and dropped it from the Periphery group. RefundRequestEvidence stays in Periphery (it's an evidence registry, not a hook). Periphery overview now points to the moved page and includes a callout that RefundRequest lives under Hooks. Stale "release" prose swept everywhere (~80 occurrences across 30 files): The operator's release() method was renamed to capture() in PR #34; this sweep finishes the prose alignment. Includes mermaid labels (Release[release] → Capture[capture]), table cells, sentence-level prose ("can release" → "can capture", "release condition" → "capture condition", "block release" → "block capture"), and Solidity refs. contracts/license.mdx kept its "first public release of each version" phrasing (a different meaning). Nav orphans added (23 pages): Every previously-orphaned .mdx is now reachable from the SDK tab nav: - Concepts group: concepts, limitations - Merchant group: + payment-operations, subscriptions, role hub - Payer (Client) group: quickstart, escrow-management, payment-queries, refund-operations, subscriptions, role hub - Arbiter group: quickstart, decision-submission, registry, ai-integration, batch-operations, subscriptions, role hub - Facilitator group: facilitator/getting-started - Helpers group: + erc8004 - AI Tools group: cursor, claude-code, windsurf Templated intros (4 sites) rewritten — dropped "The X client provides methods for Y" framing per the docs style audit. sdk/cli.mdx: moved "Supported chains" up to right after "Request options" where it explains the --chain flag (was buried after "Exports"). Numbered H3 headings replaced with in delivery-arbiter.mdx and delivery-merchant.mdx for proper right-rail anchors. delivery-merchant.mdx also got the forwardToArbiter payload shape corrected (no nested paymentInfo) and the registerAuthCaptureEvmScheme import. Redirects added for the moved paths so existing inbound links keep working: /contracts/periphery/refund-request → /contracts/hooks/refund-request and /contracts/recorders/:slug → /contracts/hooks/:slug. Verified locally: npx mintlify@latest broken-links passes. Co-Authored-By: Claude Opus 4.7 (1M context) --- ai-tools/cursor.mdx | 2 +- contracts/architecture.mdx | 36 +-- contracts/audits.mdx | 10 +- contracts/conditions/always-true.mdx | 2 +- contracts/conditions/combinators.mdx | 6 +- contracts/conditions/custom.mdx | 4 +- contracts/conditions/escrow-period.mdx | 10 +- contracts/conditions/freeze.mdx | 10 +- contracts/conditions/overview.mdx | 10 +- contracts/conditions/payer.mdx | 2 +- contracts/conditions/receiver.mdx | 6 +- contracts/examples.mdx | 54 ++--- contracts/factories.mdx | 20 +- contracts/fees.mdx | 2 +- contracts/gas-costs.mdx | 30 +-- .../authorization-time.mdx | 12 +- contracts/hooks/combinator.mdx | 50 +++++ contracts/{recorders => hooks}/custom.mdx | 48 ++-- contracts/hooks/overview.mdx | 101 +++++++++ .../{recorders => hooks}/payment-index.mdx | 12 +- .../{periphery => hooks}/refund-request.mdx | 0 contracts/overview.mdx | 22 +- contracts/payment-operator.mdx | 12 +- contracts/periphery/overview.mdx | 15 +- .../periphery/receiver-refund-collector.mdx | 4 +- contracts/recorders/combinator.mdx | 50 ----- contracts/recorders/overview.mdx | 101 --------- docs.json | 83 ++++++- index.mdx | 6 +- sdk/arbiter/decision-submission.mdx | 4 +- sdk/arbiter/quickstart.mdx | 2 +- sdk/cli.mdx | 8 +- sdk/client/escrow-management.mdx | 6 +- sdk/client/payment-queries.mdx | 6 +- sdk/concepts.mdx | 6 +- sdk/create-client.mdx | 4 +- sdk/delivery-arbiter.mdx | 210 +++++++++--------- sdk/delivery-merchant.mdx | 112 +++++----- sdk/delivery-protection.mdx | 4 +- sdk/deploy-operator.mdx | 32 +-- sdk/limitations.mdx | 2 +- sdk/merchant.mdx | 12 +- sdk/merchant/getting-started.mdx | 2 +- sdk/merchant/payment-operations.mdx | 20 +- sdk/merchant/quickstart.mdx | 24 +- sdk/merchant/refund-handling.mdx | 6 +- sdk/merchant/subscriptions.mdx | 10 +- sdk/overview.mdx | 6 +- sdk/payer.mdx | 4 +- x402-integration/auth-capture-scheme.mdx | 6 +- x402-integration/comparison.mdx | 4 +- x402-integration/overview.mdx | 16 +- 52 files changed, 646 insertions(+), 580 deletions(-) rename contracts/{recorders => hooks}/authorization-time.mdx (57%) create mode 100644 contracts/hooks/combinator.mdx rename contracts/{recorders => hooks}/custom.mdx (57%) create mode 100644 contracts/hooks/overview.mdx rename contracts/{recorders => hooks}/payment-index.mdx (72%) rename contracts/{periphery => hooks}/refund-request.mdx (100%) delete mode 100644 contracts/recorders/combinator.mdx delete mode 100644 contracts/recorders/overview.mdx diff --git a/ai-tools/cursor.mdx b/ai-tools/cursor.mdx index 061d618..81793af 100644 --- a/ai-tools/cursor.mdx +++ b/ai-tools/cursor.mdx @@ -356,7 +356,7 @@ API Use updates for changelogs: - + ## New features - Added bulk user import functionality - Improved error messages with actionable suggestions diff --git a/contracts/architecture.mdx b/contracts/architecture.mdx index 4c0e001..e0ad898 100644 --- a/contracts/architecture.mdx +++ b/contracts/architecture.mdx @@ -23,26 +23,26 @@ flowchart TB subgraph Operator["PaymentOperator (per config)"] Auth[authorize] Charge[charge] - Release[release] - RefundIE[voidPayment] - RefundPE[refund] + Capture[capture] + Void[void] + Refund[refund] end - subgraph Plugins["Conditions & Recorders"] + subgraph Plugins["Conditions & Hooks"] Cond["ICondition
(check before action)"] - Rec["IRecorder
(record after action)"] + Hook["IHook
(run after action)"] EP[EscrowPeriod] Freeze[Freeze] And[AndCondition] Or[OrCondition] + RR[RefundRequest] end Escrow[AuthCaptureEscrow] - RR[RefundRequest] Payer -->|authorize / freeze| Operator - Receiver -->|release / charge| Operator - DesAddr -->|refund / release| Operator + Receiver -->|capture / charge| Operator + DesAddr -->|void / refund / capture| Operator Payer -->|requestRefund| RR POF -->|deploys| Operator @@ -50,12 +50,13 @@ flowchart TB FF -->|deploys| Freeze Operator -->|checks| Cond - Operator -->|calls| Rec - Operator -->|locks/releases funds| Escrow + Operator -->|calls| Hook + Operator -->|locks/captures funds| Escrow EP -.->|implements| Cond - EP -.->|implements| Rec + EP -.->|implements| Hook Freeze -.->|implements| Cond + RR -.->|implements| Hook And -.->|composes| Cond Or -.->|composes| Cond ``` @@ -82,11 +83,10 @@ For additional visual diagrams, see the [x402r-contracts repository](https://git **Example: Marketplace with arbiter dispute resolution** -1. **Payer** calls `refundRequest.requestRefund(paymentInfo, amount, nonce)` +1. **Payer** calls `refundRequest.requestRefund(paymentInfo, amount)` 2. **RefundRequest** creates request with status `Pending` 3. **Designated address** (e.g., arbiter, DAO multisig) reviews dispute -4. **Designated address** calls `refundRequest.updateStatus(paymentInfo, nonce, Approved)` -5. **Designated address** calls `operator.void(paymentInfo)` +4. **Designated address** calls `operator.void(paymentInfo)`; the `VOID_POST_ACTION_HOOK` (RefundRequest) auto-flips status to `Approved` 6. **Operator** checks `VOID_PRE_ACTION_CONDITION` (configured per operator) 7. **Operator** calls `escrow.void()` to return all escrowed funds to payer 8. **Operator** calls `VOID_POST_ACTION_HOOK` @@ -104,10 +104,10 @@ Refund conditions are configurable. Can be arbiter-only (marketplace), receiver- - **Day 0:** Payment authorized, escrow period begins - **Day 0-7:** Payer can freeze if suspicious (per Freeze contract configuration) - **Day 3:** Payer freezes payment (freeze lasts 3 days per configuration) -- **Day 3-6:** Payment frozen, release blocked +- **Day 3-6:** Payment frozen, capture blocked - **Day 6:** Freeze expires automatically (or authorized address unfreezes early) - **Day 7:** Escrow period ends -- **Day 7+:** Authorized address(es) can release (if not frozen) +- **Day 7+:** Authorized address(es) can capture (if not frozen) Freeze policies are optional and configurable. Define who can freeze, who can unfreeze, and how long freeze lasts. @@ -132,7 +132,7 @@ When an action is called (e.g., `capture()`): - `true` → Proceed to execute action - `false` → Revert with `ConditionNotMet` error 4. **Execute Action** - Call escrow method -5. **Call Recorder** - Update state after successful execution +5. **Call Hook** - Run the matching `*_POST_ACTION_HOOK` after successful execution ### Combinator Example @@ -189,7 +189,7 @@ mapping(address token => uint256) public accumulatedProtocolFees; ### EscrowPeriod Recording ```solidity -// In EscrowPeriod (extends AuthorizationTimeRecorder) +// In EscrowPeriod (extends AuthorizationTimeRecorderHook) mapping(bytes32 paymentInfoHash => uint256 authorizedAt) public authorizationTimes; ``` diff --git a/contracts/audits.mdx b/contracts/audits.mdx index 1b09ad3..f2a83c8 100644 --- a/contracts/audits.mdx +++ b/contracts/audits.mdx @@ -21,21 +21,21 @@ These audits cover the core escrow lifecycle: authorize, capture, void, reclaim, | Component | Status | Risk | |-----------|--------|------| -| PaymentOperator | **Unaudited** | Core operator with condition/recorder dispatch and fee system | +| PaymentOperator | **Unaudited** | Core operator with condition/hook dispatch and fee system | | PaymentOperatorFactory | **Unaudited** | CREATE2 deterministic deployment | | ProtocolFeeConfig | **Unaudited** | Timelocked fee governance | | StaticFeeCalculator | **Unaudited** | Simple immutable fee calculator | | Condition plugins | **Unaudited** | PayerCondition, ReceiverCondition, StaticAddressCondition, AlwaysTrueCondition | | Combinator plugins | **Unaudited** | AndCondition, OrCondition, NotCondition | -| EscrowPeriod | **Unaudited** | Combined recorder + time-lock condition | +| EscrowPeriod | **Unaudited** | Combined hook + time-lock condition | | Freeze | **Unaudited** | Freeze/unfreeze state management | -| Recorder plugins | **Unaudited** | AuthorizationTimeRecorder, PaymentIndexRecorder, RecorderCombinator | +| Hook plugins | **Unaudited** | AuthorizationTimeRecorderHook, PaymentIndexRecorderHook, HookCombinator | | RefundRequest | **Unaudited** | Refund request lifecycle management | ### What This Means - The audited escrow layer covers fund custody, token transfers, and payment state transitions -- The condition/recorder plugin system is stateless or minimal-state by design, reducing attack surface +- The condition/hook plugin system is stateless or minimal-state by design, reducing attack surface Use x402r contracts on mainnet at your own risk. While we've built with security best practices (CEI pattern, reentrancy guards, immutable configuration, timelocked governance), the x402r-specific code has not undergone a formal audit. @@ -57,7 +57,7 @@ Even without a formal audit, the x402r contracts follow established security pat We plan to pursue third-party audits as the contract architecture and use cases stabilize. Priority order: 1. **PaymentOperator**: condition dispatch, fee calculation, fee locking, distribution -2. **Plugin system**: conditions, recorders, combinators, and their factories +2. **Plugin system**: conditions, hooks, combinators, and their factories 3. **EscrowPeriod + Freeze**: time-lock enforcement and freeze state management 4. **RefundRequest**: request lifecycle and access control diff --git a/contracts/conditions/always-true.mdx b/contracts/conditions/always-true.mdx index 31ea543..ebb7fdc 100644 --- a/contracts/conditions/always-true.mdx +++ b/contracts/conditions/always-true.mdx @@ -31,7 +31,7 @@ function check(PaymentInfo calldata payment, uint256, address caller) | `AUTHORIZE_PRE_ACTION_CONDITION` | Let anyone create payments (common for marketplace/e-commerce) | -**Use with caution for release/refund slots.** Setting `CAPTURE_PRE_ACTION_CONDITION` or `VOID_PRE_ACTION_CONDITION` to AlwaysTrueCondition means anyone can release or refund funds. This is functionally equivalent to leaving the slot as `address(0)` (the default behavior), but makes the intent explicit. +**Use with caution for capture/refund slots.** Setting `CAPTURE_PRE_ACTION_CONDITION` or `VOID_PRE_ACTION_CONDITION` to AlwaysTrueCondition means anyone can capture or refund funds. This is functionally equivalent to leaving the slot as `address(0)` (the default behavior), but makes the intent explicit. ## AlwaysTrueCondition vs `address(0)` diff --git a/contracts/conditions/combinators.mdx b/contracts/conditions/combinators.mdx index eb43b01..83811c0 100644 --- a/contracts/conditions/combinators.mdx +++ b/contracts/conditions/combinators.mdx @@ -22,14 +22,14 @@ const comboAddress = await andConditionFactory.write.deploy([ config.captureCondition = comboAddress; ``` -**Example:** Release requires receiver AND escrow period passed. +**Example:** Capture requires receiver AND escrow period passed. ## OrCondition At least one condition must pass (`A || B`). ```typescript -// Receiver OR Arbiter can release +// Receiver OR Arbiter can capture const comboAddress = await orConditionFactory.write.deploy([ [RECEIVER_CONDITION, ARBITER_CONDITION] ]); @@ -37,7 +37,7 @@ const comboAddress = await orConditionFactory.write.deploy([ config.captureCondition = comboAddress; ``` -**Example:** Either receiver or arbiter can release. +**Example:** Either receiver or arbiter can capture. ## NotCondition diff --git a/contracts/conditions/custom.mdx b/contracts/conditions/custom.mdx index 2225473..4bd3630 100644 --- a/contracts/conditions/custom.mdx +++ b/contracts/conditions/custom.mdx @@ -101,7 +101,7 @@ contract TimeOfDayConditionTest is Test { Review the condition system architecture. - - Build custom state recorders. + + Build custom state hooks.
diff --git a/contracts/conditions/escrow-period.mdx b/contracts/conditions/escrow-period.mdx index cf7e59d..d40fd0d 100644 --- a/contracts/conditions/escrow-period.mdx +++ b/contracts/conditions/escrow-period.mdx @@ -6,9 +6,9 @@ icon: "clock" ## Overview -EscrowPeriod is a dual-purpose contract, it functions as both a **recorder** and a **condition**: +EscrowPeriod is a dual-purpose contract, it functions as both a **hook** and a **condition**: -- **As a recorder:** Records the `block.timestamp` when a payment is authorized +- **As a hook:** Records the `block.timestamp` when a payment is authorized - **As a condition:** Returns `true` only after the escrow period has elapsed Use the **same address** for both `AUTHORIZE_POST_ACTION_HOOK` and `CAPTURE_PRE_ACTION_CONDITION` slots on the operator. @@ -19,12 +19,12 @@ Use the **same address** for both `AUTHORIZE_POST_ACTION_HOOK` and `CAPTURE_PRE_ ```mermaid flowchart LR - EP[EscrowPeriod] -->|extends| ATR[AuthorizationTimeRecorder] + EP[EscrowPeriod] -->|extends| ATR[AuthorizationTimeRecorderHook] EP -->|implements| IC[ICondition] - ATR -->|implements| IR[IRecorder] + ATR -->|implements| IR[IHook] ``` -EscrowPeriod extends [AuthorizationTimeRecorder](/contracts/recorders/authorization-time) and adds `ICondition` implementation. You don't need to deploy AuthorizationTimeRecorder separately, use EscrowPeriod directly. +EscrowPeriod extends [AuthorizationTimeRecorderHook](/contracts/hooks/authorization-time) and adds `ICondition` implementation. You don't need to deploy AuthorizationTimeRecorderHook separately, use EscrowPeriod directly. ## Logic diff --git a/contracts/conditions/freeze.mdx b/contracts/conditions/freeze.mdx index 5d3b796..b35f469 100644 --- a/contracts/conditions/freeze.mdx +++ b/contracts/conditions/freeze.mdx @@ -1,12 +1,12 @@ --- title: "Freeze" -description: "Block payment release when frozen, with configurable freeze/unfreeze authorization" +description: "Block payment capture when frozen, with configurable freeze/unfreeze authorization" icon: "snowflake" --- ## Overview -Freeze is a standalone condition that blocks release when a payment is frozen. It manages freeze/unfreeze state with configurable authorization and optional duration-based auto-expiry. +Freeze is a standalone condition that blocks capture when a payment is frozen. It manages freeze/unfreeze state with configurable authorization and optional duration-based auto-expiry. **Type:** Per-deployment via [FreezeFactory](/contracts/factories) @@ -19,7 +19,7 @@ Freeze is a standalone condition that blocks release when a payment is frozen. I ## Logic ```solidity -// ICondition, returns false when frozen (blocks release) +// ICondition, returns false when frozen (blocks capture) function check( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, uint256, @@ -51,7 +51,7 @@ const freeze = await freezeFactory.deploy( // Both: capturePreActionCondition = AndCondition([escrowPeriod, freeze]) ``` -Use [AndCondition](/contracts/conditions/combinators) to require both escrow period elapsed **and** not frozen before release. +Use [AndCondition](/contracts/conditions/combinators) to require both escrow period elapsed **and** not frozen before capture. ## Freeze Duration @@ -86,7 +86,7 @@ Freeze duration should balance payer protection with receiver UX. Too long and r - Add time-based release restrictions. + Add time-based capture restrictions. Deploy Freeze via FreezeFactory. diff --git a/contracts/conditions/overview.mdx b/contracts/conditions/overview.mdx index cf798e7..60a9266 100644 --- a/contracts/conditions/overview.mdx +++ b/contracts/conditions/overview.mdx @@ -12,9 +12,9 @@ Conditions are pluggable contracts that control who can perform actions on a Pay |------|----------| | `AUTHORIZE_PRE_ACTION_CONDITION` | Who can authorize payments | | `CHARGE_PRE_ACTION_CONDITION` | Who can charge partial amounts | -| `CAPTURE_PRE_ACTION_CONDITION` | Who can release from escrow | +| `CAPTURE_PRE_ACTION_CONDITION` | Who can capture funds from escrow | | `VOID_PRE_ACTION_CONDITION` | Who can refund during escrow | -| `REFUND_PRE_ACTION_CONDITION` | Who can refund after release | +| `REFUND_PRE_ACTION_CONDITION` | Who can refund after capture | ## ICondition Interface @@ -63,12 +63,12 @@ This means you only need to set conditions for slots you want to restrict. Leave Conditions compose to create flexible authorization policies. Here are common patterns: -### Open Authorization, Restricted Release +### Open Authorization, Restricted Capture ```solidity config = { authorizePreActionCondition: ALWAYS_TRUE_CONDITION, // Anyone can authorize - authorizePostActionHook: escrowRecorder, // Record time + authorizePostActionHook: escrowHook, // Record time capturePreActionCondition: capturePreActionCondition, // Restricted // ... }; @@ -134,7 +134,7 @@ function check(PaymentInfo calldata payment, uint256, address caller) ## Next Steps - + Learn about the state recording system. diff --git a/contracts/conditions/payer.mdx b/contracts/conditions/payer.mdx index 5e09905..7de29c7 100644 --- a/contracts/conditions/payer.mdx +++ b/contracts/conditions/payer.mdx @@ -35,7 +35,7 @@ The condition compares `caller` against `payment.payer`, pure computation with n | `REFUND_PRE_ACTION_CONDITION` | Let payer cancel streams | -Typically paired with [ReceiverCondition](/contracts/conditions/receiver) for release, since payers shouldn't release their own funds in most configurations. +Typically paired with [ReceiverCondition](/contracts/conditions/receiver) for capture, since payers shouldn't capture their own funds in most configurations. ## Gas diff --git a/contracts/conditions/receiver.mdx b/contracts/conditions/receiver.mdx index f24a8cb..8b3d05a 100644 --- a/contracts/conditions/receiver.mdx +++ b/contracts/conditions/receiver.mdx @@ -30,12 +30,12 @@ The condition compares `caller` against `payment.receiver`, pure computation wit | Slot | Use Case | |------|----------| -| `CAPTURE_PRE_ACTION_CONDITION` | Let receiver release funds after escrow | +| `CAPTURE_PRE_ACTION_CONDITION` | Let receiver capture funds after escrow | | `CHARGE_PRE_ACTION_CONDITION` | Let receiver charge partial amounts | | `VOID_PRE_ACTION_CONDITION` | Let receiver voluntarily refund | -For release, ReceiverCondition is often composed with [EscrowPeriod](/contracts/conditions/escrow-period) via [AndCondition](/contracts/conditions/combinators) to ensure the escrow window has passed before the receiver can release. +For capture, ReceiverCondition is often composed with [EscrowPeriod](/contracts/conditions/escrow-period) via [AndCondition](/contracts/conditions/combinators) to ensure the escrow window has passed before the receiver can capture. ## Gas @@ -49,6 +49,6 @@ For release, ReceiverCondition is often composed with [EscrowPeriod](/contracts/ Restrict actions to the payment payer. - Add time-based release restrictions. + Add time-based capture restrictions. diff --git a/contracts/examples.mdx b/contracts/examples.mdx index 58fa188..422ee30 100644 --- a/contracts/examples.mdx +++ b/contracts/examples.mdx @@ -10,7 +10,7 @@ The configuration examples below use simplified pseudo-code (e.g., `new StaticAd ## Example 1: Standard E-Commerce with 7-Day Escrow -**Use Case:** Online marketplace with buyer protection. 7-day escrow period, payer can freeze for 3 days, receiver or arbiter can release after escrow. +**Use Case:** Online marketplace with buyer protection. 7-day escrow period, payer can freeze for 3 days, receiver or arbiter can capture after escrow. ### Complete Configuration @@ -35,7 +35,7 @@ The configuration examples below use simplified pseudo-code (e.g., `new StaticAd ```typescript - // 7-day escrow period (combined recorder + condition) + // 7-day escrow period (combined hook + condition) const escrowPeriod = await escrowPeriodFactory.deploy( 7 * 24 * 60 * 60, // 7 days zeroHash // bytes32(0) = operator-only @@ -49,7 +49,7 @@ The configuration examples below use simplified pseudo-code (e.g., `new StaticAd ``` - + ```typescript // (Receiver OR Arbiter) AND (EscrowPassed AND NotFrozen) const receiverOrArbiter = await new OrCondition([ @@ -115,9 +115,9 @@ sequenceDiagram Note over Buyer,Seller: Day 8 - Freeze Expires Note over Operator: Freeze automatically expires - Note over Buyer,Seller: Day 9 - Release - Seller->>Operator: release(paymentId) - Operator->>Escrow: release funds + Note over Buyer,Seller: Day 9 - Capture + Seller->>Operator: capture(paymentInfo, amount) + Operator->>Escrow: capture funds Escrow->>Seller: 99.95 USDC Escrow->>Operator: 0.05 USDC ``` @@ -141,7 +141,7 @@ const config = { authorizePostActionHook: '0x0000000000000000000000000000000000000000', chargePreActionCondition: RECEIVER_CONDITION, // Only receiver can charge chargePostActionHook: '0x0000000000000000000000000000000000000000', - capturePreActionCondition: RECEIVER_CONDITION, // Fallback to release remaining + capturePreActionCondition: RECEIVER_CONDITION, // Fallback to capture remaining capturePostActionHook: '0x0000000000000000000000000000000000000000', voidPreActionCondition: arbiterCondition.address, voidPostActionHook: '0x0000000000000000000000000000000000000000', @@ -194,7 +194,7 @@ const escrowPeriod = await escrowPeriodFactory.deploy( // Deploy Freeze linked to escrow period const freeze = await freezeFactory.deploy(freezePolicy, escrowPeriod); -// Receiver OR Arbiter can release (after escrow + not frozen) +// Receiver OR Arbiter can capture (after escrow + not frozen) const capturePreActionCondition = await new AndCondition([ await new OrCondition([RECEIVER_CONDITION, arbiterCondition.address]), await new AndCondition([escrowPeriod, freeze]) @@ -253,7 +253,7 @@ sequenceDiagram --- -## Example 4: Service-Based Payments (Milestone Release) +## Example 4: Service-Based Payments (Milestone Capture) **Use Case:** Freelance work with milestone-based releases. Receiver can trigger partial releases. @@ -269,7 +269,7 @@ const escrowPeriod = await escrowPeriodFactory.deploy( zeroHash // bytes32(0) = operator-only ); -// Receiver can release after short escrow +// Receiver can capture after short escrow const capturePreActionCondition = await new AndCondition([ RECEIVER_CONDITION, // Only receiver escrowPeriod // Escrow period passed @@ -319,8 +319,8 @@ sequenceDiagram Note over Escrow: 300 USDC remaining Note over Client,Escrow: Day 15 - Final Milestone - Freelancer->>Operator: release(paymentId) - Operator->>Escrow: release remaining + Freelancer->>Operator: capture(paymentInfo, amount) + Operator->>Escrow: capture remaining Escrow->>Freelancer: 300 USDC Note over Escrow: Payment complete ``` @@ -382,7 +382,7 @@ const escrowPeriod = await escrowPeriodFactory.deploy( zeroHash // bytes32(0) = operator-only ); -// Receiver OR Arbiter can release +// Receiver OR Arbiter can capture const capturePreActionCondition = await new AndCondition([ await new OrCondition([RECEIVER_CONDITION, arbiterCondition.address]), escrowPeriod // Escrow period passed @@ -501,8 +501,8 @@ sequenceDiagram Escrow->>Provider: 100 USDC Note over User,Escrow: Month 12 - Subscription Ends - Provider->>Operator: release(paymentId) - Operator->>Escrow: release remaining + Provider->>Operator: capture(paymentInfo, amount) + Operator->>Escrow: capture remaining Escrow->>Provider: Remaining funds Note over User,Escrow: Alt: If cancelled early @@ -563,9 +563,9 @@ sequenceDiagram Note over DAO: Review and vote DAO->>DAO: Multisig approval - Note over Grantee,Escrow: Release - DAO->>Operator: release(paymentId) - Operator->>Escrow: release funds + Note over Grantee,Escrow: Capture + DAO->>Operator: capture(paymentInfo, amount) + Operator->>Escrow: capture funds Escrow->>Grantee: 50000 USDC Note over Grantee: Grant complete ``` @@ -661,7 +661,7 @@ sequenceDiagram ### Configuration ```typescript -// No arbiter - receiver controls release, platform earns fees +// No arbiter - receiver controls capture, platform earns fees const platformCondition = await new StaticAddressCondition(PLATFORM_ADDRESS); const config = { @@ -701,8 +701,8 @@ sequenceDiagram Note over CompanyA: Receives goods Note over CompanyA,Platform: Day 30 - Settlement - CompanyB->>Operator: release(paymentId) - Operator->>Escrow: release funds + CompanyB->>Operator: capture(paymentInfo, amount) + Operator->>Escrow: capture funds Escrow->>CompanyB: 99900 USDC Escrow->>Platform: 100 USDC Note over CompanyB: Invoice settled @@ -739,7 +739,7 @@ Before deploying, verify: - [ ] Freeze policy suits your use case - [ ] Escrow period is appropriate for delivery time -- [ ] Release condition prevents premature releases +- [ ] Capture condition prevents premature captures - [ ] Refund conditions allow arbiter intervention - [ ] Fee rates are competitive and sustainable - [ ] Protocol fee percentage is reasonable @@ -773,13 +773,13 @@ await walletClient.writeContract({ args: [paymentInfo, parseUnits('100', 6), tokenCollectorAddress, collectorData], }); -// 2. Try to release immediately (should fail if escrow configured) +// 2. Try to capture immediately (should fail if escrow configured) // Expect revert with ConditionNotMet try { await walletClient.writeContract({ address: operatorAddress, abi: paymentOperatorAbi, - functionName: 'release', + functionName: 'capture', args: [paymentInfo, parseUnits('100', 6)], }); } catch (e) { @@ -790,11 +790,11 @@ try { await testClient.increaseTime({ seconds: 7 * 24 * 60 * 60 }); await testClient.mine({ blocks: 1 }); -// 4. Release after escrow +// 4. Capture after escrow await walletClient.writeContract({ address: operatorAddress, abi: paymentOperatorAbi, - functionName: 'release', + functionName: 'capture', args: [paymentInfo, parseUnits('100', 6)], }); @@ -822,7 +822,7 @@ Research competitors' escrow periods and fees: Test your configuration handles: -- Immediate release attempts +- Immediate capture attempts - Freeze during escrow - Freeze expiry - Refunds in both states diff --git a/contracts/factories.mdx b/contracts/factories.mdx index 4eac432..34b6624 100644 --- a/contracts/factories.mdx +++ b/contracts/factories.mdx @@ -142,7 +142,7 @@ const factory = getContract({ const arbiterConditionHash = await staticAddressConditionFactory.write.deploy([arbiterAddress]); const arbiterConditionAddress = /* get from receipt */; -// Deploy release condition: arbiter AND escrow period passed +// Deploy capture condition: arbiter AND escrow period passed const capturePreActionConditionHash = await andConditionFactory.write.deploy([ [arbiterConditionAddress, escrowPeriodAddress] ]); @@ -204,7 +204,7 @@ If you call `deployOperator()` with the same configuration twice, the factory re ## Escrow Period Factory -Deploys `EscrowPeriod` contracts - combined recorder and condition for time-based release logic. +Deploys `EscrowPeriod` contracts - combined hook and condition for time-based capture logic. ### Contract Address @@ -228,18 +228,18 @@ function deploy( ### How It Works The factory deploys a single **EscrowPeriod** contract that: -- Extends `AuthorizationTimeRecorder` (implements `IRecorder`) +- Extends `AuthorizationTimeRecorderHook` (implements `IHook`) - Implements `ICondition` -- Records authorization timestamp when used as recorder +- Records authorization timestamp when used as hook - Checks if escrow period has passed when used as condition **Architecture:** ```mermaid flowchart LR - EP[EscrowPeriod] -->|extends| ATR[AuthorizationTimeRecorder] + EP[EscrowPeriod] -->|extends| ATR[AuthorizationTimeRecorderHook] EP -->|implements| IC[ICondition] - ATR -->|implements| IR[IRecorder] + ATR -->|implements| IR[IHook] ``` @@ -268,7 +268,7 @@ const escrowPeriodAddress = receipt.logs[0].address; console.log("EscrowPeriod:", escrowPeriodAddress); -// Use SAME address for both recorder and condition +// Use SAME address for both hook and condition const config = { authorizePreActionCondition: ALWAYS_TRUE_CONDITION, authorizePostActionHook: escrowPeriodAddress, // Record auth time @@ -293,7 +293,7 @@ const config = { ## Freeze Factory -Deploys `Freeze` condition contracts that block release when a payment is frozen. +Deploys `Freeze` condition contracts that block capture when a payment is frozen. ### Contract Address @@ -335,7 +335,7 @@ const freeze = await freezeFactory.write.deploy([ escrowPeriod // Link to EscrowPeriod (or zeroAddress for unconstrained) ]); -// Step 3: Compose with EscrowPeriod for release condition +// Step 3: Compose with EscrowPeriod for capture condition const capturePreActionCondition = await andConditionFactory.write.deploy([ [escrowPeriod, freeze] ]); @@ -470,7 +470,7 @@ Approximate gas costs for factory deployments (Base Sepolia): | Operation | Gas Cost | USD (at 0.1 gwei, $3000 ETH) | |-----------|----------|------------------------------| | Deploy PaymentOperator | ~2.5M gas | ~$0.75 | -| Deploy EscrowPeriod (condition + recorder) | ~1.8M gas | ~$0.54 | +| Deploy EscrowPeriod (condition + hook) | ~1.8M gas | ~$0.54 | | Deploy Freeze | ~1.0M gas | ~$0.30 | | Predict address (view call) | 0 gas | $0.00 | diff --git a/contracts/fees.mdx b/contracts/fees.mdx index 80af4c3..bb4110e 100644 --- a/contracts/fees.mdx +++ b/contracts/fees.mdx @@ -117,7 +117,7 @@ mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees; **Flow:** 1. `authorize()` calculates fees and stores them in `authorizedFees[hash]` -2. `release()` uses the stored fees, not the current calculator rates +2. `capture()` uses the stored fees, not the current calculator rates 3. Protocol fee timelocks can't break already-authorized payments diff --git a/contracts/gas-costs.mdx b/contracts/gas-costs.mdx index e5412f3..cc2e13f 100644 --- a/contracts/gas-costs.mdx +++ b/contracts/gas-costs.mdx @@ -19,51 +19,51 @@ The buyer never pays gas. They only sign an off-chain ERC-3009 authorization. Al | Role | Operations | Gas | Cost on Base | |------|-----------|-----|-------------| | **Facilitator** | `authorize()` | 181,544 | < $0.005 | -| **Merchant** | `release()` | 150,262 | < $0.005 | -| **Happy path total** | authorize + release | 331,806 | **< $0.01** | +| **Merchant** | `capture()` | 150,262 | < $0.005 | +| **Happy path total** | authorize + capture | 331,806 | **< $0.01** | Disputes are rare and add < $0.005 with off-chain resolution, see [Dispute Path](#dispute-path) below. ## Happy Path -The happy path has **2 on-chain transactions**: `authorize` (at purchase time) and `release` (after the escrow period expires). +The happy path has **2 on-chain transactions**: `authorize` (at purchase time) and `capture` (after the escrow period expires). | Operation | Gas | vs transfer | Who Calls | When | |-----------|-----|------------|-----------|------| | `authorize()` | 181,544 | 17.6x | Facilitator | At purchase (HTTP 402 settlement) | -| `release()` | 150,262 | 14.6x | Anyone | After escrow period expires | +| `capture()` | 150,262 | 14.6x | Anyone | After escrow period expires | The **vs transfer** column shows multiples of a cold ERC-20 `transfer()` (10,305 gas), the absolute floor for moving tokens on-chain. -In production, the merchant typically calls `release()`, but the function has no caller restriction beyond the configured release condition (EscrowPeriod + Freeze). After the escrow period passes and the payment isn't frozen, anyone can trigger it. +In production, the merchant typically calls `capture()`, but the function has no caller restriction beyond the configured capture condition (EscrowPeriod + Freeze). After the escrow period passes and the payment isn't frozen, anyone can trigger it. An escrow authorization is inherently more work than a raw ERC-20 transfer: it validates payment info, checks fee bounds, locks fees, transfers tokens into escrow, and records state. The per-plugin section below shows exactly where the gas goes. -**Facilitators: set a gas limit.** The facilitator pays gas for `authorize()`, but the operator chooses which conditions and recorders are configured. Each plugin slot adds cost, and custom plugins can run arbitrary computation. Simulate the transaction with `eth_estimateGas` before submitting and reject operators whose `authorize()` exceeds a reasonable threshold (e.g., 300,000 gas). The full x402r configuration uses ~181,000, anything significantly above that warrants investigation. +**Facilitators: set a gas limit.** The facilitator pays gas for `authorize()`, but the operator chooses which conditions and hooks are configured. Each plugin slot adds cost, and custom plugins can run arbitrary computation. Simulate the transaction with `eth_estimateGas` before submitting and reject operators whose `authorize()` exceeds a reasonable threshold (e.g., 300,000 gas). The full x402r configuration uses ~181,000, anything significantly above that warrants investigation. ## Per-Plugin Gas Costs -The PaymentOperator is configured with pluggable conditions (checked before an action) and recorders (called after). You choose which plugins to use. Here's the marginal cost of each, measured by diffing adjacent configurations. +The PaymentOperator is configured with pluggable conditions (checked before an action) and hooks (called after). You choose which plugins to use. Here's the marginal cost of each, measured by diffing adjacent configurations. ### authorize() | Configuration | Gas | Marginal Cost | Plugin | |---------------|-----|---------------|--------| | Commerce Payments escrow (no operator) | 78,353 |: | Raw `AuthCaptureEscrow.authorize()`: validates payment, escrows tokens via `PreApprovalPaymentCollector` | -| + PaymentOperator layer | 117,250 | **+38,897** | Operator dispatch, plugin slot checks, access control: all conditions, recorders, and fee calculator set to `address(0)` | +| + PaymentOperator layer | 117,250 | **+38,897** | Operator dispatch, plugin slot checks, access control: all conditions, hooks, and fee calculator set to `address(0)` | | + Fee calculation | 135,961 | **+18,711** | `StaticFeeCalculator`: calculates protocol + operator fees, validates bounds, locks fees in `authorizedFees[hash]` | -| + EscrowPeriod recorder | 162,744 | **+26,783** | `EscrowPeriod.record()`: stores `authorizationTime[hash] = block.timestamp` (cold SSTORE to cross-contract slot) | +| + EscrowPeriod hook | 162,744 | **+26,783** | `EscrowPeriod.record()`: stores `authorizationTime[hash] = block.timestamp` (cold SSTORE to cross-contract slot) | -The EscrowPeriod recorder is the single most expensive plugin on `authorize` because it writes to a new storage slot in the EscrowPeriod contract. +The EscrowPeriod hook is the single most expensive plugin on `authorize` because it writes to a new storage slot in the EscrowPeriod contract. -### release() +### capture() | Configuration | Gas | Marginal Cost | Plugin | |---------------|-----|---------------|--------| | Commerce Payments escrow (no operator) | 66,365 |: | Raw `AuthCaptureEscrow.capture()`: validates authorization, distributes tokens to receiver | -| + PaymentOperator layer | 77,926 | **+11,561** | Operator dispatch, plugin slot checks, access control: all conditions, recorders, and fee calculator set to `address(0)` | +| + PaymentOperator layer | 77,926 | **+11,561** | Operator dispatch, plugin slot checks, access control: all conditions, hooks, and fee calculator set to `address(0)` | | + Fee retrieval | 116,980 | **+39,054** | Reads locked fees from `authorizedFees[hash]`, calculates protocol share, accumulates in `accumulatedProtocolFees[token]` | | + ReceiverCondition | 121,430 | **+4,450** | Pure calldata comparison: `caller == paymentInfo.receiver`: no storage reads | | + EscrowPeriod condition | 122,520 | **+5,540** | Cross-contract SLOAD: reads `authorizationTime[hash]`, compares against `block.timestamp` | @@ -97,14 +97,14 @@ If the parties choose to handle the dispute fully on-chain instead: |-----------|-----|------------|-----------|-------| | `authorize()` | 181,544 | 17.6x | Facilitator | Already paid during happy path | | `freeze()` | 44,651 | 4.3x | Buyer | Locks payment during escrow window | -| `release()` | 150,262 | 14.6x | Anyone | Already paid during happy path | +| `capture()` | 150,262 | 14.6x | Anyone | Already paid during happy path | | `requestRefund()` | 421,689 | 40.9x | Buyer | Creates refund request with multi-index storage | | `submitEvidence()` | 135,597 | 13.2x | Any party | Stores IPFS CID on-chain | | `approveWithSignature()` | 89,935 | 8.7x | Anyone | Relays arbiter's off-chain EIP-712 signature | | `refund()` | 54,467 | 5.3x | Anyone | Pulls funds from merchant wallet via ReceiverRefundCollector | | **Total** | **1,078,145** | **104.6x** | | | -This total includes the happy path steps (`authorize` + `release`) since those have already been paid. The dispute-only overhead is 746,339 gas (< $0.02 on Base). +This total includes the happy path steps (`authorize` + `capture`) since those have already been paid. The dispute-only overhead is 746,339 gas (< $0.02 on Base). `requestRefund()` at 421,689 gas is the most expensive operation because it writes to **multiple storage mappings** for indexing: @@ -142,6 +142,6 @@ All numbers above assume one payment per transaction. Batching multiple operatio How protocol and operator fees are calculated and distributed - How conditions, recorders, and escrow fit together + How conditions, hooks, and escrow fit together diff --git a/contracts/recorders/authorization-time.mdx b/contracts/hooks/authorization-time.mdx similarity index 57% rename from contracts/recorders/authorization-time.mdx rename to contracts/hooks/authorization-time.mdx index bb2b1cf..4bf9e74 100644 --- a/contracts/recorders/authorization-time.mdx +++ b/contracts/hooks/authorization-time.mdx @@ -1,15 +1,15 @@ --- -title: "AuthorizationTimeRecorder" +title: "AuthorizationTimeRecorderHook" description: "Records authorization timestamp for time-based conditions" icon: "clock" --- ## Overview -AuthorizationTimeRecorder stores the `block.timestamp` when a payment is authorized. This timestamp is used by time-based conditions like [EscrowPeriod](/contracts/conditions/escrow-period). +AuthorizationTimeRecorderHook stores the `block.timestamp` when a payment is authorized. This timestamp is used by time-based conditions like [EscrowPeriod](/contracts/conditions/escrow-period). -[EscrowPeriod](/contracts/conditions/escrow-period) **extends** AuthorizationTimeRecorder and adds `ICondition` implementation. For escrow enforcement, use EscrowPeriod directly instead of deploying AuthorizationTimeRecorder separately. +[EscrowPeriod](/contracts/conditions/escrow-period) **extends** AuthorizationTimeRecorderHook and adds `ICondition` implementation. For escrow enforcement, use EscrowPeriod directly instead of deploying AuthorizationTimeRecorderHook separately. ## State @@ -41,7 +41,7 @@ function getAuthorizationTime( ## When to Use -Use AuthorizationTimeRecorder directly only if you need authorization timestamps **without** escrow period enforcement. For most use cases, [EscrowPeriod](/contracts/conditions/escrow-period) is the better choice since it includes this recorder plus time-lock condition logic. +Use AuthorizationTimeRecorderHook directly only if you need authorization timestamps **without** escrow period enforcement. For most use cases, [EscrowPeriod](/contracts/conditions/escrow-period) is the better choice since it includes this hook plus time-lock condition logic. ## Gas @@ -51,9 +51,9 @@ Use AuthorizationTimeRecorder directly only if you need authorization timestamps - Combined recorder + condition for escrow enforcement. + Combined hook + condition for escrow enforcement. - + Index payments for on-chain queries. diff --git a/contracts/hooks/combinator.mdx b/contracts/hooks/combinator.mdx new file mode 100644 index 0000000..befd6fd --- /dev/null +++ b/contracts/hooks/combinator.mdx @@ -0,0 +1,50 @@ +--- +title: "HookCombinator" +description: "Chain multiple hooks into a single operator slot for composite state tracking" +icon: "layer-group" +--- + +## Overview + +HookCombinator chains multiple hooks into one, calling each in sequence. Since each operator slot accepts only one hook address, use HookCombinator when you need multiple hooks for the same action. + +## Deployment + +Deploy via HookCombinatorFactory: + +```typescript +const comboAddress = await hookCombinatorFactory.write.deploy([ + [escrowPeriodAddress, paymentIndexRecorderHookAddress] // Records auth time + payment index +]); + +config.authorizeHook = comboAddress; +``` + +## Behavior + +- Hooks are called in the order provided +- **If any hook reverts, all revert**: the entire recording is atomic +- Each hook receives the same `paymentInfo`, `amount`, and `caller` parameters + +## Limits + + +**Max 10 hooks per combinator.** Each additional hook adds ~1k gas overhead for the delegation call. + + +## Gas + +**Cost:** Sum of all individual hook costs + ~1k gas overhead per hook for delegation. + +Example: EscrowPeriod (~20k) + PaymentIndexRecorderHook (~20k) + ~2k overhead = ~42k gas total. + +## Next Steps + + + + Record authorization timestamps. + + + Index payments for on-chain queries. + + diff --git a/contracts/recorders/custom.mdx b/contracts/hooks/custom.mdx similarity index 57% rename from contracts/recorders/custom.mdx rename to contracts/hooks/custom.mdx index 7ae30cf..2d0d016 100644 --- a/contracts/recorders/custom.mdx +++ b/contracts/hooks/custom.mdx @@ -1,17 +1,17 @@ --- -title: "Custom Recorders" -description: "Build your own recorder contracts for specialized state tracking" +title: "Custom Hooks" +description: "Build your own hook contracts for specialized state tracking" icon: "wrench" --- ## Overview -You can create custom recorders for specialized tracking beyond what the built-in recorders provide. Implement the `IRecorder` interface and extend `BaseRecorder` for operator access control. +You can create custom hooks for specialized tracking beyond what the built-in hooks provide. Implement the `IHook` interface and extend `BaseHook` for operator access control. -## IRecorder Interface +## IHook Interface ```solidity -interface IRecorder { +interface IHook { function record( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, uint256 amount, @@ -20,13 +20,13 @@ interface IRecorder { } ``` -## Extending BaseRecorder +## Extending BaseHook -Extend `BaseRecorder` to ensure only authorized operators can call `record()`: +Extend `BaseHook` to ensure only authorized operators can call `record()`: ```solidity -contract MyRecorder is BaseRecorder { - constructor(address _escrow) BaseRecorder(_escrow) {} +contract MyHook is BaseHook { + constructor(address _escrow) BaseHook(_escrow) {} function record( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, @@ -38,16 +38,16 @@ contract MyRecorder is BaseRecorder { } ``` -## Example: ReleaseCountRecorder +## Example: ReleaseCountHook Tracks the number and total amount of releases per payment: ```solidity -contract ReleaseCountRecorder is BaseRecorder { +contract ReleaseCountHook is BaseHook { mapping(bytes32 => uint256) public releaseCount; mapping(bytes32 => uint256) public totalReleased; - constructor(address _escrow) BaseRecorder(_escrow) {} + constructor(address _escrow) BaseHook(_escrow) {} function record( PaymentInfo calldata payment, @@ -71,29 +71,29 @@ contract ReleaseCountRecorder is BaseRecorder { ## Testing -Test custom recorders with Forge: +Test custom hooks with Forge: ```solidity -contract ReleaseCountRecorderTest is Test { - ReleaseCountRecorder recorder; +contract ReleaseCountHookTest is Test { + ReleaseCountHook hook; function setUp() public { - recorder = new ReleaseCountRecorder(address(escrow)); + hook = new ReleaseCountHook(address(escrow)); } function test_incrementsOnRecord() public { - recorder.record(paymentInfo, 100e6, caller); + hook.record(paymentInfo, 100e6, caller); bytes32 hash = escrow.getHash(paymentInfo); - (uint256 count, uint256 total) = recorder.getStats(hash); + (uint256 count, uint256 total) = hook.getStats(hash); assertEq(count, 1); assertEq(total, 100e6); } function test_tracksMultipleReleases() public { - recorder.record(paymentInfo, 50e6, caller); - recorder.record(paymentInfo, 30e6, caller); + hook.record(paymentInfo, 50e6, caller); + hook.record(paymentInfo, 30e6, caller); bytes32 hash = escrow.getHash(paymentInfo); - (uint256 count, uint256 total) = recorder.getStats(hash); + (uint256 count, uint256 total) = hook.getStats(hash); assertEq(count, 2); assertEq(total, 80e6); } @@ -102,19 +102,19 @@ contract ReleaseCountRecorderTest is Test { ## Security Checklist -- [ ] Extends `BaseRecorder` for operator access control +- [ ] Extends `BaseHook` for operator access control - [ ] Handles edge cases (zero amounts, duplicate records) - [ ] Gas-efficient storage layout - [ ] Comprehensive test coverage -Unlike conditions, recorders **do modify state**. Ensure proper access control via `BaseRecorder` to prevent unauthorized writes. +Unlike conditions, hooks **do modify state**. Ensure proper access control via `BaseHook` to prevent unauthorized writes. ## Next Steps - + Compare recording strategies. diff --git a/contracts/hooks/overview.mdx b/contracts/hooks/overview.mdx new file mode 100644 index 0000000..7426cc7 --- /dev/null +++ b/contracts/hooks/overview.mdx @@ -0,0 +1,101 @@ +--- +title: "Hooks Overview" +description: "State recording system for tracking payment lifecycle events" +icon: "database" +--- + +## What Are Hooks? + +Hooks are pluggable contracts that update state **after** an action successfully executes on a PaymentOperator. Each operator has **5 hook slots**: one per action: + +| Slot | Records After | +|------|---------------| +| `AUTHORIZE_POST_ACTION_HOOK` | Authorization (e.g., timestamp) | +| `CHARGE_POST_ACTION_HOOK` | Charge event | +| `CAPTURE_POST_ACTION_HOOK` | Capture from escrow | +| `VOID_POST_ACTION_HOOK` | Void | +| `REFUND_POST_ACTION_HOOK` | Refund (after capture) | + +## IHook Interface + +```solidity +interface IHook { + function record( + AuthCaptureEscrow.PaymentInfo calldata paymentInfo, + uint256 amount, + address caller + ) external; +} +``` + +**Parameters:** +- `paymentInfo`, The payment information struct +- `amount`, The amount involved in the action +- `caller`, The address that executed the action (msg.sender on operator) + +## Default Behavior + +**Hook slot = `address(0)`**: no-op (does nothing). No state is recorded. + +This means you only need to set hooks for slots where you want state tracking. Leave the rest as `address(0)`. + +## BaseHook + +All built-in hooks extend `BaseHook`, which verifies that the caller is an authorized operator. This prevents unauthorized contracts from writing state. + +## Choosing a Recording Strategy + +Not every payment needs on-chain hooks. Choose based on your use case: + +### Events Only (~0 Extra Gas) + +The operator already emits events (`AuthorizeExecuted`, `CaptureExecuted`, etc.) for every action. If you only need payment history for analytics or display, skip hooks entirely and index events off-chain. + +**Best for:** Micropayments, high-volume payments where gas overhead matters, simple UIs. + +### Events + Subgraph (Best Queries) + +Index operator events with a subgraph for rich queries (payment history by payer, receiver, status, date range). No on-chain hook gas cost. + +**Best for:** Analytics dashboards, payment history, multi-payment queries. + +**Trade-off:** Requires subgraph infrastructure (semi-centralized). + +### On-Chain Hooks (~20k Gas per Write) + +Use hooks when you need **on-chain reads**: other contracts or conditions that depend on recorded state. [EscrowPeriod](/contracts/conditions/escrow-period) is the most common example: it records authorization time so the capture condition can check if the escrow window has passed. + +**Best for:** Escrow enforcement, dispute evidence, decentralized frontends, on-chain composability. + +**Trade-off:** ~20k gas per `SSTORE` operation. + +### Decision Table + +| Need | Strategy | Hook Slots | +|------|----------|---------------| +| Payment history for UI | Events only | `address(0)` | +| Rich queries, analytics | Events + Subgraph | `address(0)` | +| Time-locked releases | On-chain | [EscrowPeriod](/contracts/conditions/escrow-period) on `AUTHORIZE_POST_ACTION_HOOK` | +| On-chain payment index | On-chain | [PaymentIndexRecorderHook](/contracts/hooks/payment-index) | +| Multiple data points | On-chain | [HookCombinator](/contracts/hooks/combinator) | + + +For most configurations, you only need a hook on the `AUTHORIZE_POST_ACTION_HOOK` slot (for [EscrowPeriod](/contracts/conditions/escrow-period)). Leave other hook slots as `address(0)`. + + +## Next Steps + + + + Record authorization timestamps. + + + Index payments for on-chain queries. + + + Chain multiple hooks into one slot. + + + Build your own hook. + + diff --git a/contracts/recorders/payment-index.mdx b/contracts/hooks/payment-index.mdx similarity index 72% rename from contracts/recorders/payment-index.mdx rename to contracts/hooks/payment-index.mdx index 6931645..8bf53e6 100644 --- a/contracts/recorders/payment-index.mdx +++ b/contracts/hooks/payment-index.mdx @@ -1,12 +1,12 @@ --- -title: "PaymentIndexRecorder" +title: "PaymentIndexRecorderHook" description: "Index payments by sequential count for on-chain queries and multiple refund requests" icon: "list-ol" --- ## Overview -PaymentIndexRecorder records a sequential index for each payment action, enabling multiple refund requests per payment. Each time `record()` is called, the index increments by 1. +PaymentIndexRecorderHook records a sequential index for each payment action, enabling multiple refund requests per payment. Each time `record()` is called, the index increments by 1. ## When to Use @@ -14,7 +14,7 @@ PaymentIndexRecorder records a sequential index for each payment action, enablin - You need to support **multiple refund requests** per payment (each keyed by nonce) - Other contracts need to read the payment index on-chain -**Skip when:** You're using a subgraph for payment queries — the subgraph can derive indexes from events without the on-chain gas cost. +**Skip when:** You're using a subgraph for payment queries, the subgraph can derive indexes from events without the on-chain gas cost. ## State @@ -63,10 +63,10 @@ await refundRequest.requestRefund(paymentInfo, amount2, 1); // nonce 1 ## Next Steps - - Combine with other recorders in a single slot. + + Combine with other hooks in a single slot. - + Compare recording strategies. diff --git a/contracts/periphery/refund-request.mdx b/contracts/hooks/refund-request.mdx similarity index 100% rename from contracts/periphery/refund-request.mdx rename to contracts/hooks/refund-request.mdx diff --git a/contracts/overview.mdx b/contracts/overview.mdx index 0cfd604..f54c090 100644 --- a/contracts/overview.mdx +++ b/contracts/overview.mdx @@ -22,7 +22,7 @@ Core escrow contract for holding ERC-20 tokens during payments. **Features:** - Authorization-based deposits (no direct transfers) -- Payment state machine: `NonExistent` → `InEscrow` → `Released` → `Settled` +- Payment state machine: `NonExistent` → `InEscrow` → `Captured` → `Settled` - Void/reclaim for failed authorizations - CaptureAuthorizer-based access control @@ -30,7 +30,7 @@ Core escrow contract for holding ERC-20 tokens during payments. ```solidity authorize(paymentId, payer, receiver, amount, token, operator) charge(paymentId, amount) -release(paymentId) +capture(paymentInfo, amount) void(paymentId) reclaim(paymentId, receiver, amount) ``` @@ -69,18 +69,18 @@ x402r extends commerce-payments with flexible payment capabilities: - Configurable authorization via conditions (not hardcoded roles) - Refund request states: `Pending` → `Approved`/`Denied`/`Cancelled` - Voids (during escrow period) -- Refunds (after capture) (after release) +- Refunds (after capture) - Support for marketplace, subscription, streaming, and custom flows ### 2. Pluggable Condition System **Conditions (ICondition)** - Authorization checks before actions -**Recorders (IRecorder)** - State updates after actions +**Hooks (IHook)** - State updates after actions **10-slot configuration per operator:** -- 5 condition slots (before action): authorize, charge, release, voidPayment, refund -- 5 recorder slots (after action): state tracking for each action +- 5 condition slots (before action): authorize, charge, capture, void, refund +- 5 hook slots (after action): state tracking for each action **Benefits:** - Configure operator behavior without redeploying @@ -90,9 +90,9 @@ x402r extends commerce-payments with flexible payment capabilities: ### 3. Time-Based Escrow & Freeze Policies -**EscrowPeriod** - Combined recorder and condition that tracks authorization time and enforces escrow period +**EscrowPeriod** - Combined hook and condition that tracks authorization time and enforces escrow period -**Freeze** - Standalone condition that blocks release when payment is frozen (with configurable freeze/unfreeze authorization) +**Freeze** - Standalone condition that blocks capture when payment is frozen (with configurable freeze/unfreeze authorization) **Key features:** - Configurable escrow periods (e.g., 7 days, 14 days) @@ -113,7 +113,7 @@ x402r extends commerce-payments with flexible payment capabilities: **FreezeFactory** - Deploys Freeze condition contracts -Plus factories for: StaticFeeCalculator, StaticAddressCondition, AndCondition, OrCondition, NotCondition, RecorderCombinator. +Plus factories for: StaticFeeCalculator, StaticAddressCondition, AndCondition, OrCondition, NotCondition, HookCombinator. All factories use **universal CREATE2 addresses**: same address on every supported chain. @@ -129,7 +129,7 @@ All factories use **universal CREATE2 addresses**: same address on every support | Feature | Commerce Payments | x402r | |---------|------------------|-------| | **Refunds** | Manual void/reclaim | Structured refund requests with configurable approval | -| **Escrow Period** | Not enforced | Configurable time-lock before release | +| **Escrow Period** | Not enforced | Configurable time-lock before capture | | **Dispute Resolution** | Not built-in | Arbiter workflow via conditions, signatures, and evidence | | **Authorization** | Operator-based only | Pluggable conditions (access, time, signature, combinators) | | **Freeze Mechanism** | Not available | Configurable freeze during escrow period | @@ -168,7 +168,7 @@ Protocol fee configuration is mutable via `ProtocolFeeConfig` (with 7-day timelo - Condition singletons deployed once, reused everywhere - CREATE2 for deterministic addresses (no registry lookups) - Minimal storage in operators (conditions are stateless) -- Recorder pattern separates state from logic +- Hook pattern separates state from logic diff --git a/contracts/payment-operator.mdx b/contracts/payment-operator.mdx index e570f0d..396435b 100644 --- a/contracts/payment-operator.mdx +++ b/contracts/payment-operator.mdx @@ -11,7 +11,7 @@ The main payment operator contract with pluggable conditions for flexible author - **Type:** Operator instance (one per fee recipient + configuration) - **Deployment:** Via PaymentOperatorFactory - **Immutability:** Cannot be paused or upgraded -- **Configuration:** 10 slots for conditions and recorders +- **Configuration:** 10 slots for conditions and hooks - **Use Cases:** Marketplace, subscriptions, streaming, grants, custom flows ## Immutable Fields @@ -38,17 +38,17 @@ mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees; 1. **AUTHORIZE_PRE_ACTION_CONDITION** - Who can authorize payments 2. **CHARGE_PRE_ACTION_CONDITION** - Who can charge partial amounts -3. **CAPTURE_PRE_ACTION_CONDITION** - Who can release from escrow +3. **CAPTURE_PRE_ACTION_CONDITION** - Who can capture funds from escrow 4. **VOID_PRE_ACTION_CONDITION** - Who can refund during escrow -5. **REFUND_PRE_ACTION_CONDITION** - Who can refund after release +5. **REFUND_PRE_ACTION_CONDITION** - Who can refund after capture **Default:** `address(0)` = always allow - + 1. **AUTHORIZE_POST_ACTION_HOOK** - Record authorization (e.g., timestamp) 2. **CHARGE_POST_ACTION_HOOK** - Record charge event -3. **CAPTURE_POST_ACTION_HOOK** - Record release +3. **CAPTURE_POST_ACTION_HOOK** - Record capture 4. **VOID_POST_ACTION_HOOK** - Record void 5. **REFUND_POST_ACTION_HOOK** - Record refund @@ -136,7 +136,7 @@ function capture( **Parameters:** - `paymentInfo` - Payment info struct -- `amount` - Amount to release +- `amount` - Amount to capture **Flow:** 1. Check `CAPTURE_PRE_ACTION_CONDITION` (if set) diff --git a/contracts/periphery/overview.mdx b/contracts/periphery/overview.mdx index 527139d..074373e 100644 --- a/contracts/periphery/overview.mdx +++ b/contracts/periphery/overview.mdx @@ -12,10 +12,13 @@ Periphery contracts support the [PaymentOperator](/contracts/payment-operator) b | Contract | Role | Type | |----------|------|------| -| [Commerce Payments](/contracts/periphery/auth-capture-escrow) | AuthCaptureEscrow + ERC3009PaymentCollector (base layer) | Singleton | -| [RefundRequest](/contracts/periphery/refund-request) | Tracks refund request lifecycle and approvals | Singleton | -| [RefundRequestEvidence](/contracts/periphery/refund-request-evidence) | On-chain evidence submission for disputes | Singleton | -| [ReceiverRefundCollector](/contracts/periphery/receiver-refund-collector) | Pulls funds from receiver for refunds | Singleton | +| [Commerce Payments](/contracts/periphery/auth-capture-escrow) | AuthCaptureEscrow + ERC3009PaymentCollector + Permit2PaymentCollector (base layer) | Singleton | +| [RefundRequestEvidence](/contracts/periphery/refund-request-evidence) | On-chain evidence submission tied to RefundRequest | Singleton | +| [ReceiverRefundCollector](/contracts/periphery/receiver-refund-collector) | Pulls funds from receiver for refunds (after capture) | Singleton | + + +**RefundRequest** is a hook plugin, see [Hooks: RefundRequest](/contracts/hooks/refund-request). + ## Contract Addresses @@ -41,7 +44,7 @@ All periphery contracts use **universal CREATE2 addresses**: the same address on | AndConditionFactory | `0x2B07d750C639b65a26e43F1FDCE404b21DCf16D9` | | OrConditionFactory | `0x0519a37c0A996DD5F1e81e07b4aD3B24C257BC90` | | NotConditionFactory | `0xb9c3223D059C3cAbD482bB54f3d7cD52DE70A9ae` | -| RecorderCombinatorFactory | `0x30B5373FD791D2d7b28C3B8020EB68b032f3f960` | +| HookCombinatorFactory | `0x30B5373FD791D2d7b28C3B8020EB68b032f3f960` | ### Condition Singletons @@ -61,7 +64,7 @@ All addresses are available programmatically via `@x402r/core`'s `getChainConfig AuthCaptureEscrow and ERC3009PaymentCollector. - + Refund request lifecycle and approvals. diff --git a/contracts/periphery/receiver-refund-collector.mdx b/contracts/periphery/receiver-refund-collector.mdx index b6b8988..38aa5c7 100644 --- a/contracts/periphery/receiver-refund-collector.mdx +++ b/contracts/periphery/receiver-refund-collector.mdx @@ -7,7 +7,7 @@ icon: "arrow-rotate-left" ## Overview - **Type:** Singleton (one per network) -- **Purpose:** Collect tokens from the receiver to refund the payer after escrow release +- **Purpose:** Collect tokens from the receiver to refund the payer after capture - **Address:** `0x88C9826dFA17Ad9d3a726015C667dD995394D341` (all chains) ## Features @@ -18,7 +18,7 @@ icon: "arrow-rotate-left" ## How It Works -After funds have been released to the receiver (state: `Released`), refunds require pulling tokens back from the receiver's wallet. The `ReceiverRefundCollector` handles this by: +After funds have been released to the receiver (state: `Captured`), refunds require pulling tokens back from the receiver's wallet. The `ReceiverRefundCollector` handles this by: 1. Operator calls `refund(paymentInfo, amount, receiverRefundCollector, collectorData)` 2. The collector transfers tokens from the receiver to the escrow contract diff --git a/contracts/recorders/combinator.mdx b/contracts/recorders/combinator.mdx deleted file mode 100644 index 690f8b3..0000000 --- a/contracts/recorders/combinator.mdx +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: "RecorderCombinator" -description: "Chain multiple recorders into a single operator slot for composite state tracking" -icon: "layer-group" ---- - -## Overview - -RecorderCombinator chains multiple recorders into one, calling each in sequence. Since each operator slot accepts only one recorder address, use RecorderCombinator when you need multiple recorders for the same action. - -## Deployment - -Deploy via RecorderCombinatorFactory: - -```typescript -const comboAddress = await recorderCombinatorFactory.write.deploy([ - [escrowPeriodAddress, paymentIndexRecorderHookAddress] // Records auth time + payment index -]); - -config.authorizeHook = comboAddress; -``` - -## Behavior - -- Recorders are called in the order provided -- **If any recorder reverts, all revert**: the entire recording is atomic -- Each recorder receives the same `paymentInfo`, `amount`, and `caller` parameters - -## Limits - - -**Max 10 recorders per combinator.** Each additional recorder adds ~1k gas overhead for the delegation call. - - -## Gas - -**Cost:** Sum of all individual recorder costs + ~1k gas overhead per recorder for delegation. - -Example: EscrowPeriod (~20k) + PaymentIndexRecorder (~20k) + ~2k overhead = ~42k gas total. - -## Next Steps - - - - Record authorization timestamps. - - - Index payments for on-chain queries. - - diff --git a/contracts/recorders/overview.mdx b/contracts/recorders/overview.mdx deleted file mode 100644 index 8456209..0000000 --- a/contracts/recorders/overview.mdx +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: "Recorders Overview" -description: "State recording system for tracking payment lifecycle events" -icon: "database" ---- - -## What Are Recorders? - -Recorders are pluggable contracts that update state **after** an action successfully executes on a PaymentOperator. Each operator has **5 recorder slots**: one per action: - -| Slot | Records After | -|------|---------------| -| `AUTHORIZE_POST_ACTION_HOOK` | Authorization (e.g., timestamp) | -| `CHARGE_POST_ACTION_HOOK` | Charge event | -| `CAPTURE_POST_ACTION_HOOK` | Release from escrow | -| `VOID_POST_ACTION_HOOK` | Void | -| `REFUND_POST_ACTION_HOOK` | Refund (after capture) | - -## IRecorder Interface - -```solidity -interface IRecorder { - function record( - AuthCaptureEscrow.PaymentInfo calldata paymentInfo, - uint256 amount, - address caller - ) external; -} -``` - -**Parameters:** -- `paymentInfo`, The payment information struct -- `amount`, The amount involved in the action -- `caller`, The address that executed the action (msg.sender on operator) - -## Default Behavior - -**Recorder slot = `address(0)`**: no-op (does nothing). No state is recorded. - -This means you only need to set recorders for slots where you want state tracking. Leave the rest as `address(0)`. - -## BaseRecorder - -All built-in recorders extend `BaseRecorder`, which verifies that the caller is an authorized operator. This prevents unauthorized contracts from writing state. - -## Choosing a Recording Strategy - -Not every payment needs on-chain recorders. Choose based on your use case: - -### Events Only (~0 Extra Gas) - -The operator already emits events (`AuthorizeExecuted`, `CaptureExecuted`, etc.) for every action. If you only need payment history for analytics or display, skip recorders entirely and index events off-chain. - -**Best for:** Micropayments, high-volume payments where gas overhead matters, simple UIs. - -### Events + Subgraph (Best Queries) - -Index operator events with a subgraph for rich queries (payment history by payer, receiver, status, date range). No on-chain recorder gas cost. - -**Best for:** Analytics dashboards, payment history, multi-payment queries. - -**Trade-off:** Requires subgraph infrastructure (semi-centralized). - -### On-Chain Recorders (~20k Gas per Write) - -Use recorders when you need **on-chain reads**: other contracts or conditions that depend on recorded state. [EscrowPeriod](/contracts/conditions/escrow-period) is the most common example: it records authorization time so the release condition can check if the escrow window has passed. - -**Best for:** Escrow enforcement, dispute evidence, decentralized frontends, on-chain composability. - -**Trade-off:** ~20k gas per `SSTORE` operation. - -### Decision Table - -| Need | Strategy | Recorder Slots | -|------|----------|---------------| -| Payment history for UI | Events only | `address(0)` | -| Rich queries, analytics | Events + Subgraph | `address(0)` | -| Time-locked releases | On-chain | [EscrowPeriod](/contracts/conditions/escrow-period) on `AUTHORIZE_POST_ACTION_HOOK` | -| On-chain payment index | On-chain | [PaymentIndexRecorder](/contracts/recorders/payment-index) | -| Multiple data points | On-chain | [RecorderCombinator](/contracts/recorders/combinator) | - - -For most configurations, you only need a recorder on the `AUTHORIZE_POST_ACTION_HOOK` slot (for [EscrowPeriod](/contracts/conditions/escrow-period)). Leave other recorder slots as `address(0)`. - - -## Next Steps - - - - Record authorization timestamps. - - - Index payments for on-chain queries. - - - Chain multiple recorders into one slot. - - - Build your own recorder. - - diff --git a/docs.json b/docs.json index 38aa139..ad21f33 100644 --- a/docs.json +++ b/docs.json @@ -57,7 +57,6 @@ "group": "Periphery", "pages": [ "contracts/periphery/overview", - "contracts/periphery/refund-request", "contracts/periphery/refund-request-evidence", "contracts/periphery/receiver-refund-collector" ] @@ -77,13 +76,14 @@ ] }, { - "group": "Recorders", + "group": "Hooks", "pages": [ - "contracts/recorders/overview", - "contracts/recorders/authorization-time", - "contracts/recorders/payment-index", - "contracts/recorders/combinator", - "contracts/recorders/custom" + "contracts/hooks/overview", + "contracts/hooks/authorization-time", + "contracts/hooks/payment-index", + "contracts/hooks/combinator", + "contracts/hooks/refund-request", + "contracts/hooks/custom" ] } ] @@ -95,16 +95,62 @@ "group": "Getting Started", "pages": [ "sdk/overview", + "sdk/create-client", "sdk/deploy-operator" ] }, { - "group": "Marketplace", + "group": "Concepts", + "pages": [ + "sdk/concepts", + "sdk/limitations" + ] + }, + { + "group": "Merchant", "pages": [ + "sdk/merchant", "sdk/merchant/getting-started", - "sdk/helpers/forward-to-arbiter", "sdk/merchant/quickstart", - "sdk/merchant/refund-handling" + "sdk/merchant/payment-operations", + "sdk/merchant/refund-handling", + "sdk/merchant/subscriptions" + ] + }, + { + "group": "Payer (Client)", + "pages": [ + "sdk/payer", + "sdk/client/quickstart", + "sdk/client/escrow-management", + "sdk/client/payment-queries", + "sdk/client/refund-operations", + "sdk/client/subscriptions" + ] + }, + { + "group": "Arbiter", + "pages": [ + "sdk/arbiter", + "sdk/arbiter/quickstart", + "sdk/arbiter/decision-submission", + "sdk/arbiter/registry", + "sdk/arbiter/ai-integration", + "sdk/arbiter/batch-operations", + "sdk/arbiter/subscriptions" + ] + }, + { + "group": "Facilitator", + "pages": [ + "sdk/facilitator/getting-started" + ] + }, + { + "group": "Helpers", + "pages": [ + "sdk/helpers/forward-to-arbiter", + "sdk/helpers/erc8004" ] }, { @@ -119,9 +165,16 @@ "group": "Reference", "pages": [ "sdk/cli", - "sdk/create-client", "sdk/examples" ] + }, + { + "group": "AI Tools", + "pages": [ + "ai-tools/cursor", + "ai-tools/claude-code", + "ai-tools/windsurf" + ] } ] } @@ -172,6 +225,14 @@ { "source": "/x402-integration/escrow-scheme", "destination": "/x402-integration/auth-capture-scheme" + }, + { + "source": "/contracts/periphery/refund-request", + "destination": "/contracts/hooks/refund-request" + }, + { + "source": "/contracts/recorders/:slug", + "destination": "/contracts/hooks/:slug" } ] } diff --git a/index.mdx b/index.mdx index 6bdfb80..8083b09 100644 --- a/index.mdx +++ b/index.mdx @@ -27,7 +27,7 @@ sequenceDiagram Escrow-->>Merchant: Payment notification alt Happy path - Merchant->>Escrow: Release funds + Merchant->>Escrow: Capture funds Escrow->>Merchant: Transfer else Refund requested Client->>Escrow: Request refund @@ -45,7 +45,7 @@ sequenceDiagram Request refunds, freeze suspicious payments, and track payment state. - Release funds, process refunds, and manage escrow periods. + Capture funds, process refunds, and manage escrow periods. Resolve disputes and approve/deny refund requests. @@ -77,7 +77,7 @@ x402r consists of these core components: |-----------|---------| | **PaymentOperator** | Manages payment authorization, capture, charge, void, and refunds with pluggable conditions | | **AuthCaptureEscrow** | Holds ERC-20 tokens during the payment lifecycle (from commerce-payments) | -| **Conditions & Recorders** | Pluggable authorization checks (before action) and state updates (after action) | +| **Conditions & Hooks** | Pluggable authorization checks (before action) and state updates (after action) | | **EscrowPeriod & Freeze** | Time-based capture and freeze policies for buyer protection | | **RefundRequest** | Handles refund request lifecycle and approvals | diff --git a/sdk/arbiter/decision-submission.mdx b/sdk/arbiter/decision-submission.mdx index 2d68776..58a299b 100644 --- a/sdk/arbiter/decision-submission.mdx +++ b/sdk/arbiter/decision-submission.mdx @@ -4,7 +4,7 @@ description: "Submit decisions on refund requests and execute refunds as an arbi icon: "gavel" --- -The arbiter client provides methods for reviewing refund requests, making decisions, and executing refunds through the `refund`, `payment`, and `evidence` action groups. +As an arbiter, you review pending refund requests, then either execute a refund via `payment.voidPayment` / `payment.refund` (which auto-approves the request), or terminally `refund.deny` / `refund.refuse` it. Evidence reads sit on `evidence.*`. ## Approve a refund (voidPayment) @@ -204,7 +204,7 @@ flowchart TD How arbiters are discovered by merchants and clients. - + RefundRequest contract and state machine details. diff --git a/sdk/arbiter/quickstart.mdx b/sdk/arbiter/quickstart.mdx index 289596b..5a431e2 100644 --- a/sdk/arbiter/quickstart.mdx +++ b/sdk/arbiter/quickstart.mdx @@ -35,7 +35,7 @@ const arbiter = createArbiterClient({ The arbiter client provides these action groups: -- **`payment`**: `release`, `voidPayment`, `refund`, `getAmounts`, `getState` and more +- **`payment`**: `capture`, `voidPayment`, `refund`, `getAmounts`, `getState` and more - **`refund`**: `get`, `getStatus`, `has`, `deny`, `refuse`, `getOperatorRequests` and more - **`evidence`**: `submit`, `get`, `getBatch`, `count` - **`escrow`**: `isDuringEscrow`, `getAuthorizationTime`, `getDuration` diff --git a/sdk/cli.mdx b/sdk/cli.mdx index ac0e6be..3138160 100644 --- a/sdk/cli.mdx +++ b/sdk/cli.mdx @@ -53,6 +53,10 @@ Environment variable names are unprefixed to match Foundry, Hardhat, and x402-re | `--max-amount ` | Refuse to pay more than `n` atomic token units. Exits with code 3 if the price exceeds this. | | `--json` | Emit a single JSON envelope to stdout instead of plain text. | +### Supported chains + +The CLI auto-detects the chain from the 402 response's `accepts[].network` field. Any EVM chain known to `viem/chains` is supported (Base, Base Sepolia, Ethereum, Arbitrum, Optimism, and others). For unknown chain IDs, pass `--rpc ` to provide an RPC endpoint. + ### Exit codes | Code | Meaning | @@ -185,10 +189,6 @@ The `@x402r/cli` package exports: | `SettlementError` | class | Exit code 5 | | `SignerResolutionError` | class | Exit code 6 | -### Supported chains - -The CLI auto-detects the chain from the 402 response's `accepts[].network` field. Any EVM chain known to `viem/chains` is supported (Base, Base Sepolia, Ethereum, Arbitrum, Optimism, and others). For unknown chain IDs, pass `--rpc ` to provide an RPC endpoint. - ## Next steps diff --git a/sdk/client/escrow-management.mdx b/sdk/client/escrow-management.mdx index c5da9a7..5e7204f 100644 --- a/sdk/client/escrow-management.mdx +++ b/sdk/client/escrow-management.mdx @@ -4,7 +4,7 @@ description: "Manage escrow periods and freeze payments with the payer client" icon: "lock" --- -The payer client provides methods to interact with the escrow system through the `freeze` and `escrow` action groups. These let you freeze payments during disputes and query escrow period timing. +Freeze payments during disputes and query escrow-period timing from the payer client. Methods sit on the `freeze` and `escrow` action groups. ## Freeze operations @@ -89,7 +89,7 @@ console.log('Escrow period:', duration, 'seconds') ## Understanding escrow timing -| Condition | Escrow Timer | Can Request Refund | Can Release | +| Condition | Escrow Timer | Can Request Refund | Can Capture | |-----------|--------------|-------------------|-------------| | Normal (unfrozen, period active) | Running | Yes | Partial only | | Frozen | Paused | Yes | No | @@ -101,7 +101,7 @@ The escrow period length is configured at the contract level when the `EscrowPer ## Example: freeze and request refund -A common pattern is to freeze a payment before submitting a refund request, ensuring the merchant cannot release funds while the request is pending. +A common pattern is to freeze a payment before submitting a refund request, ensuring the merchant cannot capture funds while the request is pending. ```typescript import { createPayerClient } from '@x402r/sdk' diff --git a/sdk/client/payment-queries.mdx b/sdk/client/payment-queries.mdx index 0e2c1c1..416df87 100644 --- a/sdk/client/payment-queries.mdx +++ b/sdk/client/payment-queries.mdx @@ -4,7 +4,7 @@ description: "Query payment states, amounts, and history with the payer client" icon: "magnifying-glass" --- -The payer client provides methods for querying payment state and history across the `payment`, `query`, and `operator` action groups. +Query lifecycle state, history, and operator config from the payer client. Methods sit on the `payment`, `query`, and `operator` action groups. ## payment.getState @@ -47,7 +47,7 @@ for (const payment of payments ?? []) { ``` -The `query` group uses a tiered resolver: it checks the in-memory store first, then the on-chain recorder, then falls back to event log scanning. Pass `eventFromBlock` in the client config to limit the scan range. +The `query` group uses a tiered resolver: it checks the in-memory store first, then the on-chain hook, then falls back to event log scanning. Pass `eventFromBlock` in the client config to limit the scan range. ## query.getReceiverPayments @@ -73,7 +73,7 @@ Retrieve all slot addresses from the PaymentOperator contract. ```typescript const config = await client.operator.getConfig() -console.log('Release condition:', config.captureCondition) +console.log('Capture condition:', config.captureCondition) console.log('Fee calculator:', config.feeCalculator) console.log('Fee recipient:', config.feeReceiver) ``` diff --git a/sdk/concepts.mdx b/sdk/concepts.mdx index 31c1f93..e233abc 100644 --- a/sdk/concepts.mdx +++ b/sdk/concepts.mdx @@ -97,12 +97,12 @@ RefundRequestStatus.Refused // 4 - Arbiter declined to rule ### Refund flow -In v3, RefundRequest is wired as an IRecorder plugin. Refund approval happens automatically when the merchant or arbiter calls `voidPayment()` on the operator, no separate approve step is needed. +In v3, RefundRequest is wired as an IHook plugin. Refund approval happens automatically when the merchant or arbiter calls `voidPayment()` on the operator, no separate approve step is needed. ```mermaid sequenceDiagram participant P as Payer - participant R as RefundRequest (IRecorder) + participant R as RefundRequest (IHook) participant M as Merchant participant O as PaymentOperator participant A as Arbiter @@ -173,7 +173,7 @@ flowchart TB subgraph Components[Supporting Contracts] direction LR - subgraph RR[RefundRequest IRecorder] + subgraph RR[RefundRequest IHook] rr1[Request/Cancel] rr2[Deny/Refuse] rr3[Auto-approve on refund] diff --git a/sdk/create-client.mdx b/sdk/create-client.mdx index 473f399..93db678 100644 --- a/sdk/create-client.mdx +++ b/sdk/create-client.mdx @@ -90,7 +90,7 @@ import { createX402r, queryActions } from '@x402r/sdk' const client = createX402r({ publicClient, operatorAddress: '0x...' }) const extended = client.extend( - queryActions('0xRecorderAddress', { eventFromBlock: 100000n }) + queryActions('0xHookAddress', { eventFromBlock: 100000n }) ) // extended.query is now defined @@ -167,7 +167,7 @@ For standalone helpers that extract identity data from x402 extension responses - Accept payments and release funds. + Accept payments and capture funds. Get the addresses for your client config. diff --git a/sdk/delivery-arbiter.mdx b/sdk/delivery-arbiter.mdx index 7bcc26f..6e60fa9 100644 --- a/sdk/delivery-arbiter.mdx +++ b/sdk/delivery-arbiter.mdx @@ -14,111 +14,111 @@ icon: "shield-check" There is a full [AI garbage detector example](https://github.com/BackTrackCo/arbiter-examples) that implements this pattern with heuristic + LLM evaluation. -### 1. Install Dependencies - - -```bash npm -npm install @x402r/sdk @x402r/helpers -``` -```bash pnpm -pnpm add @x402r/sdk @x402r/helpers -``` -```bash bun -bun add @x402r/sdk @x402r/helpers -``` - - -### 2. Create the Arbiter Client - -```typescript -import { createPublicClient, createWalletClient, http } from 'viem' -import { baseSepolia } from 'viem/chains' -import { privateKeyToAccount } from 'viem/accounts' -import { createArbiterClient } from '@x402r/sdk' -import { fromNetworkId } from '@x402r/core' - -const account = privateKeyToAccount(process.env.ARBITER_PRIVATE_KEY as `0x${string}`) - -const arbiter = createArbiterClient({ - publicClient: createPublicClient({ chain: baseSepolia, transport: http() }), - walletClient: createWalletClient({ - account, - chain: baseSepolia, - transport: http(), - }), - operatorAddress: process.env.OPERATOR_ADDRESS as `0x${string}`, - escrowPeriodAddress: process.env.ESCROW_PERIOD_ADDRESS as `0x${string}`, -}) -``` - -### 3. Handle the Verify Endpoint - -The merchant's `forwardToArbiter()` hook POSTs `{ responseBody, transaction, paymentPayload }` to `/verify`. The payload does not contain `PaymentInfo` directly. Reconstruct it from `paymentPayload.accepted.extra` + `paymentPayload.payload.salt` + the recovered `payer` + the top-level requirements (`payTo`, `asset`, `amount`), or run the payload back through your own facilitator's verify step to recover it. - -```typescript -import { isAuthCapturePayload } from '@x402r/helpers' -import express from 'express' - -const app = express() -app.use(express.json()) - -app.post('/verify', async (req, res) => { - const { responseBody, transaction, paymentPayload } = req.body - - if (!isAuthCapturePayload(paymentPayload?.payload)) { - res.status(400).json({ error: 'unsupported_payload' }) - return - } - - // Reconstruct PaymentInfo from the wire payload (see forwardToArbiter docs). - const paymentInfo = reconstructPaymentInfo(paymentPayload) - - // Your evaluation logic - const passed = await evaluate(responseBody) - - if (passed) { - const amounts = await arbiter.payment.getAmounts(paymentInfo) - await arbiter.payment.capture(paymentInfo, amounts.capturableAmount) - res.json({ verdict: 'PASS' }) - } else { - // Arbiter can refund immediately without waiting for escrow expiry. - await arbiter.payment.voidPayment(paymentInfo) - res.json({ verdict: 'FAIL' }) - } -}) - -app.listen(3001) -``` - -### 4. Implement Your Evaluation Logic - -The `evaluate()` function is where your logic lives. It could be: - -- **Heuristic checks:** HTTP status code, response size, content-type validation -- **AI evaluation:** send response body to an LLM and ask "is this a valid response?" -- **Schema validation:** check if the response matches an expected JSON schema - -```typescript -async function evaluate(responseBody: string): Promise { - // Example: reject empty or error responses - if (!responseBody || responseBody.length < 10) return false - if (responseBody.includes('"error"')) return false - - // Example: LLM evaluation - // const result = await llm.evaluate(responseBody) - // return result.verdict === 'PASS' - - return true -} -``` - -### 5. What Happens on Failure - -With delivery protection v2, the arbiter can call `voidPayment()` immediately on a FAIL verdict. You do not need to wait for escrow expiry. The receiver (merchant) can also trigger a voluntary refund at any time. - - -If your service goes down, no payments get evaluated and funds stay in escrow until timeout. The escrow period protects payers, but add uptime monitoring and alerting. - + + + + ```bash npm + npm install @x402r/sdk @x402r/helpers + ``` + ```bash pnpm + pnpm add @x402r/sdk @x402r/helpers + ``` + ```bash bun + bun add @x402r/sdk @x402r/helpers + ``` + + + + + ```typescript + import { createPublicClient, createWalletClient, http } from 'viem' + import { baseSepolia } from 'viem/chains' + import { privateKeyToAccount } from 'viem/accounts' + import { createArbiterClient } from '@x402r/sdk' + + const account = privateKeyToAccount(process.env.ARBITER_PRIVATE_KEY as `0x${string}`) + + const arbiter = createArbiterClient({ + publicClient: createPublicClient({ chain: baseSepolia, transport: http() }), + walletClient: createWalletClient({ + account, + chain: baseSepolia, + transport: http(), + }), + operatorAddress: process.env.OPERATOR_ADDRESS as `0x${string}`, + escrowPeriodAddress: process.env.ESCROW_PERIOD_ADDRESS as `0x${string}`, + }) + ``` + + + + The merchant's `forwardToArbiter()` hook POSTs `{ responseBody, transaction, paymentPayload }` to `/verify`. The payload does not contain `PaymentInfo` directly. Reconstruct it from `paymentPayload.accepted.extra` + `paymentPayload.payload.salt` + the recovered `payer` + the top-level requirements (`payTo`, `asset`, `amount`), or run the payload back through your own facilitator's verify step to recover it. + + ```typescript + import { isAuthCapturePayload } from '@x402r/helpers' + import express from 'express' + + const app = express() + app.use(express.json()) + + app.post('/verify', async (req, res) => { + const { responseBody, transaction, paymentPayload } = req.body + + if (!isAuthCapturePayload(paymentPayload?.payload)) { + res.status(400).json({ error: 'unsupported_payload' }) + return + } + + // Reconstruct PaymentInfo from the wire payload (see forwardToArbiter docs). + const paymentInfo = reconstructPaymentInfo(paymentPayload) + + const passed = await evaluate(responseBody) + + if (passed) { + const amounts = await arbiter.payment.getAmounts(paymentInfo) + await arbiter.payment.capture(paymentInfo, amounts.capturableAmount) + res.json({ verdict: 'PASS' }) + } else { + // Arbiter can refund immediately without waiting for escrow expiry. + await arbiter.payment.voidPayment(paymentInfo) + res.json({ verdict: 'FAIL' }) + } + }) + + app.listen(3001) + ``` + + + + The `evaluate()` function is where your logic lives. It could be: + + - **Heuristic checks**: HTTP status code, response size, content-type validation + - **AI evaluation**: send response body to an LLM and ask "is this a valid response?" + - **Schema validation**: check if the response matches an expected JSON schema + + ```typescript + async function evaluate(responseBody: string): Promise { + // Reject empty or error responses + if (!responseBody || responseBody.length < 10) return false + if (responseBody.includes('"error"')) return false + + // LLM evaluation + // const result = await llm.evaluate(responseBody) + // return result.verdict === 'PASS' + + return true + } + ``` + + + + With delivery protection, the arbiter can call `voidPayment()` immediately on a FAIL verdict. You do not need to wait for escrow expiry. The receiver (merchant) can also trigger a voluntary refund at any time. + + + If your service goes down, no payments get evaluated and funds stay in escrow until timeout. The escrow period protects payers, but add uptime monitoring and alerting. + + + ## Next Steps diff --git a/sdk/delivery-merchant.mdx b/sdk/delivery-merchant.mdx index 28ab7f7..66ef98b 100644 --- a/sdk/delivery-merchant.mdx +++ b/sdk/delivery-merchant.mdx @@ -9,61 +9,63 @@ icon: "store" * A deployed delivery protection operator (see [Deploy an Operator](/sdk/deploy-operator#delivery-protection-operator)) * An arbiter service URL (see [Arbiter Setup](/sdk/delivery-arbiter)) -### 1. Install Dependencies - - -```bash npm -npm install @x402r/helpers -``` -```bash pnpm -pnpm add @x402r/helpers -``` -```bash bun -bun add @x402r/helpers -``` - - -### 2. Configure forwardToArbiter() - -Add the `forwardToArbiter()` hook to your x402 resource server. After every payment settlement, it POSTs the HTTP response body to your arbiter service: - -```typescript -import { forwardToArbiter } from '@x402r/helpers' - -const resourceServer = new x402ResourceServer(facilitatorConfig) -registerCommerceEvmScheme(resourceServer) - -resourceServer.onAfterSettle( - forwardToArbiter('http://your-arbiter:3001', { - onError: (err) => console.error('Arbiter unreachable:', err), - }) -) -``` - -The hook POSTs to `{arbiterUrl}/verify` with: - -```json -{ - "responseBody": "the HTTP response body as a string", - "transaction": "0xsettlement_tx_hash", - "paymentPayload": { - "x402Version": 1, - "scheme": "commerce", - "accepted": { "network": "eip155:84532", ... }, - "payload": { "paymentInfo": { ... }, ... } - } -} -``` - -The arbiter uses `parseForwardedPayload()` from `@x402r/helpers` to extract `paymentInfo` and `network` from the nested structure. - - -`forwardToArbiter()` is fire-and-forget. If the arbiter service is unreachable, funds stay in escrow until timeout. Add monitoring for arbiter availability. - - -### 3. Share Addresses with the Arbiter - -The arbiter service needs `operatorAddress` and `escrowPeriodAddress` from your [deployment](/sdk/deploy-operator#delivery-protection-operator) to create its SDK client. Share these via config, environment variables, or a shared registry. + + + + ```bash npm + npm install @x402r/helpers + ``` + ```bash pnpm + pnpm add @x402r/helpers + ``` + ```bash bun + bun add @x402r/helpers + ``` + + + + + Add the `forwardToArbiter()` hook to your x402 resource server. After every successful `authCapture` settlement, it POSTs to your arbiter service fire-and-forget: + + ```typescript + import { forwardToArbiter } from '@x402r/helpers' + import { registerAuthCaptureEvmScheme } from '@x402r/evm/authCapture/facilitator' + + const resourceServer = new x402ResourceServer(facilitatorConfig) + registerAuthCaptureEvmScheme(resourceServer) + + resourceServer.onAfterSettle( + forwardToArbiter('http://your-arbiter:3001', { + onError: (err) => console.error('Arbiter unreachable:', err), + }), + ) + ``` + + The hook POSTs to `{arbiterUrl}/verify` with: + + ```json + { + "responseBody": "the HTTP response body as a string", + "transaction": "0xsettlement_tx_hash", + "paymentPayload": { + "x402Version": 2, + "accepted": { "scheme": "authCapture", "network": "eip155:84532", "...": "..." }, + "payload": { "authorization": { "...": "..." }, "signature": "0x...", "salt": "0x..." } + } + } + ``` + + The payload does not include a nested `paymentInfo`. The arbiter reconstructs `PaymentInfo` from `accepted.extra` + `payload.salt` + the recovered payer + the top-level requirements. See [forwardToArbiter() docs](/sdk/helpers/forward-to-arbiter) for the full payload shape. + + + `forwardToArbiter()` is fire-and-forget. If the arbiter service is unreachable, funds stay in escrow until timeout. Add monitoring for arbiter availability. + + + + + The arbiter service needs `operatorAddress` and `escrowPeriodAddress` from your [deployment](/sdk/deploy-operator#delivery-protection-operator) to construct its SDK client. Share these via config, environment variables, or a shared registry. + + ## Next Steps diff --git a/sdk/delivery-protection.mdx b/sdk/delivery-protection.mdx index e4867e1..7876853 100644 --- a/sdk/delivery-protection.mdx +++ b/sdk/delivery-protection.mdx @@ -4,7 +4,7 @@ description: "Automated quality verification for every transaction." icon: "shield-check" --- -In the delivery protection model, the arbiter evaluates every transaction automatically. The arbiter or a satisfied payer can release funds. If the arbiter issues a FAIL verdict, it can trigger an immediate refund without waiting for escrow expiry. If nobody acts, funds auto-refund to the payer after escrow expires. +In the delivery protection model, the arbiter evaluates every transaction automatically. The arbiter or a satisfied payer can capture funds. If the arbiter issues a FAIL verdict, it can trigger an immediate refund without waiting for escrow expiry. If nobody acts, funds auto-refund to the payer after escrow expires. This is different from the [marketplace model](/sdk/overview) where the merchant releases funds and the arbiter only gets involved when a payer files a dispute. @@ -14,7 +14,7 @@ This is different from the [marketplace model](/sdk/overview) where the merchant | Refund during escrow | Receiver or arbiter | Escrow expiry, receiver, or arbiter | | Dispute process | Payer files refund request | No disputes needed | | Arbiter involvement | Only on disputes | Every transaction | -| Contracts deployed | ~8 (Operator, EscrowPeriod, RefundRequest, Evidence, Freeze, etc.) | 6 (Operator, EscrowPeriod, SAC, 2x OrCondition, RecorderCombinator) | +| Contracts deployed | ~8 (Operator, EscrowPeriod, RefundRequest, Evidence, Freeze, etc.) | 6 (Operator, EscrowPeriod, SAC, 2x OrCondition, HookCombinator) | | Deploy preset | `deployMarketplaceOperator()` | `deployDeliveryProtectionOperator()` | Use this when every response needs automated quality checks: AI content verification, garbage detection, schema validation. diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index 57bcc7f..410c764 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -25,10 +25,10 @@ All contracts are deployed via factories using CREATE2, so identical configurati A complete marketplace operator deployment includes: -1. **EscrowPeriod**: Records authorization time, enforces waiting period before release +1. **EscrowPeriod**: Records authorization time, enforces waiting period before capture 2. **Freeze**: Allows payer to freeze payment during escrow, receiver to unfreeze 3. **ReceiverCondition**: Gates voids to the merchant (receiver) -4. **RefundRequest (IRecorder)**: Wired as `voidPostActionHook`, auto-approves pending refund requests during `voidPayment()` +4. **RefundRequest (IHook)**: Wired as `voidPostActionHook`, auto-approves pending refund requests during `voidPayment()` 5. **StaticFeeCalculator**: Optional operator fee (basis points) 6. **PaymentOperator**: The main contract tying everything together @@ -150,7 +150,7 @@ console.log('Existing (reused):', result.summary.existingCount); ```typescript interface MarketplaceOperatorDeployment { operatorAddress: Address // The PaymentOperator - escrowPeriodAddress: Address // EscrowPeriod recorder/condition + escrowPeriodAddress: Address // EscrowPeriod hook/condition freezeAddress: Address | null // Freeze condition (null if disabled) refundRequestAddress: Address // RefundRequest contract refundRequestEvidenceAddress: Address // RefundRequestEvidence contract @@ -195,7 +195,7 @@ The deployed marketplace operator has the following slot configuration: | `AUTHORIZE_PRE_ACTION_CONDITION` | UsdcTvlLimit | Safety limit on authorization | | `AUTHORIZE_POST_ACTION_HOOK` | EscrowPeriod | Records authorization timestamp | | `CHARGE_PRE_ACTION_CONDITION` | (none) | No restrictions on charge | -| `CAPTURE_PRE_ACTION_CONDITION` | EscrowPeriod (or AND(EscrowPeriod, Freeze) if freeze enabled) | Blocks release during escrow period | +| `CAPTURE_PRE_ACTION_CONDITION` | EscrowPeriod (or AND(EscrowPeriod, Freeze) if freeze enabled) | Blocks capture during escrow period | | `VOID_PRE_ACTION_CONDITION` | OR(Receiver, Arbiter) | Receiver or arbiter can approve | | `VOID_POST_ACTION_HOOK` | RefundRequest | Tracks refund request state | | `REFUND_PRE_ACTION_CONDITION` | Receiver | Only receiver after escrow | @@ -221,7 +221,7 @@ Deployment requires gas fees. Ensure your wallet has ETH on the target network. ## Delivery Protection Operator -For automated quality verification (AI garbage detection, schema validation), use the delivery protection preset. No RefundRequest, Evidence, or Freeze contracts. The arbiter or payer can release funds, and the arbiter can issue immediate refunds without waiting for escrow expiry. +For automated quality verification (AI garbage detection, schema validation), use the delivery protection preset. No RefundRequest, Evidence, or Freeze contracts. The arbiter or payer can capture funds, and the arbiter can issue immediate refunds without waiting for escrow expiry. ```typescript import { createPublicClient, createWalletClient, http } from 'viem'; @@ -256,17 +256,17 @@ console.log('Operator:', deployment.operatorAddress) console.log('EscrowPeriod:', deployment.escrowPeriodAddress) console.log('ArbiterCondition:', deployment.arbiterConditionAddress) console.log('ReleaseCondition:', deployment.captureConditionAddress) -console.log('AuthorizeRecorder:', deployment.authorizeHookAddress) +console.log('AuthorizeHook:', deployment.authorizeHookAddress) ``` | Option | Type | Description | |--------|------|-------------| | `chainId` | `number` | Target chain | -| `arbiter` | `Address` | Arbiter address for release and refund decisions | +| `arbiter` | `Address` | Arbiter address for capture and refund decisions | | `feeReceiver` | `Address` | Receives protocol fees | | `escrowPeriodSeconds` | `bigint` | Verification window before auto-refund | -| `authorizedCodehash` | `Hex` | Override the default `recorderCombinatorCodehash`. Optional | -| `paymentIndexRecorderHookAddress` | `Address` | Override the default PaymentIndexRecorder. Pass `zeroAddress` to skip on-chain payment indexing. Optional | +| `authorizedCodehash` | `Hex` | Override the default `hookCombinatorCodehash`. Optional | +| `paymentIndexRecorderHookAddress` | `Address` | Override the default PaymentIndexRecorderHook. Pass `zeroAddress` to skip on-chain payment indexing. Optional | | `allowArbiterRefund` | `boolean` | Allow arbiter to refund immediately during escrow. Default: `false` | @@ -278,7 +278,7 @@ console.log('AuthorizeRecorder:', deployment.authorizeHookAddress) arbiterConditionAddress: Address captureConditionAddress: Address // OrCondition([arbiter, payer]) voidConditionAddress: Address // OrCondition([escrowPeriod, receiver, arbiter]) - authorizeHookAddress: Address // RecorderCombinator([escrowPeriod, paymentIndexRecorder]) + authorizeHookAddress: Address // HookCombinator([escrowPeriod, paymentIndexRecorderHook]) paymentIndexRecorderHookAddress: Address operatorConfig: OperatorConfig deployments: DeployResult[] @@ -290,7 +290,7 @@ console.log('AuthorizeRecorder:', deployment.authorizeHookAddress) } ``` - Deploys 6 contracts by default: EscrowPeriod, StaticAddressCondition(arbiter), OrCondition(release), OrCondition(refund), RecorderCombinator, and the Operator. If you pass `paymentIndexRecorderHookAddress: zeroAddress`, the RecorderCombinator is skipped (5 contracts). + Deploys 6 contracts by default: EscrowPeriod, StaticAddressCondition(arbiter), OrCondition(release), OrCondition(refund), HookCombinator, and the Operator. If you pass `paymentIndexRecorderHookAddress: zeroAddress`, the HookCombinator is skipped (5 contracts). Redeploying with the same parameters is idempotent (CREATE2). It detects existing contracts and skips them. @@ -310,15 +310,15 @@ console.log('AuthorizeRecorder:', deployment.authorizeHookAddress) console.log('Operator will be at:', preview.operatorAddress) console.log('EscrowPeriod will be at:', preview.escrowPeriodAddress) - console.log('AuthorizeRecorder will be at:', preview.authorizeHookAddress) + console.log('AuthorizeHook will be at:', preview.authorizeHookAddress) ``` | Slot | Contract | Purpose | |------|----------|---------| - | `CAPTURE_PRE_ACTION_CONDITION` | OrCondition([SAC(arbiter), PayerCondition]) | Arbiter or satisfied payer can release | - | `AUTHORIZE_POST_ACTION_HOOK` | RecorderCombinator([EscrowPeriod, PaymentIndexRecorder]) | Records authorization time and indexes payments on-chain | + | `CAPTURE_PRE_ACTION_CONDITION` | OrCondition([SAC(arbiter), PayerCondition]) | Arbiter or satisfied payer can capture | + | `AUTHORIZE_POST_ACTION_HOOK` | HookCombinator([EscrowPeriod, PaymentIndexRecorderHook]) | Records authorization time and indexes payments on-chain | | `VOID_PRE_ACTION_CONDITION` | OrCondition([EscrowPeriod, ReceiverCondition, SAC(arbiter)]) | Escrow expiry, receiver voluntary refund, or arbiter immediate refund | | `REFUND_PRE_ACTION_CONDITION` | ReceiverCondition | Only receiver after escrow | @@ -328,7 +328,7 @@ console.log('AuthorizeRecorder:', deployment.authorizeHookAddress) - Accept payments, release funds from escrow. + Accept payments, capture funds from escrow. Request refunds, freeze payments, submit evidence. @@ -340,6 +340,6 @@ console.log('AuthorizeRecorder:', deployment.authorizeHookAddress) Forward escrow settlements to an arbiter service. - On-chain architecture, conditions, and recorders. + On-chain architecture, conditions, and hooks. diff --git a/sdk/limitations.mdx b/sdk/limitations.mdx index fdf8cf3..f1eedc5 100644 --- a/sdk/limitations.mdx +++ b/sdk/limitations.mdx @@ -4,7 +4,7 @@ description: "Known limitations and constraints in the current SDK" icon: "circle-info" --- -The SDK provides full coverage of core payment flows including authorization, release, charge, refund, dispute resolution, and evidence submission. This page documents the known limitations. +The SDK provides full coverage of core payment flows including authorization, capture, charge, refund, dispute resolution, and evidence submission. This page documents the known limitations. ## API Constraints diff --git a/sdk/merchant.mdx b/sdk/merchant.mdx index ee89724..42ae697 100644 --- a/sdk/merchant.mdx +++ b/sdk/merchant.mdx @@ -1,6 +1,6 @@ --- title: "Merchant Guide" -description: "Accept a payment into escrow, check state, and release funds." +description: "Accept a payment into escrow, check state, and capture funds." icon: "store" --- @@ -70,19 +70,19 @@ const inEscrow = await merchant.escrow?.isDuringEscrow(paymentInfo) console.log('In escrow:', inEscrow) // true ``` -### 4. Release Funds After Escrow +### 4. Capture Funds After Escrow -`release()` reverts if called during escrow. Check `escrow.isDuringEscrow()` first. Pass a smaller amount to release partially. +`capture()` reverts if called during escrow. Check `escrow.isDuringEscrow()` first. Pass a smaller amount to capture partially. ```typescript const releaseTx = await merchant.payment.capture(paymentInfo, 1_000_000n) -console.log('Released:', releaseTx) +console.log('Captured:', releaseTx) // Verify const after = await merchant.payment.getAmounts(paymentInfo) -console.log('Capturable after release:', after.capturableAmount) // 0n +console.log('Capturable after capture:', after.capturableAmount) // 0n ``` ### 5. Handle Refund Requests (Optional) @@ -97,7 +97,7 @@ if (hasRefund) { console.log('Refund amount:', request?.amount) console.log('Status:', request?.status) // 0 = Pending - // Approve by executing voidPayment (recorder auto-approves) + // Approve by executing voidPayment (hook auto-approves) const refundTx = await merchant.payment.voidPayment( paymentInfo, request!.amount, diff --git a/sdk/merchant/getting-started.mdx b/sdk/merchant/getting-started.mdx index cb0f57f..dd0ec53 100644 --- a/sdk/merchant/getting-started.mdx +++ b/sdk/merchant/getting-started.mdx @@ -175,7 +175,7 @@ The full source code for this example is available on [GitHub](https://github.co Forward escrow settlements to an arbiter service. - Release payments, handle refunds, and manage escrow. + Capture payments, handle refunds, and manage escrow. Deploy your own PaymentOperator contract. diff --git a/sdk/merchant/payment-operations.mdx b/sdk/merchant/payment-operations.mdx index 89903ff..97b4bad 100644 --- a/sdk/merchant/payment-operations.mdx +++ b/sdk/merchant/payment-operations.mdx @@ -1,10 +1,10 @@ --- title: "Payment operations" -description: "Release funds, charge payments, process refunds, and query escrow state" +description: "Capture funds, charge payments, process refunds, and query escrow state" icon: "coins" --- -The merchant client provides methods for managing the full payment lifecycle through the `payment` and `operator` action groups: releasing escrowed funds, charging directly for subscriptions, processing refunds, and querying operator configuration. +Use `createMerchantClient` to capture escrowed funds, charge directly, void or refund, and query operator state. The methods below sit on the `payment` and `operator` action groups. ## Payment operations @@ -13,15 +13,15 @@ The merchant client provides methods for managing the full payment lifecycle thr Transfer escrowed funds to the receiver (merchant). The `amount` parameter is required. ```typescript -// Release 10 USDC (6 decimals) from escrow +// Capture 10 USDC (6 decimals) from escrow const tx = await merchant.payment.capture(paymentInfo, 10_000_000n) -console.log('Released:', tx) +console.log('Captured:', tx) ``` -For partial releases, specify a smaller amount. The remaining funds stay in escrow. +For partial captures, specify a smaller amount. The remaining funds stay in escrow. ```typescript -// Release 3 USDC of a 10 USDC escrow +// Capture 3 USDC of a 10 USDC escrow const tx = await merchant.payment.capture(paymentInfo, 3_000_000n) // Check what remains @@ -167,19 +167,19 @@ console.log('Total fee:', fees.totalFeeAmount) console.log('Net amount:', fees.netAmount) ``` -## Release vs refund decision flow +## Capture vs refund decision flow ```mermaid flowchart TD A[Payment in Escrow] --> B{Check payment.getAmounts} B --> C{capturableAmount > 0?} C -->|Yes| D{Has refund request?} - C -->|No| E[Nothing to release] - D -->|No| F[Safe to release] + C -->|No| E[Nothing to capture] + D -->|No| F[Safe to capture] D -->|Yes| G{Approve refund?} F --> H["payment.capture(paymentInfo, amount)"] G -->|Yes| I["payment.voidPayment(paymentInfo)"] - G -->|No| J[Deny request, then release] + G -->|No| J[Deny request, then capture] J --> H ``` diff --git a/sdk/merchant/quickstart.mdx b/sdk/merchant/quickstart.mdx index 6c84e4e..a8a7baa 100644 --- a/sdk/merchant/quickstart.mdx +++ b/sdk/merchant/quickstart.mdx @@ -1,6 +1,6 @@ --- title: "Merchant SDK" -description: "Release funds, charge payments, process refunds, and query escrow state" +description: "Capture funds, charge payments, process refunds, and query escrow state" icon: "rocket" --- @@ -49,22 +49,22 @@ const merchant = createMerchantClient({ }) ``` -## Release funds from escrow +## Capture funds from escrow Use `payment.capture()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is required. ```typescript -// Release 10 USDC (6 decimals) from escrow +// Capture 10 USDC (6 decimals) from escrow const tx = await merchant.payment.capture(paymentInfo, 10_000_000n) -console.log('Released:', tx) +console.log('Captured:', tx) ``` -For partial releases, specify a smaller amount. The remaining funds stay in escrow. +For partial captures, specify a smaller amount. The remaining funds stay in escrow. ```typescript -// Release 3 USDC of a 10 USDC escrow +// Capture 3 USDC of a 10 USDC escrow const tx = await merchant.payment.capture(paymentInfo, 3_000_000n) -console.log('Partial release:', tx) +console.log('Partial capture:', tx) // Check what remains const amounts = await merchant.payment.getAmounts(paymentInfo) @@ -106,7 +106,7 @@ console.log('Charged:', tx) The `charge()` method is designed for recurring payments and session-based billing where funds are not pre-escrowed. The token collector contract handles the actual token transfer. -## Refund after release (after capture) +## Refund after capture (after capture) Use `payment.refund()` to refund funds that have already been released. This requires a token collector to source the refund from the merchant's balance. @@ -192,19 +192,19 @@ console.log('Total fee:', fees.totalFeeAmount) console.log('Net amount:', fees.netAmount) ``` -## Release vs refund decision flow +## Capture vs refund decision flow ```mermaid flowchart TD A[Payment in Escrow] --> B{Check payment.getAmounts} B --> C{capturableAmount > 0?} C -->|Yes| D{Has refund request?} - C -->|No| E[Nothing to release] - D -->|No| F[Safe to release] + C -->|No| E[Nothing to capture] + D -->|No| F[Safe to capture] D -->|Yes| G{Approve refund?} F --> H["payment.capture(paymentInfo, amount)"] G -->|Yes| I["payment.voidPayment(paymentInfo)"] - G -->|No| J[Deny request, then release] + G -->|No| J[Deny request, then capture] J --> H ``` diff --git a/sdk/merchant/refund-handling.mdx b/sdk/merchant/refund-handling.mdx index 7d5bc41..48f1e76 100644 --- a/sdk/merchant/refund-handling.mdx +++ b/sdk/merchant/refund-handling.mdx @@ -139,7 +139,7 @@ Check whether a payment has been frozen by the payer. Frozen payments cannot be const frozen = await merchant.freeze?.isFrozen(paymentInfo) if (frozen) { - console.log('Payment is frozen, cannot release until unfrozen') + console.log('Payment is frozen, cannot capture until unfrozen') } ``` @@ -257,9 +257,9 @@ sequenceDiagram Watch for refund requests in real-time instead of polling. - Release funds, charge, and query escrow state. + Capture funds, charge, and query escrow state. - + RefundRequest contract details and state machine. diff --git a/sdk/merchant/subscriptions.mdx b/sdk/merchant/subscriptions.mdx index 81b8692..b0be4d1 100644 --- a/sdk/merchant/subscriptions.mdx +++ b/sdk/merchant/subscriptions.mdx @@ -1,6 +1,6 @@ --- title: "Merchant events" -description: "Subscribe to real-time refund, release, and fee events" +description: "Subscribe to real-time refund, capture, and fee events" icon: "bell" --- @@ -24,14 +24,14 @@ unwatch() ### Example: revenue tracking ```typescript -let totalReleased = 0n +let totalCaptured = 0n const unwatch = merchant.watch.onPayment((logs) => { for (const log of logs) { if (log.eventName === 'CaptureExecuted') { const amount = log.args?.amount ?? 0n - totalReleased += amount - console.log('Release: +', amount, 'Total:', totalReleased) + totalCaptured += amount + console.log('Capture: +', amount, 'Total:', totalCaptured) } } }) @@ -119,7 +119,7 @@ For reliable real-time delivery, configure your `publicClient` with a [WebSocket - Release funds, charge, and query escrow state. + Capture funds, charge, and query escrow state. Learn about dispute resolution from the arbiter perspective. diff --git a/sdk/overview.mdx b/sdk/overview.mdx index d64ea17..088e8b7 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -10,7 +10,7 @@ The X402r SDK is in active development. APIs may change between releases. Always Three roles interact with the protocol: -- **Merchants** receive payments into escrow and release funds after delivery +- **Merchants** receive payments into escrow and capture funds after delivery - **Payers** can request refunds, freeze payments, and submit evidence during disputes - **Arbiters** verify transactions or resolve disputes (two models below) @@ -18,7 +18,7 @@ Three roles interact with the protocol: **Marketplace** (`deployMarketplaceOperator`): The merchant releases funds after escrow. If the payer contests, they file a refund request and an arbiter resolves it. Use this for general commerce where most transactions are uncontested. -**Delivery Protection** (`deployDeliveryProtectionOperator`): The arbiter evaluates every transaction automatically. The arbiter or a satisfied payer can release funds. On a FAIL verdict, the arbiter can trigger an immediate refund. If nobody acts, funds auto-refund after escrow. Use this for AI content verification, schema validation, or automated quality checks. +**Delivery Protection** (`deployDeliveryProtectionOperator`): The arbiter evaluates every transaction automatically. The arbiter or a satisfied payer can capture funds. On a FAIL verdict, the arbiter can trigger an immediate refund. If nobody acts, funds auto-refund after escrow. Use this for AI content verification, schema validation, or automated quality checks. ### Packages @@ -82,7 +82,7 @@ bunx @x402r/cli pay [options] - Deploy an operator, accept a payment, release funds from escrow. + Deploy an operator, accept a payment, capture funds from escrow. High-level client with role-scoped factories (`createPayerClient`, `createMerchantClient`, `createArbiterClient`) and the generic `createX402r`. diff --git a/sdk/payer.mdx b/sdk/payer.mdx index 4289d64..8466f42 100644 --- a/sdk/payer.mdx +++ b/sdk/payer.mdx @@ -54,7 +54,7 @@ const payer = createPayerClient({ ``` -All payer actions (refund, freeze, evidence) must happen during the escrow period. Once escrow expires, the merchant can release. +All payer actions (refund, freeze, evidence) must happen during the escrow period. Once escrow expires, the merchant can capture. ### 3. Check Payment State @@ -96,7 +96,7 @@ console.log('Status:', status) // 0 = Pending, 1 = Approved, 2 = Denied, 3 = Can ### 5. Freeze a Payment (Optional) -`freeze()` blocks the merchant from releasing until the arbiter unfreezes. Only use when you need to prevent a release during investigation. +`freeze()` blocks the merchant from releasing until the arbiter unfreezes. Only use when you need to prevent a capture during investigation. ```typescript diff --git a/x402-integration/auth-capture-scheme.mdx b/x402-integration/auth-capture-scheme.mdx index e2a731d..e0d4533 100644 --- a/x402-integration/auth-capture-scheme.mdx +++ b/x402-integration/auth-capture-scheme.mdx @@ -35,7 +35,7 @@ AUTHORIZE -> RESOURCE DELIVERED -> CAPTURE / VOID -> (REFUND) - The captureAuthorizer can capture (release funds to the receiver) or void (return escrowed funds to the client). Capture conditions are policy-defined per captureAuthorizer (time-locked, arbiter-approved, etc.). + The captureAuthorizer can capture (capture funds to the receiver) or void (return escrowed funds to the client). Capture conditions are policy-defined per captureAuthorizer (time-locked, arbiter-approved, etc.). @@ -140,7 +140,7 @@ The **captureAuthorizer** is the address authorized to authorize/capture/void/re |---|---| | Session billing | EOA that tracks usage off-chain, captures periodically | | Time-locked escrow | Contract that releases after a period expires | -| Dispute resolution | Arbiter contract that decides release vs refund | +| Dispute resolution | Arbiter contract that decides capture vs refund | | Immediate (exact-like) | Facilitator with `autoCapture: true` for instant settlement | | Streaming payments | Contract that performs time-proportional captures | @@ -364,7 +364,7 @@ The escrow contract enforces invariants on-chain: -**CaptureAuthorizer Trust Required:** The captureAuthorizer controls when and how much to release. Choose carefully and understand the release policy. See [Operators](/contracts/overview#payment-operator) for examples. +**CaptureAuthorizer Trust Required:** The captureAuthorizer controls when and how much to capture. Choose carefully and understand the capture policy. See [Operators](/contracts/overview#payment-operator) for examples. ## Error Codes diff --git a/x402-integration/comparison.mdx b/x402-integration/comparison.mdx index 0a4f808..09c576a 100644 --- a/x402-integration/comparison.mdx +++ b/x402-integration/comparison.mdx @@ -30,7 +30,7 @@ icon: "scale-balanced" |---------|-------|--------| | **Settlement Timing** | Immediate | Deferred (conditional) | | **Payer Protection** | None (payment final) | Refund possible until capture | -| **Receiver Risk** | No risk (paid upfront) | Must wait for release | +| **Receiver Risk** | No risk (paid upfront) | Must wait for capture | | **Gas Cost** | Single transaction | Two transactions (auth + capture) | | **Complexity** | Minimal (direct transfer) | Higher (operator logic) | | **Variable Pricing** | Not supported | Supported (authorize max, capture actual) | @@ -222,7 +222,7 @@ Why: Variable pricing, multiple requests, moderate trust. Authorize $20 max, cap **Duration:** 48 hours **Decision:** ✅ **Use escrow** -Why: High value, long-running, need verification before payment. Release after job completes successfully. +Why: High value, long-running, need verification before payment. Capture after job completes successfully. ### Scenario 4: Freelance Work diff --git a/x402-integration/overview.mdx b/x402-integration/overview.mdx index 7651a53..bfd4acc 100644 --- a/x402-integration/overview.mdx +++ b/x402-integration/overview.mdx @@ -66,10 +66,10 @@ Client authorizes $10 → Uses $6.50 → Operator captures $6.50 → Refund $3.5 Client pays for video rendering → 48 hours later → How to verify completion? ``` -**Escrow Solution:** Conditional release with verification +**Escrow Solution:** Conditional capture with verification ``` -Client authorizes → Work progresses → Client verifies → Release on approval +Client authorizes → Work progresses → Client verifies → Capture on approval ``` ### Multi-Request Sessions @@ -96,10 +96,10 @@ x402r provides the **authCapture scheme implementation** for x402: - On-chain safety guarantees 2. **Operator Contracts** - - Conditional release logic + - Conditional capture logic - Dispute resolution - Fee distribution - - Time-based release + - Time-based capture 3. **Payment Facilitator** - Validates ERC-3009 signatures @@ -132,7 +132,7 @@ sequenceDiagram Server-->>Client: 200 OK + resource Note over Operator,Escrow: Later: capture or void - Operator->>Escrow: release(paymentInfo, amount) + Operator->>Escrow: capture(paymentInfo, amount) Note over Escrow: Funds released to receiver (minus fees) ``` @@ -187,7 +187,7 @@ sequenceDiagram Lock funds in escrow without immediate transfer. Client signs an ERC-3009 authorization allowing the escrow contract to pull tokens. ### Capture -Release authorized funds to the receiver. The operator contract decides when and how much to release based on configured conditions. +Capture authorized funds to the receiver. The operator contract decides when and how much to capture based on configured conditions. ### Void Return funds to payer before capture. Used for full refunds during the escrow period. @@ -197,8 +197,8 @@ Safety valve for payer. If authorization expires without capture, payer can recl ### Operator Smart contract that controls capture/void logic. Different operators enable different payment patterns: -- **Time-locked**: Release after period expires -- **Arbiter-controlled**: Third party decides release +- **Time-locked**: Capture after period expires +- **Arbiter-controlled**: Third party decides capture - **Usage-based**: Capture proportional to consumption - **Immediate**: Behaves like `exact` scheme From a8cb1397fa7fad70778062502c6bba2e27e47448 Mon Sep 17 00:00:00 2001 From: A1igator Date: Tue, 12 May 2026 00:20:19 -0700 Subject: [PATCH 26/37] =?UTF-8?q?docs:=20redo=200ce04f3=20=E2=80=94=20dele?= =?UTF-8?q?te=20nav=20orphans=20instead=20of=20exposing=20them?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous version of this commit added 23 orphan files to docs.json. The right move is to drop the files; the Hooks/Conditions reframe (the other half of 0ce04f3) stays. Deleted (were never in nav; content was either duplicate of in-nav pages or experimental): - ai-tools/{cursor,claude-code,windsurf}.mdx - sdk/{arbiter,merchant,payer}.mdx (role hubs duplicating quickstarts) - sdk/{concepts,limitations}.mdx - sdk/arbiter/{ai-integration,batch-operations,decision-submission, quickstart,registry,subscriptions}.mdx - sdk/client/{escrow-management,payment-queries,quickstart, refund-operations,subscriptions}.mdx - sdk/facilitator/getting-started.mdx - sdk/helpers/erc8004.mdx - sdk/merchant/{payment-operations,subscriptions}.mdx docs.json: SDK tab reverted to the 5e6850c 4-group layout (Getting Started / Marketplace / Delivery Protection / Reference). Contracts tab keeps the Hooks group (no change). Dangling references to deleted pages fixed in: - contracts/{architecture,examples}.mdx - index.mdx, sdk/cli.mdx, sdk/create-client.mdx, sdk/deploy-operator.mdx - sdk/examples.mdx, sdk/delivery-arbiter.mdx, sdk/overview.mdx - sdk/helpers/forward-to-arbiter.mdx - sdk/merchant/{getting-started,refund-handling}.mdx Verified: npx mintlify@latest broken-links passes. Co-Authored-By: Claude Opus 4.7 (1M context) --- ai-tools/claude-code.mdx | 76 ----- ai-tools/cursor.mdx | 420 ---------------------------- ai-tools/windsurf.mdx | 96 ------- contracts/architecture.mdx | 13 +- contracts/examples.mdx | 2 +- docs.json | 63 +---- index.mdx | 6 - sdk/arbiter.mdx | 175 ------------ sdk/arbiter/ai-integration.mdx | 156 ----------- sdk/arbiter/batch-operations.mdx | 119 -------- sdk/arbiter/decision-submission.mdx | 210 -------------- sdk/arbiter/quickstart.mdx | 61 ---- sdk/arbiter/registry.mdx | 158 ----------- sdk/arbiter/subscriptions.mdx | 98 ------- sdk/cli.mdx | 3 - sdk/client/escrow-management.mdx | 177 ------------ sdk/client/payment-queries.mdx | 109 -------- sdk/client/quickstart.mdx | 66 ----- sdk/client/refund-operations.mdx | 138 --------- sdk/client/subscriptions.mdx | 109 -------- sdk/concepts.mdx | 213 -------------- sdk/create-client.mdx | 5 +- sdk/delivery-arbiter.mdx | 2 +- sdk/deploy-operator.mdx | 3 - sdk/examples.mdx | 3 - sdk/facilitator/getting-started.mdx | 146 ---------- sdk/helpers/erc8004.mdx | 156 ----------- sdk/helpers/forward-to-arbiter.mdx | 3 - sdk/limitations.mdx | 72 ----- sdk/merchant.mdx | 121 -------- sdk/merchant/getting-started.mdx | 5 +- sdk/merchant/payment-operations.mdx | 201 ------------- sdk/merchant/refund-handling.mdx | 6 - sdk/merchant/subscriptions.mdx | 133 --------- sdk/overview.mdx | 8 +- sdk/payer.mdx | 150 ---------- 36 files changed, 15 insertions(+), 3467 deletions(-) delete mode 100644 ai-tools/claude-code.mdx delete mode 100644 ai-tools/cursor.mdx delete mode 100644 ai-tools/windsurf.mdx delete mode 100644 sdk/arbiter.mdx delete mode 100644 sdk/arbiter/ai-integration.mdx delete mode 100644 sdk/arbiter/batch-operations.mdx delete mode 100644 sdk/arbiter/decision-submission.mdx delete mode 100644 sdk/arbiter/quickstart.mdx delete mode 100644 sdk/arbiter/registry.mdx delete mode 100644 sdk/arbiter/subscriptions.mdx delete mode 100644 sdk/client/escrow-management.mdx delete mode 100644 sdk/client/payment-queries.mdx delete mode 100644 sdk/client/quickstart.mdx delete mode 100644 sdk/client/refund-operations.mdx delete mode 100644 sdk/client/subscriptions.mdx delete mode 100644 sdk/concepts.mdx delete mode 100644 sdk/facilitator/getting-started.mdx delete mode 100644 sdk/helpers/erc8004.mdx delete mode 100644 sdk/limitations.mdx delete mode 100644 sdk/merchant.mdx delete mode 100644 sdk/merchant/payment-operations.mdx delete mode 100644 sdk/merchant/subscriptions.mdx delete mode 100644 sdk/payer.mdx diff --git a/ai-tools/claude-code.mdx b/ai-tools/claude-code.mdx deleted file mode 100644 index bdc4e04..0000000 --- a/ai-tools/claude-code.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -title: "Claude Code setup" -description: "Configure Claude Code for your documentation workflow" -icon: "asterisk" ---- - -Claude Code is Anthropic's official CLI tool. This guide will help you set up Claude Code to help you write and maintain your documentation. - -## Prerequisites - -- Active Claude subscription (Pro, Max, or API access) - -## Setup - -1. Install Claude Code globally: - - ```bash - npm install -g @anthropic-ai/claude-code -``` - -2. Navigate to your docs directory. -3. (Optional) Add the `CLAUDE.md` file below to your project. -4. Run `claude` to start. - -## Create `CLAUDE.md` - -Create a `CLAUDE.md` file at the root of your documentation repository to train Claude Code on your specific documentation standards: - -````markdown -# Mintlify documentation - -## Working relationship -- You can push back on ideas-this can lead to better documentation. Cite sources and explain your reasoning when you do so -- ALWAYS ask for clarification rather than making assumptions -- NEVER lie, guess, or make up information - -## Project context -- Format: MDX files with YAML frontmatter -- Config: docs.json for navigation, theme, settings -- Components: Mintlify components - -## Content strategy -- Document just enough for user success - not too much, not too little -- Prioritize accuracy and usability of information -- Make content evergreen when possible -- Search for existing information before adding new content. Avoid duplication unless it is done for a strategic reason -- Check existing patterns for consistency -- Start by making the smallest reasonable changes - -## Frontmatter requirements for pages -- title: Clear, descriptive page title -- description: Concise summary for SEO/navigation - -## Writing standards -- Second-person voice ("you") -- Prerequisites at start of procedural content -- Test all code examples before publishing -- Match style and formatting of existing pages -- Include both basic and advanced use cases -- Language tags on all code blocks -- Alt text on all images -- Relative paths for internal links - -## Git workflow -- NEVER use --no-verify when committing -- Ask how to handle uncommitted changes before starting -- Create a new branch when no clear branch exists for changes -- Commit frequently throughout development -- NEVER skip or disable pre-commit hooks - -## Do not -- Skip frontmatter on any MDX file -- Use absolute URLs for internal links -- Include untested code examples -- Make assumptions - always ask for clarification -```` diff --git a/ai-tools/cursor.mdx b/ai-tools/cursor.mdx deleted file mode 100644 index 81793af..0000000 --- a/ai-tools/cursor.mdx +++ /dev/null @@ -1,420 +0,0 @@ ---- -title: "Cursor setup" -description: "Configure Cursor for your documentation workflow" -icon: "arrow-pointer" ---- - -Use Cursor to help write and maintain your documentation. This guide shows how to configure Cursor for better results on technical writing tasks and using Mintlify components. - -## Prerequisites - -- Cursor editor installed -- Access to your documentation repository - -## Project rules - -Create project rules that all team members can use. In your documentation repository root: - -```bash -mkdir -p .cursor -``` - -Create `.cursor/rules.md`: - -````markdown -# Mintlify technical writing rule - -You are an AI writing assistant specialized in creating exceptional technical documentation using Mintlify components and following industry-leading technical writing practices. - -## Core writing principles - -### Language and style requirements - -- Use clear, direct language appropriate for technical audiences -- Write in second person ("you") for instructions and procedures -- Use active voice over passive voice -- Employ present tense for current states, future tense for outcomes -- Avoid jargon unless necessary and define terms when first used -- Maintain consistent terminology throughout all documentation -- Keep sentences concise while providing necessary context -- Use parallel structure in lists, headings, and procedures - -### Content organization standards - -- Lead with the most important information (inverted pyramid structure) -- Use progressive disclosure: basic concepts before advanced ones -- Break complex procedures into numbered steps -- Include prerequisites and context before instructions -- Provide expected outcomes for each major step -- Use descriptive, keyword-rich headings for navigation and SEO -- Group related information logically with clear section breaks - -### User-centered approach - -- Focus on user goals and outcomes rather than system features -- Anticipate common questions and address them proactively -- Include troubleshooting for likely failure points -- Write for scannability with clear headings, lists, and white space -- Include verification steps to confirm success - -## Mintlify component reference - -### Callout components - -#### Note - Additional helpful information - - -Supplementary information that supports the main content without interrupting flow - - -#### Tip - Best practices and pro tips - - -Expert advice, shortcuts, or best practices that enhance user success - - -#### Warning - Important cautions - - -Critical information about potential issues, breaking changes, or destructive actions - - -#### Info - Neutral contextual information - - -Background information, context, or neutral announcements - - -#### Check - Success confirmations - - -Positive confirmations, successful completions, or achievement indicators - - -### Code components - -#### Single code block - -Example of a single code block: - -```javascript config.js -const apiConfig = { - baseURL: 'https://api.example.com', - timeout: 5000, - headers: { - 'Authorization': `Bearer ${process.env.API_TOKEN}` - } -}; -``` - -#### Code group with multiple languages - -Example of a code group: - - -```javascript Node.js -const response = await fetch('/api/endpoint', { - headers: { Authorization: `Bearer ${apiKey}` } -}); -``` - -```python Python -import requests -response = requests.get('/api/endpoint', - headers={'Authorization': f'Bearer {api_key}'}) -``` - -```curl cURL -curl -X GET '/api/endpoint' \ - -H 'Authorization: Bearer YOUR_API_KEY' -``` - - -#### Request/response examples - -Example of request/response documentation: - - -```bash cURL -curl -X POST 'https://api.example.com/users' \ - -H 'Content-Type: application/json' \ - -d '{"name": "John Doe", "email": "john@example.com"}' -``` - - - -```json Success -{ - "id": "user_123", - "name": "John Doe", - "email": "john@example.com", - "created_at": "2024-01-15T10:30:00Z" -} -``` - - -### Structural components - -#### Steps for procedures - -Example of step-by-step instructions: - - - - Run `npm install` to install required packages. - - - Verify installation by running `npm list`. - - - - - Create a `.env` file with your API credentials. - - ```bash - API_KEY=your_api_key_here - ``` - - - Never commit API keys to version control. - - - - -#### Tabs for alternative content - -Example of tabbed content: - - - - ```bash - brew install node - npm install -g package-name - ``` - - - - ```powershell - choco install nodejs - npm install -g package-name - ``` - - - - ```bash - sudo apt install nodejs npm - npm install -g package-name - ``` - - - -#### Accordions for collapsible content - -Example of accordion groups: - - - - - **Firewall blocking**: Ensure ports 80 and 443 are open - - **Proxy configuration**: Set HTTP_PROXY environment variable - - **DNS resolution**: Try using 8.8.8.8 as DNS server - - - - ```javascript - const config = { - performance: { cache: true, timeout: 30000 }, - security: { encryption: 'AES-256' } - }; - ``` - - - -### Cards and columns for emphasizing information - -Example of cards and card groups: - - -Complete walkthrough from installation to your first API call in under 10 minutes. - - - - - Learn how to set up and configure the SDK client for payments. - - - - Understand current limitations and best practices for usage. - - - -### API documentation components - -#### Parameter fields - -Example of parameter documentation: - - -Unique identifier for the user. Must be a valid UUID v4 format. - - - -User's email address. Must be valid and unique within the system. - - - -Maximum number of results to return. Range: 1-100. - - - -Bearer token for API authentication. Format: `Bearer YOUR_API_KEY` - - -#### Response fields - -Example of response field documentation: - - -Unique identifier assigned to the newly created user. - - - -ISO 8601 formatted timestamp of when the user was created. - - - -List of permission strings assigned to this user. - - -#### Expandable nested fields - -Example of nested field documentation: - - -Complete user object with all associated data. - - - - User profile information including personal details. - - - - User's first name as entered during registration. - - - - URL to user's profile picture. Returns null if no avatar is set. - - - - - - -### Media and advanced components - -#### Frames for images - -Wrap all images in frames: - - -Main dashboard showing analytics overview - - - -Analytics dashboard with charts - - -#### Videos - -Use the HTML video element for self-hosted video content: - - - -Embed YouTube videos using iframe elements: - - - -#### Tooltips - -Example of tooltip usage: - - -API - - -#### Updates - -Use updates for changelogs: - - -## New features -- Added bulk user import functionality -- Improved error messages with actionable suggestions - -## Bug fixes -- Fixed pagination issue with large datasets -- Resolved authentication timeout problems - - -## Required page structure - -Every documentation page must begin with YAML frontmatter: - -```yaml ---- -title: "Clear, specific, keyword-rich title" -description: "Concise description explaining page purpose and value" ---- -``` - -## Content quality standards - -### Code examples requirements - -- Always include complete, runnable examples that users can copy and execute -- Show proper error handling and edge case management -- Use realistic data instead of placeholder values -- Include expected outputs and results for verification -- Test all code examples thoroughly before publishing -- Specify language and include filename when relevant -- Add explanatory comments for complex logic -- Never include real API keys or secrets in code examples - -### API documentation requirements - -- Document all parameters including optional ones with clear descriptions -- Show both success and error response examples with realistic data -- Include rate limiting information with specific limits -- Provide authentication examples showing proper format -- Explain all HTTP status codes and error handling -- Cover complete request/response cycles - -### Accessibility requirements - -- Include descriptive alt text for all images and diagrams -- Use specific, actionable link text instead of "click here" -- Ensure proper heading hierarchy starting with H2 -- Provide keyboard navigation considerations -- Use sufficient color contrast in examples and visuals -- Structure content for easy scanning with headers and lists - -## Component selection logic - -- Use **Steps** for procedures and sequential instructions -- Use **Tabs** for platform-specific content or alternative approaches -- Use **CodeGroup** when showing the same concept in multiple programming languages -- Use **Accordions** for progressive disclosure of information -- Use **RequestExample/ResponseExample** specifically for API endpoint documentation -- Use **ParamField** for API parameters, **ResponseField** for API responses -- Use **Expandable** for nested object properties or hierarchical information -```` diff --git a/ai-tools/windsurf.mdx b/ai-tools/windsurf.mdx deleted file mode 100644 index fce12bf..0000000 --- a/ai-tools/windsurf.mdx +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: "Windsurf setup" -description: "Configure Windsurf for your documentation workflow" -icon: "water" ---- - -Configure Windsurf's Cascade AI assistant to help you write and maintain documentation. This guide shows how to set up Windsurf specifically for your Mintlify documentation workflow. - -## Prerequisites - -- Windsurf editor installed -- Access to your documentation repository - -## Workspace rules - -Create workspace rules that provide Windsurf with context about your documentation project and standards. - -Create `.windsurf/rules.md` in your project root: - -````markdown -# Mintlify technical writing rule - -## Project context - -- This is a documentation project on the Mintlify platform -- We use MDX files with YAML frontmatter -- Navigation is configured in `docs.json` -- We follow technical writing best practices - -## Writing standards - -- Use second person ("you") for instructions -- Write in active voice and present tense -- Start procedures with prerequisites -- Include expected outcomes for major steps -- Use descriptive, keyword-rich headings -- Keep sentences concise but informative - -## Required page structure - -Every page must start with frontmatter: - -```yaml ---- -title: "Clear, specific title" -description: "Concise description for SEO and navigation" ---- -``` - -## Mintlify components - -### Callouts - -- `` for helpful supplementary information -- `` for important cautions and breaking changes -- `` for best practices and expert advice -- `` for neutral contextual information -- `` for success confirmations - -### Code examples - -- When appropriate, include complete, runnable examples -- Use `` for multiple language examples -- Specify language tags on all code blocks -- Include realistic data, not placeholders -- Use `` and `` for API docs - -### Procedures - -- Use `` component for sequential instructions -- Include verification steps with `` components when relevant -- Break complex procedures into smaller steps - -### Content organization - -- Use `` for platform-specific content -- Use `` for progressive disclosure -- Use `` and `` for highlighting content -- Wrap images in `` components with descriptive alt text - -## API documentation requirements - -- Document all parameters with `` -- Show response structure with `` -- Include both success and error examples -- Use `` for nested object properties -- Always include authentication examples - -## Quality standards - -- Test all code examples before publishing -- Use relative paths for internal links -- Include alt text for all images -- Ensure proper heading hierarchy (start with h2) -- Check existing patterns for consistency -```` diff --git a/contracts/architecture.mdx b/contracts/architecture.mdx index e0ad898..44bd5f5 100644 --- a/contracts/architecture.mdx +++ b/contracts/architecture.mdx @@ -86,11 +86,11 @@ For additional visual diagrams, see the [x402r-contracts repository](https://git 1. **Payer** calls `refundRequest.requestRefund(paymentInfo, amount)` 2. **RefundRequest** creates request with status `Pending` 3. **Designated address** (e.g., arbiter, DAO multisig) reviews dispute -4. **Designated address** calls `operator.void(paymentInfo)`; the `VOID_POST_ACTION_HOOK` (RefundRequest) auto-flips status to `Approved` -6. **Operator** checks `VOID_PRE_ACTION_CONDITION` (configured per operator) -7. **Operator** calls `escrow.void()` to return all escrowed funds to payer -8. **Operator** calls `VOID_POST_ACTION_HOOK` -9. Funds transferred back to payer +4. **Designated address** calls `operator.void(paymentInfo)` +5. **Operator** checks `VOID_PRE_ACTION_CONDITION` (configured per operator) +6. **Operator** calls `escrow.void()` to return all escrowed funds to payer +7. **Operator** calls `VOID_POST_ACTION_HOOK` (RefundRequest auto-flips status to `Approved`) +8. Funds transferred back to payer Refund conditions are configurable. Can be arbiter-only (marketplace), receiver-allowed (return policy), DAO-controlled (governance), or disabled (subscriptions). @@ -318,7 +318,4 @@ These events enable off-chain monitoring and indexing. Deploy a PaymentOperator using the SDK. - - Understand how the SDK maps to contract architecture. - diff --git a/contracts/examples.mdx b/contracts/examples.mdx index 422ee30..96ac6e5 100644 --- a/contracts/examples.mdx +++ b/contracts/examples.mdx @@ -5,7 +5,7 @@ icon: "code" --- -The configuration examples below use simplified pseudo-code (e.g., `new StaticAddressCondition(...)`, `new AndCondition(...)`) to illustrate the logical composition of conditions. In practice, deploy conditions via their respective [factory contracts](/contracts/factories) using viem. See the [SDK quickstart](/sdk/client/quickstart) for executable code. +The configuration examples below use simplified pseudo-code (e.g., `new StaticAddressCondition(...)`, `new AndCondition(...)`) to illustrate the logical composition of conditions. In practice, deploy conditions via their respective [factory contracts](/contracts/factories) using viem. See the [Deploy an operator guide](/sdk/deploy-operator) for executable code. ## Example 1: Standard E-Commerce with 7-Day Escrow diff --git a/docs.json b/docs.json index ad21f33..51d4627 100644 --- a/docs.json +++ b/docs.json @@ -95,62 +95,16 @@ "group": "Getting Started", "pages": [ "sdk/overview", - "sdk/create-client", "sdk/deploy-operator" ] }, { - "group": "Concepts", + "group": "Marketplace", "pages": [ - "sdk/concepts", - "sdk/limitations" - ] - }, - { - "group": "Merchant", - "pages": [ - "sdk/merchant", "sdk/merchant/getting-started", - "sdk/merchant/quickstart", - "sdk/merchant/payment-operations", - "sdk/merchant/refund-handling", - "sdk/merchant/subscriptions" - ] - }, - { - "group": "Payer (Client)", - "pages": [ - "sdk/payer", - "sdk/client/quickstart", - "sdk/client/escrow-management", - "sdk/client/payment-queries", - "sdk/client/refund-operations", - "sdk/client/subscriptions" - ] - }, - { - "group": "Arbiter", - "pages": [ - "sdk/arbiter", - "sdk/arbiter/quickstart", - "sdk/arbiter/decision-submission", - "sdk/arbiter/registry", - "sdk/arbiter/ai-integration", - "sdk/arbiter/batch-operations", - "sdk/arbiter/subscriptions" - ] - }, - { - "group": "Facilitator", - "pages": [ - "sdk/facilitator/getting-started" - ] - }, - { - "group": "Helpers", - "pages": [ "sdk/helpers/forward-to-arbiter", - "sdk/helpers/erc8004" + "sdk/merchant/quickstart", + "sdk/merchant/refund-handling" ] }, { @@ -165,16 +119,9 @@ "group": "Reference", "pages": [ "sdk/cli", + "sdk/create-client", "sdk/examples" ] - }, - { - "group": "AI Tools", - "pages": [ - "ai-tools/cursor", - "ai-tools/claude-code", - "ai-tools/windsurf" - ] } ] } @@ -235,4 +182,4 @@ "destination": "/contracts/hooks/:slug" } ] -} +} \ No newline at end of file diff --git a/index.mdx b/index.mdx index 8083b09..7d9f63b 100644 --- a/index.mdx +++ b/index.mdx @@ -41,15 +41,9 @@ sequenceDiagram ## Who Is This For? - - Request refunds, freeze suspicious payments, and track payment state. - Capture funds, process refunds, and manage escrow periods. - - Resolve disputes and approve/deny refund requests. - ## Get Started diff --git a/sdk/arbiter.mdx b/sdk/arbiter.mdx deleted file mode 100644 index d04f5d3..0000000 --- a/sdk/arbiter.mdx +++ /dev/null @@ -1,175 +0,0 @@ ---- -title: "Arbiter Guide" -description: "Review refund requests, approve or deny refunds, and distribute fees." -icon: "scale-balanced" ---- - -### Prerequisites - -* A wallet with ETH on Base Sepolia for gas ([faucet](https://www.alchemy.com/faucets/base-sepolia)) -* Node.js 18+ and npm -* A marketplace operator where your address is configured as the arbiter (see [Deploy an Operator](/sdk/deploy-operator)) - - -There are pre-configured [arbiter examples](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/arbiter) and a full [dispute resolution scenario](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/scenarios/dispute-resolution.ts) in the SDK repo. - - -### 1. Install Dependencies - - -```bash npm -npm install @x402r/sdk -``` -```bash pnpm -pnpm add @x402r/sdk -``` -```bash bun -bun add @x402r/sdk -``` - - -### 2. Create an Arbiter Client - -```typescript -import { createPublicClient, createWalletClient, http } from 'viem' -import { baseSepolia } from 'viem/chains' -import { privateKeyToAccount } from 'viem/accounts' -import { createArbiterClient } from '@x402r/sdk' - -const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) - -const arbiter = createArbiterClient({ - publicClient: createPublicClient({ chain: baseSepolia, transport: http() }), - walletClient: createWalletClient({ - account, - chain: baseSepolia, - transport: http(), - }), - operatorAddress: '0x...', // from deploy result - refundRequestAddress: '0x...', // from deploy result - refundRequestEvidenceAddress: '0x...', // from deploy result - escrowPeriodAddress: '0x...', - freezeAddress: '0x...', -}) -``` - -### 3. Check for Pending Refund Requests - -```typescript -import type { PaymentInfo } from '@x402r/sdk' - -const paymentInfo: PaymentInfo = { /* ... */ } - -const hasRefund = await arbiter.refund?.has(paymentInfo) -if (hasRefund) { - const request = await arbiter.refund?.get(paymentInfo) - console.log('Amount:', request?.amount) - console.log('Status:', request?.status) // 0 = Pending, 1 = Approved, 2 = Denied, 3 = Cancelled, 4 = Refused -} -``` - -To list all refund requests for your operator: - -```typescript -const requests = await arbiter.refund?.getOperatorRequests( - arbiter.config.operatorAddress, - 0n, // offset - 10n, // count -) -console.log('Pending requests:', requests) -``` - -### 4. Review Evidence - -Both payers and merchants can submit evidence as IPFS CIDs. Read all entries before making a decision: - -```typescript -const count = await arbiter.evidence?.count(paymentInfo) -console.log('Evidence entries:', count) - -const batch = await arbiter.evidence?.getBatch(paymentInfo, 0n, count!) - -for (const entry of batch!.entries) { - console.log('CID:', entry.cid) - console.log('Submitter:', entry.submitter) - console.log('Timestamp:', entry.timestamp) -} -``` - -### 5. Approve a Refund - - -`voidPayment()` auto-approves the pending RefundRequest. There is no undo. - - -```typescript -const tx = await arbiter.payment.voidPayment(paymentInfo) -console.log('Refund approved:', tx) - -// Verify -const approved = await arbiter.refund?.get(paymentInfo) -console.log('Approved amount:', approved?.approvedAmount) -console.log('Status:', approved?.status) // 1 = Approved -``` - -### 6. Deny a Refund Request - - -`deny()` = you reviewed and rejected the claim. `refuse()` = you decline to rule (e.g. conflict of interest). - - -```typescript -const denyTx = await arbiter.refund?.deny(paymentInfo) -console.log('Refund denied:', denyTx) -``` - -Or decline to rule entirely: - -```typescript -const refuseTx = await arbiter.refund?.refuse(paymentInfo) -console.log('Declined to rule:', refuseTx) -``` - -### 7. Unfreeze a Payment - -If the payer froze the payment during the dispute, unfreeze it after resolution: - -```typescript -const frozen = await arbiter.freeze?.isFrozen(paymentInfo) -if (frozen) { - const tx = await arbiter.freeze?.unfreeze(paymentInfo) - console.log('Unfrozen:', tx) -} -``` - -### 8. Distribute Accumulated Fees - -Protocol fees accumulate on the operator when payments are released: - -```typescript -import { getChainConfig } from '@x402r/sdk' - -const config = getChainConfig(84532) - -const fees = await arbiter.operator.getAccumulatedProtocolFees(config.usdc) -console.log('Accumulated fees:', fees) - -if (fees > 0n) { - const tx = await arbiter.operator.distributeFees(config.usdc) - console.log('Fees distributed:', tx) -} -``` - -## Next Steps - - - - Automated evaluation for every transaction. - - - How your address gets configured as arbiter on an operator. - - - Full dispute resolution scenario end-to-end. - - diff --git a/sdk/arbiter/ai-integration.mdx b/sdk/arbiter/ai-integration.mdx deleted file mode 100644 index 8e19cd7..0000000 --- a/sdk/arbiter/ai-integration.mdx +++ /dev/null @@ -1,156 +0,0 @@ ---- -title: "AI integration" -description: "Automate dispute resolution with AI using the arbiter client" -icon: "robot" ---- - -You can build AI-powered dispute resolution on top of the arbiter client by combining the `watch`, `refund`, `evidence`, and `payment` action groups. Plug in any AI model (LLMs, rule engines, or hybrid systems) to automatically evaluate and decide on refund requests. - -## Watch and auto-evaluate pattern - -The most common pattern combines `watch.onRefundRequest` with your evaluation logic to automatically process incoming refund requests: - -```typescript -import { createArbiterClient, RefundRequestStatus } from '@x402r/sdk' -import type { PaymentInfo } from '@x402r/sdk' - -const arbiter = createArbiterClient({ - publicClient, - walletClient, - operatorAddress: '0x...', - refundRequestAddress: '0x...', - refundRequestEvidenceAddress: '0x...', - escrowPeriodAddress: '0x...', -}) - -// Watch for new refund requests -const unwatch = arbiter.watch.onRefundRequest(async (logs) => { - for (const log of logs) { - console.log('New refund event:', log.eventName) - - // Look up the full payment info from your database or the refund contract - const paymentInfo = await lookupPaymentInfo(log) - - // Evaluate the case - const decision = await evaluateWithAI(arbiter, paymentInfo) - - if (decision.approve && decision.confidence >= 0.9) { - const request = await arbiter.refund?.get(paymentInfo) - if (request && request.status === RefundRequestStatus.Pending) { - await arbiter.payment.voidPayment(paymentInfo) - console.log('Auto-approved refund') - } - } else if (!decision.approve && decision.confidence >= 0.9) { - await arbiter.refund?.deny(paymentInfo) - console.log('Auto-denied refund') - } else { - console.log('Low confidence, queuing for manual review') - } - } -}) - -// Graceful shutdown -process.on('SIGINT', () => { - unwatch() - process.exit() -}) -``` - -## Building the evaluation context - -Gather all available on-chain data before sending to your AI model: - -```typescript -async function buildEvaluationContext( - arbiter: ReturnType, - paymentInfo: PaymentInfo, -) { - // Get payment state and amounts - const state = await arbiter.payment.getState(paymentInfo) - const amounts = await arbiter.payment.getAmounts(paymentInfo) - - // Get the refund request - const request = await arbiter.refund?.get(paymentInfo) - - // Check escrow timing - const inEscrow = await arbiter.escrow?.isDuringEscrow(paymentInfo) - - // Get evidence if available - const evidenceCount = await arbiter.evidence?.count(paymentInfo) - let evidence: Array<{ cid: string; submitter: string }> = [] - - if (evidenceCount && evidenceCount > 0n) { - const batch = await arbiter.evidence?.getBatch(paymentInfo, 0n, evidenceCount) - evidence = (batch?.entries ?? []).map((e) => ({ - cid: e.cid, - submitter: e.submitter, - })) - } - - return { - paymentInfo, - state, - amounts, - request, - inEscrow, - evidence, - } -} -``` - -## Evaluation patterns - -Three common approaches for the evaluation function: - -- **LLM-based**: send the structured context to an LLM (GPT-4, Claude, etc.) with a system prompt that outputs JSON `{decision, reasoning, confidence}`. Sanitize inputs to prevent prompt injection. -- **Rule-based**: apply deterministic rules (amount thresholds, blocklists) for predictable, high-confidence decisions. Best for clear-cut cases. -- **Hybrid**: apply hard rules first; if no rule matches with high confidence, fall back to LLM evaluation. Deny and flag for manual review when confidence is low. - -```typescript -interface DecisionResult { - approve: boolean - reasoning: string - confidence: number -} - -async function evaluateWithAI( - arbiter: ReturnType, - paymentInfo: PaymentInfo, -): Promise { - const context = await buildEvaluationContext(arbiter, paymentInfo) - - // Rule-based: auto-approve small amounts - if (context.request && context.request.amount < 5_000_000n) { - return { approve: true, reasoning: 'Small amount auto-approved', confidence: 1.0 } - } - - // LLM fallback for larger amounts - const llmResult = await callYourLLM(context) - return { - approve: llmResult.decision === 'approve', - reasoning: llmResult.explanation, - confidence: llmResult.confidence, - } -} -``` - - -AI-powered dispute resolution handles financial decisions. Always implement prompt injection protection, input validation, and confidence thresholds before deploying to production. - - -## Next steps - - - - Watch for new cases to evaluate in real-time. - - - Process queued AI decisions in batches. - - - Approve/deny individual cases and execute refunds. - - - Register your arbiter for on-chain discovery. - - diff --git a/sdk/arbiter/batch-operations.mdx b/sdk/arbiter/batch-operations.mdx deleted file mode 100644 index 93bcece..0000000 --- a/sdk/arbiter/batch-operations.mdx +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: "Batch operations" -description: "Process multiple refund decisions efficiently as an arbiter" -icon: "layer-group" ---- - -The arbiter client can process multiple refund requests by iterating over cases and calling refund/payment methods individually. There is no batch method, but you can build batch workflows on top of the action groups. - - -Each refund decision is a separate on-chain transaction. If one fails mid-batch, previously processed items are not rolled back. Design your error handling accordingly. - - -## Batch approve and execute - -Fetch pending cases, evaluate each one, then approve or deny: - -```typescript -import { createArbiterClient, RefundRequestStatus } from '@x402r/sdk' -import type { PaymentInfo } from '@x402r/sdk' - -async function batchProcess( - arbiter: ReturnType, - paymentInfos: PaymentInfo[], -) { - const approved: string[] = [] - const denied: string[] = [] - - for (const paymentInfo of paymentInfos) { - const request = await arbiter.refund?.get(paymentInfo) - if (!request || request.status !== RefundRequestStatus.Pending) continue - - const shouldApprove = request.amount < 10_000_000n // Auto-approve < 10 USDC - - if (shouldApprove) { - const tx = await arbiter.payment.voidPayment(paymentInfo) - approved.push(tx) - } else { - const tx = await arbiter.refund?.deny(paymentInfo) - if (tx) denied.push(tx) - } - } - - console.log('Approved:', approved.length, 'Denied:', denied.length) - return { approved, denied } -} -``` - -## Triage from operator requests - -Fetch all pending cases for your operator and triage them: - -```typescript -async function triageOperatorCases( - arbiter: ReturnType, - lookupPaymentInfo: (hash: `0x${string}`) => Promise, -) { - // Step 1: Fetch all refund requests for this operator - const requests = await arbiter.refund?.getOperatorRequests( - arbiter.config.operatorAddress, - 0n, - 100n, - ) - - // Step 2: Filter to pending only - const pending = (requests ?? []).filter( - (r) => r.status === RefundRequestStatus.Pending, - ) - console.log('Pending cases:', pending.length) - - // Step 3: Process each - for (const request of pending) { - const paymentInfo = await lookupPaymentInfo(request.paymentInfoHash) - - // Review evidence if available - const evidenceCount = await arbiter.evidence?.count(paymentInfo) - if (evidenceCount && evidenceCount > 0n) { - const batch = await arbiter.evidence?.getBatch(paymentInfo, 0n, evidenceCount) - console.log('Evidence entries:', batch?.entries.length) - } - - // Make decision - if (request.amount < 10_000_000n) { - await arbiter.payment.voidPayment(paymentInfo) - } else { - await arbiter.refund?.deny(paymentInfo) - } - } -} -``` - -## Performance considerations - - -Each decision is a separate on-chain transaction. Gas costs scale linearly with the number of items. Plan batch sizes around your RPC provider's rate limits. - - -| Factor | Detail | -|--------|--------| -| **Transaction ordering** | Process items sequentially to ensure correct nonce ordering. | -| **Gas costs** | Each item is a separate transaction. Batch processing saves SDK overhead, not gas. | -| **Partial failures** | If one transaction fails, previous ones remain on-chain. Handle partial failures in your logic. | -| **Rate limiting** | Large batches may hit RPC rate limits. Consider adding delays for 50+ item batches. | - -## Next steps - - - - Automate decisions with AI evaluation hooks. - - - Watch for new cases in real-time. - - - Individual approve/deny methods and refund execution. - - - Review the complete arbiter setup guide. - - diff --git a/sdk/arbiter/decision-submission.mdx b/sdk/arbiter/decision-submission.mdx deleted file mode 100644 index 58a299b..0000000 --- a/sdk/arbiter/decision-submission.mdx +++ /dev/null @@ -1,210 +0,0 @@ ---- -title: "Decision submission" -description: "Submit decisions on refund requests and execute refunds as an arbiter" -icon: "gavel" ---- - -As an arbiter, you review pending refund requests, then either execute a refund via `payment.voidPayment` / `payment.refund` (which auto-approves the request), or terminally `refund.deny` / `refund.refuse` it. Evidence reads sit on `evidence.*`. - -## Approve a refund (voidPayment) - -To approve and execute a refund in one step, call `payment.voidPayment()`. This auto-approves the pending RefundRequest and transfers funds back to the payer. - -```typescript -const amounts = await arbiter.payment.getAmounts(paymentInfo) -const tx = await arbiter.payment.voidPayment(paymentInfo) -console.log('Refund approved and executed:', tx) -``` - - -`payment.voidPayment()` auto-approves the pending RefundRequest. There is no undo. - - -## Deny a refund request - -Deny a pending refund request: - -```typescript -const tx = await arbiter.refund?.deny(paymentInfo) -console.log('Refund denied:', tx) -``` - -## Refuse to rule - -Decline to rule on a dispute (e.g., conflict of interest): - -```typescript -const tx = await arbiter.refund?.refuse(paymentInfo) -console.log('Declined to rule:', tx) -``` - -## Check if a refund request exists - -```typescript -const hasRequest = await arbiter.refund?.has(paymentInfo) - -if (!hasRequest) { - console.log('No refund request found for this payment') - return -} -``` - -## Get refund request data - -Retrieve the full refund request data, including amount and status: - -```typescript -import { RefundRequestStatus } from '@x402r/sdk' - -const request = await arbiter.refund?.get(paymentInfo) - -console.log('Payment hash:', request?.paymentInfoHash) -console.log('Refund amount:', request?.amount) -console.log('Approved amount:', request?.approvedAmount) -console.log('Status:', request?.status) -``` - -The `RefundRequestData` type contains: - -```typescript -interface RefundRequestData { - paymentInfoHash: `0x${string}` - amount: bigint - approvedAmount: bigint - status: RefundRequestStatus -} -``` - -## Get refund request status - -```typescript -import { RefundRequestStatus } from '@x402r/sdk' - -const status = await arbiter.refund?.getStatus(paymentInfo) - -switch (status) { - case RefundRequestStatus.Pending: - console.log('Awaiting decision') - break - case RefundRequestStatus.Approved: - console.log('Already approved') - break - case RefundRequestStatus.Denied: - console.log('Already denied') - break - case RefundRequestStatus.Cancelled: - console.log('Cancelled by payer') - break - case RefundRequestStatus.Refused: - console.log('Arbiter declined to rule') - break -} -``` - -## List refund requests (paginated) - -Retrieve paginated refund requests for an operator: - -```typescript -const requests = await arbiter.refund?.getOperatorRequests( - arbiter.config.operatorAddress, - 0n, // offset - 50n, // count -) - -for (const request of requests ?? []) { - console.log('Amount:', request.amount, 'Status:', request.status) -} -``` - -You can also query by payer or receiver: - -```typescript -const payerRequests = await arbiter.refund?.getPayerRequests(payerAddress, 0n, 10n) -const receiverRequests = await arbiter.refund?.getReceiverRequests(receiverAddress, 0n, 10n) -``` - -## Check if a payment is frozen - -```typescript -const frozen = await arbiter.freeze?.isFrozen(paymentInfo) - -if (frozen) { - console.log('Payment is frozen, dispute in progress') -} -``` - -## Complete decision workflow - -This example shows the full arbiter workflow: fetching pending cases, reviewing evidence, making a decision, and executing the refund. - -```typescript -import { createArbiterClient, RefundRequestStatus } from '@x402r/sdk' -import type { PaymentInfo } from '@x402r/sdk' - -async function processCase( - arbiter: ReturnType, - paymentInfo: PaymentInfo, -) { - // Step 1: Check if a refund request exists - const hasRequest = await arbiter.refund?.has(paymentInfo) - if (!hasRequest) return - - // Step 2: Get the request data - const request = await arbiter.refund?.get(paymentInfo) - if (request?.status !== RefundRequestStatus.Pending) return - - // Step 3: Review evidence - const evidenceCount = await arbiter.evidence?.count(paymentInfo) - if (evidenceCount && evidenceCount > 0n) { - const batch = await arbiter.evidence?.getBatch(paymentInfo, 0n, evidenceCount) - for (const entry of batch?.entries ?? []) { - console.log('Evidence CID:', entry.cid, 'from:', entry.submitter) - } - } - - // Step 4: Make a decision - const shouldApprove = await evaluateCase(request) - - if (shouldApprove) { - const tx = await arbiter.payment.voidPayment(paymentInfo) - console.log('Refund executed:', tx) - } else { - const tx = await arbiter.refund?.deny(paymentInfo) - console.log('Denied:', tx) - } -} -``` - -## Decision flow diagram - -```mermaid -flowchart TD - A[Check refund.has] --> B[refund.get] - B --> C{Status Pending?} - C -->|No| D[Skip] - C -->|Yes| E[Review evidence] - E --> F{Decision} - F -->|Approve| G[payment.voidPayment] - F -->|Deny| H[refund.deny] - F -->|Decline| I[refund.refuse] - G --> J[Funds Returned to Payer] - H --> K[Merchant Keeps Funds] -``` - -## Next steps - - - - Process multiple cases at once. - - - Automate decisions with AI evaluation hooks. - - - How arbiters are discovered by merchants and clients. - - - RefundRequest contract and state machine details. - - diff --git a/sdk/arbiter/quickstart.mdx b/sdk/arbiter/quickstart.mdx deleted file mode 100644 index 5a431e2..0000000 --- a/sdk/arbiter/quickstart.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: "Arbiter SDK" -description: "Dispute resolution SDK for reviewing cases and making decisions" -icon: "rocket" ---- - -The `@x402r/sdk` package provides arbiter methods for resolving disputes: reviewing refund requests, approving or denying them, executing refunds, reviewing evidence, and distributing fees. - -## Setup - -```typescript -import { createArbiterClient } from '@x402r/sdk' -import { createPublicClient, createWalletClient, http } from 'viem' -import { baseSepolia } from 'viem/chains' -import { privateKeyToAccount } from 'viem/accounts' - -const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) - -const arbiter = createArbiterClient({ - publicClient: createPublicClient({ chain: baseSepolia, transport: http() }), - walletClient: createWalletClient({ - account, - chain: baseSepolia, - transport: http(), - }), - operatorAddress: '0x...', - refundRequestAddress: '0x...', - refundRequestEvidenceAddress: '0x...', - escrowPeriodAddress: '0x...', - freezeAddress: '0x...', -}) -``` - -## Available action groups - -The arbiter client provides these action groups: - -- **`payment`**: `capture`, `voidPayment`, `refund`, `getAmounts`, `getState` and more -- **`refund`**: `get`, `getStatus`, `has`, `deny`, `refuse`, `getOperatorRequests` and more -- **`evidence`**: `submit`, `get`, `getBatch`, `count` -- **`escrow`**: `isDuringEscrow`, `getAuthorizationTime`, `getDuration` -- **`freeze`**: `freeze`, `unfreeze`, `isFrozen` -- **`operator`**: `getConfig`, `calculateFees`, `distributeFees`, `getAccumulatedProtocolFees` and more -- **`watch`**: `onPayment`, `onRefundRequest`, `onRefundExecuted`, `onFeeDistribution` - -## Try it now - - - Approve refunds, review evidence, and distribute fees. - - -## Next steps - - - - See all working examples including the full payment flow. - - - Deploy a PaymentOperator with arbiter support. - - diff --git a/sdk/arbiter/registry.mdx b/sdk/arbiter/registry.mdx deleted file mode 100644 index f2933c9..0000000 --- a/sdk/arbiter/registry.mdx +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: "Identity and discovery" -description: "Register, discover, and rate arbiters using the ERC-8004 plugin" -icon: "clipboard-list" ---- - -The `@x402r/sdk` includes an ERC-8004 plugin that provides on-chain identity registration, reputation scoring, and service endpoint discovery. Arbiters can register themselves and be discovered by merchants and payers. - -## What changed - -Add the `erc8004Actions` plugin to your client using `.extend()`: - -```typescript -import { createArbiterClient, erc8004Actions } from '@x402r/sdk' - -const arbiter = createArbiterClient({ - publicClient, - walletClient, - operatorAddress: '0x...', - refundRequestAddress: '0x...', -}) - -const extended = arbiter.extend(erc8004Actions()) -``` - -This adds three action groups: `identity`, `reputation`, and `discovery`. - -## Identity actions - -### identity.register - -Register your address in the on-chain identity registry: - -```typescript -const tx = await extended.identity.register('my-arbiter-id') -console.log('Registered:', tx) -``` - -### identity.isRegistered - -Check if an address is registered: - -```typescript -const registered = await extended.identity.isRegistered('0xArbiterAddress...') -console.log('Is registered:', registered) -``` - -### identity.resolveAgent - -Look up an agent's registration data: - -```typescript -const agent = await extended.identity.resolveAgent('0xArbiterAddress...') -console.log('Agent:', agent) -``` - -### identity.verifyAgentId - -Verify that an agent ID matches an address: - -```typescript -const valid = await extended.identity.verifyAgentId('0xArbiterAddress...', 'my-arbiter-id') -console.log('Valid:', valid) -``` - -## Reputation actions - -### reputation.rate - -Submit a rating (0-100) for another address: - -```typescript -const tx = await extended.reputation.rate('0xTargetAddress...', 85) -console.log('Rating submitted:', tx) -``` - -### reputation.getSummary - -Get the reputation summary for an address: - -```typescript -const summary = await extended.reputation.getSummary('0xArbiterAddress...') -console.log('Reputation:', summary) -``` - -### reputation.giveFeedback - -Submit raw feedback with custom tags: - -```typescript -const tx = await extended.reputation.giveFeedback({ - target: '0xTargetAddress...', - score: 90n, - tag1: 'starred', - tag2: 'x402', -}) -console.log('Feedback submitted:', tx) -``` - -## Discovery actions - -### discovery.resolveServiceEndpoint - -Look up a service endpoint for an address: - -```typescript -const endpoint = await extended.discovery.resolveServiceEndpoint('0xArbiterAddress...') -console.log('Service endpoint:', endpoint) -``` - -## Complete example - -```typescript -import { createArbiterClient, erc8004Actions } from '@x402r/sdk' -import { createPublicClient, createWalletClient, http } from 'viem' -import { baseSepolia } from 'viem/chains' -import { privateKeyToAccount } from 'viem/accounts' - -const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) - -const arbiter = createArbiterClient({ - publicClient: createPublicClient({ chain: baseSepolia, transport: http() }), - walletClient: createWalletClient({ - account, - chain: baseSepolia, - transport: http(), - }), - operatorAddress: '0x...', -}).extend(erc8004Actions()) - -// Register -await arbiter.identity.register('my-dispute-resolver') - -// Verify -const isRegistered = await arbiter.identity.isRegistered(account.address) -console.log('Registered:', isRegistered) - -// Check reputation -const summary = await arbiter.reputation.getSummary(account.address) -console.log('Reputation:', summary) -``` - -## Next steps - - - - Approve and deny refund requests. - - - Automate dispute resolution with AI. - - - Complete arbiter setup guide. - - - Understand arbiter conditions in contracts. - - diff --git a/sdk/arbiter/subscriptions.mdx b/sdk/arbiter/subscriptions.mdx deleted file mode 100644 index 0a96770..0000000 --- a/sdk/arbiter/subscriptions.mdx +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: "Arbiter events" -description: "Subscribe to dispute events and build real-time arbiter dashboards" -icon: "bell" ---- - -The arbiter client provides real-time event subscriptions through the `watch` action group. Each method returns an unsubscribe function for cleanup. - -## watch.onRefundRequest - -Watch for refund request lifecycle events on the RefundRequest contract. This fires for new requests, status updates, and cancellations. - -```typescript -const unwatch = arbiter.watch.onRefundRequest((logs) => { - for (const log of logs) { - console.log('Refund event:', log.eventName) - } -}) - -// Later: stop watching -unwatch() -``` - - -This is a no-op if `refundRequestAddress` was not provided in the client config. - - -## watch.onPayment - -Watch for payment lifecycle events: `AuthorizeExecuted`, `ChargeExecuted`, and `CaptureExecuted`. - -```typescript -const unwatch = arbiter.watch.onPayment((logs) => { - for (const log of logs) { - console.log('Payment event:', log.eventName) - } -}) - -unwatch() -``` - -## watch.onRefundExecuted - -Watch for refund execution events: `VoidExecuted` and `RefundExecuted`. - -```typescript -const unwatch = arbiter.watch.onRefundExecuted((logs) => { - for (const log of logs) { - console.log('Refund executed:', log.eventName) - } -}) - -unwatch() -``` - -## watch.onFeeDistribution - -Watch for `FeesDistributed` events on the PaymentOperator contract. - -```typescript -const unwatch = arbiter.watch.onFeeDistribution((logs) => { - for (const log of logs) { - console.log('Fees distributed:', log) - } -}) - -unwatch() -``` - -## Event types reference - -| Method | Events Watched | Contract | -|--------|---------------|----------| -| `watch.onRefundRequest` | All RefundRequest ABI events | RefundRequest | -| `watch.onPayment` | `AuthorizeExecuted`, `ChargeExecuted`, `CaptureExecuted` | PaymentOperator | -| `watch.onRefundExecuted` | `VoidExecuted`, `RefundExecuted` | PaymentOperator | -| `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | - - -For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). - - -## Next steps - - - - Automate case evaluation with AI hooks. - - - Review the complete arbiter setup guide. - - - Process multiple queued cases efficiently. - - - See how payers subscribe to the same events. - - diff --git a/sdk/cli.mdx b/sdk/cli.mdx index 3138160..e839082 100644 --- a/sdk/cli.mdx +++ b/sdk/cli.mdx @@ -192,9 +192,6 @@ The `@x402r/cli` package exports: ## Next steps - - Use the SDK programmatically for richer payer workflows. - Accept payments and manage escrow releases. diff --git a/sdk/client/escrow-management.mdx b/sdk/client/escrow-management.mdx deleted file mode 100644 index 5e7204f..0000000 --- a/sdk/client/escrow-management.mdx +++ /dev/null @@ -1,177 +0,0 @@ ---- -title: "Escrow management" -description: "Manage escrow periods and freeze payments with the payer client" -icon: "lock" ---- - -Freeze payments during disputes and query escrow-period timing from the payer client. Methods sit on the `freeze` and `escrow` action groups. - -## Freeze operations - -Freezing a payment prevents the merchant from releasing funds while a dispute is being resolved. The `freeze` group requires `freezeAddress` in the client config. - -### freeze.freeze - -Freeze a payment to block the merchant from releasing. Only the payer can freeze a payment. - -```typescript -const tx = await client.freeze?.freeze(paymentInfo) -console.log('Payment frozen:', tx) -``` - - -Freezing is useful when: -- You need more time to resolve a dispute with the merchant -- You are waiting for additional information before deciding on a refund -- The merchant is unresponsive to your refund request - - -### freeze.unfreeze - -Unfreeze a previously frozen payment. The receiver (merchant) or arbiter can unfreeze a payment once the dispute is resolved. - -```typescript -const tx = await client.freeze?.unfreeze(paymentInfo) -console.log('Payment unfrozen:', tx) -``` - -### freeze.isFrozen - -Check whether a payment is currently frozen. - -```typescript -const frozen = await client.freeze?.isFrozen(paymentInfo) - -if (frozen) { - console.log('Payment is frozen') -} else { - console.log('Payment is not frozen') -} -``` - -## Escrow period operations - -These methods query timing information about a payment's escrow window. The `escrow` group requires `escrowPeriodAddress` in the client config. - -### escrow.getAuthorizationTime - -Get the timestamp (in seconds) when a payment was authorized on-chain. This is the starting point of the escrow period. - -```typescript -const authTime = await client.escrow?.getAuthorizationTime(paymentInfo) -const authDate = new Date(Number(authTime) * 1000) - -console.log('Payment authorized at:', authDate.toISOString()) -``` - -### escrow.isDuringEscrow - -Check whether a payment is still within its escrow period. Returns `true` if the escrow period is still active (refund window is open). - -```typescript -const duringEscrow = await client.escrow?.isDuringEscrow(paymentInfo) - -if (duringEscrow) { - console.log('Still in escrow period, refund is possible') -} else { - console.log('Escrow period has passed, funds can be fully released') -} -``` - -### escrow.getDuration - -Get the configured escrow period duration in seconds. - -```typescript -const duration = await client.escrow?.getDuration() -console.log('Escrow period:', duration, 'seconds') -``` - -## Understanding escrow timing - -| Condition | Escrow Timer | Can Request Refund | Can Capture | -|-----------|--------------|-------------------|-------------| -| Normal (unfrozen, period active) | Running | Yes | Partial only | -| Frozen | Paused | Yes | No | -| Escrow period passed (unfrozen) | Stopped | No | Full amount | - - -The escrow period length is configured at the contract level when the `EscrowPeriod` condition is deployed. Common values are 7 days, 14 days, or 30 days. You can calculate the remaining time from `escrow.getAuthorizationTime()` and `escrow.getDuration()`. - - -## Example: freeze and request refund - -A common pattern is to freeze a payment before submitting a refund request, ensuring the merchant cannot capture funds while the request is pending. - -```typescript -import { createPayerClient } from '@x402r/sdk' -import type { PaymentInfo } from '@x402r/sdk' - -async function freezeAndRequestRefund( - client: ReturnType, - paymentInfo: PaymentInfo, - refundAmount: bigint -) { - // Step 1: Check if already frozen - const alreadyFrozen = await client.freeze?.isFrozen(paymentInfo) - - if (!alreadyFrozen) { - const tx = await client.freeze?.freeze(paymentInfo) - console.log('Payment frozen:', tx) - } - - // Step 2: Check if refund request already exists - const hasRequest = await client.refund?.has(paymentInfo) - - if (!hasRequest) { - const tx = await client.refund?.request(paymentInfo, refundAmount) - console.log('Refund requested:', tx) - } - - // Step 3: Watch for resolution - const unwatch = client.watch.onRefundRequest((logs) => { - for (const log of logs) { - console.log('Refund event:', log.eventName) - } - unwatch() - }) -} -``` - -## Freeze / unfreeze flow - -```mermaid -sequenceDiagram - participant P as Payer - participant F as Freeze Contract - participant M as Merchant / Arbiter - - Note over F: Escrow timer running - P->>F: freeze.freeze() - Note over F: Timer paused - - alt Dispute resolved favorably - M->>F: freeze.unfreeze() - Note over F: Timer resumes - else Payer cancels dispute - P->>F: freeze.unfreeze() - Note over F: Timer resumes - end -``` - -## Next steps - - - - Watch for freeze and unfreeze events in real-time. - - - Request refunds while payment is in escrow. - - - Query payment state and amounts. - - - Full setup guide for the payer client. - - diff --git a/sdk/client/payment-queries.mdx b/sdk/client/payment-queries.mdx deleted file mode 100644 index 416df87..0000000 --- a/sdk/client/payment-queries.mdx +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: "Payment queries" -description: "Query payment states, amounts, and history with the payer client" -icon: "magnifying-glass" ---- - -Query lifecycle state, history, and operator config from the payer client. Methods sit on the `payment`, `query`, and `operator` action groups. - -## payment.getState - -Returns the lifecycle position of a payment as a tuple. - -```typescript -const [hasCollectedPayment, capturableAmount, refundableAmount] = - await client.payment.getState(paymentInfo) - -// Interpret: -// !hasCollectedPayment → not yet authorized -// hasCollectedPayment && capturableAmount > 0n → in escrow, still capturable -// hasCollectedPayment && capturableAmount === 0n && refundableAmount > 0n -// → captured, still refundable -// refundableAmount === 0n → fully settled -``` - -## payment.getAmounts - -Query the current capturable and refundable amounts for a payment. - -```typescript -const amounts = await client.payment.getAmounts(paymentInfo) - -console.log('Has collected:', amounts.hasCollectedPayment) -console.log('Capturable:', amounts.capturableAmount) -console.log('Refundable:', amounts.refundableAmount) -``` - -## query.getPayerPayments - -List all payments where a given address is the payer. Requires `paymentIndexRecorderHookAddress` in the client config. - -```typescript -const payments = await client.query?.getPayerPayments(payerAddress) - -for (const payment of payments ?? []) { - console.log('Payment info:', payment) -} -``` - - -The `query` group uses a tiered resolver: it checks the in-memory store first, then the on-chain hook, then falls back to event log scanning. Pass `eventFromBlock` in the client config to limit the scan range. - - -## query.getReceiverPayments - -List all payments where a given address is the receiver. - -```typescript -const payments = await client.query?.getReceiverPayments(receiverAddress) -``` - -## query.getPayment - -Look up a single payment by its hash. - -```typescript -const payment = await client.query?.getPayment(paymentInfoHash) -``` - -## operator.getConfig - -Retrieve all slot addresses from the PaymentOperator contract. - -```typescript -const config = await client.operator.getConfig() - -console.log('Capture condition:', config.captureCondition) -console.log('Fee calculator:', config.feeCalculator) -console.log('Fee recipient:', config.feeReceiver) -``` - -## operator.calculateFees - -Calculate the fee breakdown for a given payment amount. - -```typescript -const fees = await client.operator.calculateFees(paymentInfo, 1_000_000n) - -console.log('Operator fee:', fees.operatorFeeAmount) -console.log('Protocol fee:', fees.protocolFeeAmount) -console.log('Total fee:', fees.totalFeeAmount) -console.log('Net amount:', fees.netAmount) -``` - -## Next steps - - - - Request and manage refunds. - - - Freeze payments and query escrow periods. - - - Full setup guide for the payer client. - - - Known constraints including event log scanning limits. - - diff --git a/sdk/client/quickstart.mdx b/sdk/client/quickstart.mdx deleted file mode 100644 index a9dfe77..0000000 --- a/sdk/client/quickstart.mdx +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: "Client SDK" -description: "Payer-side SDK for refunds, freezes, and escrow management" -icon: "rocket" ---- - -The `@x402r/sdk` package provides payer-side methods for interacting with x402r payments: requesting refunds, freezing payments, submitting evidence, and querying escrow state. - -## Setup - -```typescript -import { createPayerClient } from '@x402r/sdk' -import { createPublicClient, createWalletClient, http } from 'viem' -import { baseSepolia } from 'viem/chains' -import { privateKeyToAccount } from 'viem/accounts' - -const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) - -const client = createPayerClient({ - publicClient: createPublicClient({ chain: baseSepolia, transport: http() }), - walletClient: createWalletClient({ - account, - chain: baseSepolia, - transport: http(), - }), - operatorAddress: '0x...', - refundRequestAddress: '0x...', - refundRequestEvidenceAddress: '0x...', - escrowPeriodAddress: '0x...', - freezeAddress: '0x...', -}) -``` - -## Available action groups - -The payer client provides these action groups: - -- **`payment`**: `getAmounts`, `getState`, `authorize`, `voidPayment` and more -- **`escrow`**: `isDuringEscrow`, `getAuthorizationTime`, `getDuration` -- **`refund`**: `request`, `cancel`, `get`, `getStatus`, `has`, `getByKey`, `getPayerRequests` and more -- **`evidence`**: `submit`, `get`, `getBatch`, `count` -- **`freeze`**: `freeze`, `unfreeze`, `isFrozen` -- **`watch`**: `onPayment`, `onRefundRequest`, `onRefundExecuted`, `onFeeDistribution` -- **`operator`**: `getConfig`, `getFeeAddresses`, `calculateFees` and more -- **`query`**: `getPayerPayments`, `getReceiverPayments`, `getPayment` (requires `paymentIndexRecorderHookAddress`) - -Optional groups (`escrow`, `refund`, `evidence`, `freeze`, `query`) are `undefined` if you do not pass the corresponding contract address. Use optional chaining when calling them. - -## Try it now - -The easiest way to try payer features is with the runnable examples in the SDK repo: - - - Request refunds, submit evidence, and freeze payments. - - -## Next steps - - - - See all working examples including the full payment flow. - - - Deploy a PaymentOperator to test client operations against. - - diff --git a/sdk/client/refund-operations.mdx b/sdk/client/refund-operations.mdx deleted file mode 100644 index a0d0d23..0000000 --- a/sdk/client/refund-operations.mdx +++ /dev/null @@ -1,138 +0,0 @@ ---- -title: "Refund operations" -description: "Request and manage refunds with the payer client" -icon: "rotate-left" ---- - -The payer client provides complete refund management through the `refund` action group. All methods interact directly with the `RefundRequest` contract on-chain. - - -The `refund` group requires `refundRequestAddress` in the client config. Without it, `client.refund` is `undefined`. - - -## refund.request - -Submit a refund request for a payment that is in escrow. The request goes on-chain and is visible to the merchant and any assigned arbiter. - -```typescript -const tx = await client.refund?.request(paymentInfo, 1_000_000n) // 1 USDC -console.log('Refund requested:', tx) -``` - -## refund.cancel - -Cancel a pending refund request that you submitted. Only the original requester (payer) can cancel, and only while the request status is `Pending`. - -```typescript -const tx = await client.refund?.cancel(paymentInfo) -console.log('Refund request cancelled:', tx) -``` - -## Query refund state - -These methods read on-chain state for refund requests. - -### Check existence and status - -```typescript -// Check if a refund request exists -const hasRequest = await client.refund?.has(paymentInfo) - -// Get just the status -const status = await client.refund?.getStatus(paymentInfo) -// Returns: 0 = Pending, 1 = Approved, 2 = Denied, 3 = Cancelled, 4 = Refused -``` - -### Get full refund request data - -```typescript -// By paymentInfo -const request = await client.refund?.get(paymentInfo) -console.log(request?.amount, request?.status) - -// By hash (from list methods) -const request2 = await client.refund?.getByKey(paymentInfoHash) -``` - -### List refund requests - -```typescript -// Get payer's refund requests (paginated) -const payerRequests = await client.refund?.getPayerRequests( - payerAddress, - 0n, // offset - 10n, // count -) - -// Get receiver's refund requests -const receiverRequests = await client.refund?.getReceiverRequests( - receiverAddress, - 0n, - 10n, -) - -// Get all requests for an operator -const operatorRequests = await client.refund?.getOperatorRequests( - operatorAddress, - 0n, - 10n, -) -``` - -### Track cancellations - -```typescript -const cancelCount = await client.refund?.getCancelCount(paymentInfo) -const cancelledAmount = await client.refund?.getCancelledAmount(paymentInfo, 0n) -``` - -## Refund request lifecycle - -```mermaid -stateDiagram-v2 - [*] --> Pending: refund.request() - Pending --> Approved: Merchant/Arbiter approves - Pending --> Denied: Merchant/Arbiter denies - Pending --> Refused: Arbiter declines to rule - Pending --> Cancelled: refund.cancel() - Approved --> [*]: Funds returned - Denied --> [*] - Refused --> [*] - Cancelled --> [*] -``` - -## Method reference - -| Method | Parameters | Returns | -|--------|-----------|---------| -| `refund.request` | `paymentInfo, amount` | `Hash` | -| `refund.cancel` | `paymentInfo` | `Hash` | -| `refund.deny` | `paymentInfo` | `Hash` | -| `refund.refuse` | `paymentInfo` | `Hash` | -| `refund.has` | `paymentInfo` | `boolean` | -| `refund.getStatus` | `paymentInfo` | `RefundRequestStatus` | -| `refund.get` | `paymentInfo` | `RefundRequestData` | -| `refund.getByKey` | `paymentInfoHash` | `RefundRequestData` | -| `refund.getStoredPaymentInfo` | `paymentInfoHash` | `PaymentInfo` | -| `refund.getPayerRequests` | `payer, offset, count` | `RefundRequestData[]` | -| `refund.getReceiverRequests` | `receiver, offset, count` | `RefundRequestData[]` | -| `refund.getOperatorRequests` | `operator, offset, count` | `RefundRequestData[]` | -| `refund.getCancelCount` | `paymentInfo` | `bigint` | -| `refund.getCancelledAmount` | `paymentInfo, cancelIndex` | `bigint` | - -## Next steps - - - - Freeze payments and check escrow period timing. - - - Watch for refund status updates in real-time. - - - Query payment state and details. - - - Full setup guide for the payer client. - - diff --git a/sdk/client/subscriptions.mdx b/sdk/client/subscriptions.mdx deleted file mode 100644 index 1460753..0000000 --- a/sdk/client/subscriptions.mdx +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: "Client events" -description: "Subscribe to real-time payment, refund, and fee events" -icon: "bell" ---- - -The `watch` action group provides real-time event subscriptions using viem's `watchContractEvent` underneath. Each method returns an unsubscribe function you should call when you no longer need the watcher. - -The `watch` group is always available on the client (no optional address required). - -## watch.onPayment - -Watch for payment lifecycle events: `AuthorizeExecuted`, `ChargeExecuted`, and `CaptureExecuted` on the PaymentOperator contract. - -```typescript -const unwatch = client.watch.onPayment((logs) => { - for (const log of logs) { - console.log('Payment event:', log.eventName) - - switch (log.eventName) { - case 'AuthorizeExecuted': - console.log('New payment authorized') - break - case 'ChargeExecuted': - console.log('Payment charged') - break - case 'CaptureExecuted': - console.log('Funds released to merchant') - break - } - } -}) - -// Stop watching when done -unwatch() -``` - -## watch.onRefundRequest - -Watch for refund request lifecycle events on the RefundRequest contract. This is a no-op if `refundRequestAddress` was not provided in the client config. - -```typescript -const unwatch = client.watch.onRefundRequest((logs) => { - for (const log of logs) { - console.log('Refund event:', log.eventName) - } -}) - -// Stop watching when done -unwatch() -``` - -## watch.onRefundExecuted - -Watch for refund execution events: `VoidExecuted` and `RefundExecuted` on the PaymentOperator contract. - -```typescript -const unwatch = client.watch.onRefundExecuted((logs) => { - for (const log of logs) { - console.log('Refund executed:', log.eventName) - } -}) - -unwatch() -``` - -## watch.onFeeDistribution - -Watch for `FeesDistributed` events on the PaymentOperator contract. - -```typescript -const unwatch = client.watch.onFeeDistribution((logs) => { - for (const log of logs) { - console.log('Fees distributed:', log) - } -}) - -unwatch() -``` - -## Event types reference - -| Method | Events Watched | Contract | -|--------|---------------|----------| -| `watch.onPayment` | `AuthorizeExecuted`, `ChargeExecuted`, `CaptureExecuted` | PaymentOperator | -| `watch.onRefundRequest` | All RefundRequest ABI events | RefundRequest | -| `watch.onRefundExecuted` | `VoidExecuted`, `RefundExecuted` | PaymentOperator | -| `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | - - -For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). - - -## Next steps - - - - Review the complete client setup. - - - Request and manage refunds. - - - Freeze payments and check escrow timing. - - - Learn about the merchant side of x402r. - - diff --git a/sdk/concepts.mdx b/sdk/concepts.mdx deleted file mode 100644 index e233abc..0000000 --- a/sdk/concepts.mdx +++ /dev/null @@ -1,213 +0,0 @@ ---- -title: "Core Concepts" -description: "Understand the X402r payment lifecycle, escrow, and dispute resolution" -icon: "lightbulb" ---- - -## Payment States - -`getPaymentState()` returns a tuple, not an enum. There is no exported `PaymentState` symbol; derive lifecycle position from the returned values. - -```typescript -// getPaymentState() return type: -// readonly [hasCollectedPayment: boolean, capturableAmount: bigint, refundableAmount: bigint] -const [collected, capturable, refundable] = await client.payment.getState(paymentInfo) -``` - -Conceptually, every payment moves through this lifecycle: - -```mermaid -stateDiagram-v2 - [*] --> InEscrow: authorize() - InEscrow --> Captured: capture() - InEscrow --> Settled: voidPayment() - InEscrow --> Reclaimable: captureDeadline passes - Captured --> Settled: refund() - Reclaimable --> Settled: reclaim() - Settled --> [*] -``` - -## Payment Info - -The `PaymentInfo` struct uniquely identifies a payment and contains all its parameters: - -```typescript -interface PaymentInfo { - operator: `0x${string}`; // PaymentOperator contract address - payer: `0x${string}`; // Payer's address - receiver: `0x${string}`; // Merchant's address - token: `0x${string}`; // Payment token (e.g., USDC) - maxAmount: bigint; // Maximum payment amount - preApprovalExpiry: bigint; // Pre-approval expiry timestamp (0n if not used) - authorizationExpiry: bigint; // When payer can reclaim funds - refundExpiry: bigint; // Deadline for refund requests - minFeeBps: number; // Minimum fee in basis points - maxFeeBps: number; // Maximum fee in basis points - feeReceiver: `0x${string}`; // Address that receives fees - salt: bigint; // Unique salt for this payment -} -``` - - -Use `computePaymentInfoHash()` from `@x402r/sdk` to compute the unique hash of a payment. - - -## Escrow Period - -The **EscrowPeriod** contract tracks when a payment was authorized and enforces a configurable waiting period before funds can be captured. During the escrow period: - -- **Payers** can request refunds or freeze the payment -- **Merchants** can void (return funds) but cannot capture -- **After the period**: merchants can capture funds - -```typescript -import { createPayerClient } from '@x402r/sdk' - -const client = createPayerClient({ - publicClient, - walletClient, - operatorAddress: '0x...', - escrowPeriodAddress: '0x...', -}) - -// Check when payment was authorized -const authTime = await client.escrow?.getAuthorizationTime(paymentInfo) - -// Check if still within escrow period -const inEscrow = await client.escrow?.isDuringEscrow(paymentInfo) -if (!inEscrow) { - console.log('Escrow period has passed, funds can be released') -} -``` - -## Refund requests - -When a payer wants a refund, they create a refund request. Each payment supports one refund request, keyed by its `paymentInfoHash`. - -```typescript -import { RefundRequestStatus } from '@x402r/sdk' - -// RefundRequestStatus values: -RefundRequestStatus.Pending // 0 - Awaiting decision -RefundRequestStatus.Approved // 1 - Approved by merchant/arbiter -RefundRequestStatus.Denied // 2 - Denied by merchant/arbiter -RefundRequestStatus.Cancelled // 3 - Cancelled by payer -RefundRequestStatus.Refused // 4 - Arbiter declined to rule -``` - -### Refund flow - -In v3, RefundRequest is wired as an IHook plugin. Refund approval happens automatically when the merchant or arbiter calls `voidPayment()` on the operator, no separate approve step is needed. - -```mermaid -sequenceDiagram - participant P as Payer - participant R as RefundRequest (IHook) - participant M as Merchant - participant O as PaymentOperator - participant A as Arbiter - - P->>R: requestRefund(paymentInfo, amount) - R-->>M: RefundRequested event - - alt Merchant refunds - M->>O: voidPayment(paymentInfo) - O->>R: record() auto-approves pending request - O->>P: Funds returned - else Merchant denies - M->>R: denyRefundRequest(paymentInfo) - else Escalate to arbiter - A->>O: voidPayment(paymentInfo) - O->>R: record() auto-approves pending request - O->>P: Funds returned - end -``` - -```typescript -// Request a refund (one per payment) -await client.refund.request(paymentInfo, amount); - -// Check status -const status = await client.refund.getStatus(paymentInfo); -``` - -## Freeze / Unfreeze - -The **Freeze** contract allows payers to freeze a payment during the escrow period, preventing capture until the freeze expires or is lifted: - -```typescript -// Payer freezes payment (requires payer authorization) -await client.freeze?.freeze(paymentInfo) - -// Merchant unfreezes payment (requires receiver authorization) -await merchant.freeze?.unfreeze(paymentInfo) - -// Check frozen status -const frozen = await client.freeze?.isFrozen(paymentInfo) -``` - - -Freezing a payment does not automatically escalate to an arbiter. It pauses the capture condition to allow time for dispute resolution. - - -## Roles and Permissions - -| Role | Can Do | -|---|---| -| **Payer** | Request refunds, freeze payments, cancel requests, query escrow state | -| **Merchant** | Capture payments, charge, void (auto-approves requests), deny refunds | -| **Arbiter** | Deny/refuse disputed refunds, void, review evidence | - -## Contract Architecture - -```mermaid -flowchart TB - subgraph PO[PaymentOperator] - direction LR - auth[authorize] - cap[capture] - chg[charge] - v[void] - refp[refund] - end - - subgraph Components[Supporting Contracts] - direction LR - subgraph RR[RefundRequest IHook] - rr1[Request/Cancel] - rr2[Deny/Refuse] - rr3[Auto-approve on refund] - end - subgraph EP[EscrowPeriod] - ep1[Track auth time] - ep2[Check period] - end - subgraph FR[Freeze] - fr1[Freeze/Unfreeze] - fr2[Check frozen] - end - subgraph Cond[Conditions] - c1[Access Control] - c2[Combinators] - end - end - - PO --> RR - PO --> EP - PO --> FR - PO --> Cond -``` - -## Next Steps - - - - Deploy your own PaymentOperator with all supporting contracts. - - - Working examples for merchants, clients, and arbiters. - - - Understand the underlying contract architecture. - - diff --git a/sdk/create-client.mdx b/sdk/create-client.mdx index 93db678..6b65deb 100644 --- a/sdk/create-client.mdx +++ b/sdk/create-client.mdx @@ -160,7 +160,7 @@ interface CheckAgentResult { When `reviewers` is omitted or empty, `reputation` is `null` and only on-chain verification runs. -For standalone helpers that extract identity data from x402 extension responses without a client instance, see [ERC-8004 helpers](/sdk/helpers/erc8004). +For standalone helpers that extract identity data from x402 extension responses without a client instance, use the `extractArbiterIdentity`, `extractReputationRegistrations`, and `fetchArbiterIdentity` exports from `@x402r/sdk`. ## Next steps @@ -172,7 +172,4 @@ For standalone helpers that extract identity data from x402 extension responses Get the addresses for your client config. - - Standalone extraction helpers for arbiter identity and reputation data. - diff --git a/sdk/delivery-arbiter.mdx b/sdk/delivery-arbiter.mdx index 6e60fa9..8cdd449 100644 --- a/sdk/delivery-arbiter.mdx +++ b/sdk/delivery-arbiter.mdx @@ -126,7 +126,7 @@ There is a full [AI garbage detector example](https://github.com/BackTrackCo/arb Deploy the operator and configure forwardToArbiter(). - + For human-reviewed disputes instead of automated evaluation. diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index 410c764..be8db06 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -330,9 +330,6 @@ console.log('AuthorizeHook:', deployment.authorizeHookAddress) Accept payments, capture funds from escrow. - - Request refunds, freeze payments, submit evidence. - See working merchant and client examples. diff --git a/sdk/examples.mdx b/sdk/examples.mdx index 4741440..7638598 100644 --- a/sdk/examples.mdx +++ b/sdk/examples.mdx @@ -78,9 +78,6 @@ See the [examples directory README](https://github.com/BackTrackCo/x402r-sdk/tre Forward `authCapture` settlements to an arbiter service. - - The payment lifecycle and key terms. - Browse every example. diff --git a/sdk/facilitator/getting-started.mdx b/sdk/facilitator/getting-started.mdx deleted file mode 100644 index a58b442..0000000 --- a/sdk/facilitator/getting-started.mdx +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: "Facilitator Quickstart" -description: "Run your own x402r facilitator service to verify and settle escrow payments" -icon: "server" ---- - -A facilitator is a service that verifies payment signatures and settles escrow transactions on-chain. This guide walks you through running your own facilitator on Base Sepolia. - - -The full source code for this example is available on [GitHub](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/facilitator/basic). - - -## Prerequisites - -- Node.js 20+ -- A wallet private key with Base Sepolia ETH - - -Never commit private keys to source control. Use environment variables or a secrets manager. - - -## Setup - - - - ```bash - mkdir facilitator && cd facilitator - npm init -y - npm install @x402/core @x402/evm @x402r/evm express dotenv viem - ``` - - - - Create a `.env` file in the project root: - - ```bash - PRIVATE_KEY=0xYourPrivateKey - PORT=4022 - ``` - - - - Create `index.ts`. This is identical to a standard x402 facilitator, with two additions: the `@x402r/evm` import and the `registerAuthCaptureEvmScheme()` call. - - ```typescript - import "dotenv/config"; - import express from "express"; - import { x402Facilitator } from "@x402/core/facilitator"; - import { PaymentPayload, PaymentRequirements } from "@x402/core/types"; - import { toFacilitatorEvmSigner } from "@x402/evm"; - import { registerAuthCaptureEvmScheme } from "@x402r/evm/authCapture/facilitator"; - import { createWalletClient, http, publicActions } from "viem"; - import { privateKeyToAccount } from "viem/accounts"; - import { baseSepolia } from "viem/chains"; - - if (!process.env.PRIVATE_KEY) { - console.error("PRIVATE_KEY environment variable is required"); - process.exit(1); - } - - const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); - const viemClient = createWalletClient({ - account, chain: baseSepolia, transport: http(), - }).extend(publicActions); - const evmSigner = toFacilitatorEvmSigner({ - address: account.address, - getCode: (args) => viemClient.getCode(args), - readContract: (args) => viemClient.readContract({ ...args, args: args.args || [] }), - verifyTypedData: (args) => viemClient.verifyTypedData(args as any), - writeContract: (args) => viemClient.writeContract({ ...args, args: args.args || [] }), - sendTransaction: (args) => viemClient.sendTransaction(args), - waitForTransactionReceipt: (args) => viemClient.waitForTransactionReceipt(args), - }); - - // Standard x402 facilitator setup - const facilitator = new x402Facilitator(); - - // x402r: Register the escrow scheme to handle refundable payments - registerAuthCaptureEvmScheme(facilitator, { - signer: evmSigner, - networks: "eip155:84532", - }); - - const app = express(); - app.use(express.json()); - - app.post("/verify", async (req, res) => { - const { paymentPayload, paymentRequirements } = req.body as { - paymentPayload: PaymentPayload; - paymentRequirements: PaymentRequirements; - }; - res.json(await facilitator.verify(paymentPayload, paymentRequirements)); - }); - - app.post("/settle", async (req, res) => { - const { paymentPayload, paymentRequirements } = req.body; - res.json(await facilitator.settle( - paymentPayload as PaymentPayload, - paymentRequirements as PaymentRequirements, - )); - }); - - app.get("/supported", async (_req, res) => { - res.json(facilitator.getSupported()); - }); - - const PORT = process.env.PORT || "4022"; - app.listen(parseInt(PORT), () => { - console.log(`Facilitator listening on http://localhost:${PORT}`); - }); - ``` - - - - ```bash - npx tsx index.ts - ``` - - You should see: - ``` - Facilitator account: 0x... - Facilitator listening on http://localhost:4022 - ``` - - - -## How it works - -- **`x402Facilitator`** is the core facilitator class from `@x402/core` that routes verify and settle requests to registered scheme handlers. -- **`registerAuthCaptureEvmScheme`** adds x402r escrow support to the facilitator, enabling it to verify escrow payment signatures and settle them on-chain. -- **`toFacilitatorEvmSigner`** adapts a viem wallet client into the signer interface the facilitator expects for on-chain interactions. -- The three endpoints (`/verify`, `/settle`, `/supported`) match the interface that `HTTPFacilitatorClient` on the merchant side expects. - -## Next Steps - - - - Set up a merchant server that uses your facilitator. - - - Understand the payment lifecycle and escrow flow. - - - Deploy a PaymentOperator contract for your merchant. - - diff --git a/sdk/helpers/erc8004.mdx b/sdk/helpers/erc8004.mdx deleted file mode 100644 index 2372e2c..0000000 --- a/sdk/helpers/erc8004.mdx +++ /dev/null @@ -1,156 +0,0 @@ ---- -title: "ERC-8004 helpers" -description: "Extract arbiter identity and agent registrations from x402 extension responses" ---- - -The `@x402r/sdk` package exports three helper functions for working with ERC-8004 identity data from x402 payment extensions. These are standalone functions that do not require a client instance. - -## `extractArbiterIdentity()` - -Extracts the arbiter's `agentId` and `address` from a raw attestation extension response. The attestation extension passes through untyped data, so this helper validates the shape before returning a typed result. - -Returns `undefined` if the arbiter has not registered or if the attestation extension is absent. - -```typescript -import { extractArbiterIdentity } from '@x402r/sdk' - -const identity = extractArbiterIdentity( - paymentRequired.extensions?.attestation?.info?.identity -) - -if (identity) { - console.log('Agent ID:', identity.agentId) // bigint - console.log('Address:', identity.address) // 0x-prefixed address -} -``` - -### Parameters - -| Parameter | Type | Description | -|-----------|------|-------------| -| `attestationInfo` | `unknown` | Raw attestation identity object from `extensions.attestation.info.identity` | - -### Return type - -Returns `ArbiterIdentity | undefined`: - -```typescript -interface ArbiterIdentity { - agentId: bigint - address: Address -} -``` - -The `agentId` field is coerced to `bigint` from any of: `bigint`, integer `number`, or numeric `string` (covers JSON deserialization). Non-integer numbers and non-numeric strings return `undefined`. - -## `extractReputationRegistrations()` - -Parses agent registrations from the upstream x402 reputation extension (`extensions["reputation"].info.registrations`). Agents can have multiple registrations across chains (EVM + Solana). Each registration has a CAIP-10 `agentRegistry` and a numeric `agentId`. - -Returns an empty array if the reputation extension is absent or malformed. - - -The upstream x402 reputation extension is not yet merged. The shape of `extensions["reputation"]` may change. - - -```typescript -import { extractReputationRegistrations } from '@x402r/sdk' - -const registrations = extractReputationRegistrations( - paymentRequired.extensions -) - -for (const reg of registrations) { - console.log('Agent ID:', reg.agentId) // bigint - console.log('Registry:', reg.agentRegistry) // CAIP-10 string -} -``` - -### Parameters - -| Parameter | Type | Description | -|-----------|------|-------------| -| `extensions` | `Record \| undefined` | Extensions object from `PaymentRequired` or `PaymentPayload` | - -### Return type - -Returns `AgentRegistration[]`: - -```typescript -interface AgentRegistration { - agentId: bigint - /** CAIP-10 identity registry address (e.g. "eip155:8453:0x8004..." or "solana:5eykt4...:satiRkx...") */ - agentRegistry: string -} -``` - -Entries with non-numeric `agentId` values (such as Solana base58 mint addresses) or empty `agentRegistry` strings are silently skipped. - -## `fetchArbiterIdentity()` - -Fetches the arbiter's identity by POSTing to the `/attest/identity` endpoint. You can use this at merchant startup to verify the arbiter before serving customers. - -Returns `undefined` if the arbiter does not include an `agentId`. Network errors (DNS failures, connection refused) propagate as thrown exceptions. - -```typescript -import { fetchArbiterIdentity } from '@x402r/sdk' - -const identity = await fetchArbiterIdentity('https://arbiter.example.com') - -if (identity) { - console.log('Arbiter agent:', identity.agentId, identity.address) -} else { - console.warn('Arbiter is not registered with an agentId') -} -``` - -### Parameters - -| Parameter | Type | Description | -|-----------|------|-------------| -| `arbiterUrl` | `string` | Base URL of the arbiter service (trailing slash is handled) | - -### Return type - -Returns `Promise`. See `extractArbiterIdentity()` for the `ArbiterIdentity` type. - -## Complete example - -A merchant service that verifies the arbiter at startup and checks agent registrations on each payment: - -```typescript -import { - fetchArbiterIdentity, - extractReputationRegistrations, -} from '@x402r/sdk' - -// At startup: verify the arbiter -const arbiterIdentity = await fetchArbiterIdentity( - process.env.ARBITER_URL! -) -if (!arbiterIdentity) { - throw new Error('Arbiter is not registered, refusing to start') -} -console.log('Arbiter verified:', arbiterIdentity.agentId) - -// On each payment: check reputation registrations -function handlePayment(paymentRequired: { extensions?: Record }) { - const registrations = extractReputationRegistrations( - paymentRequired.extensions - ) - if (registrations.length === 0) { - console.log('No reputation registrations found') - } - for (const reg of registrations) { - console.log(`Agent ${reg.agentId} on ${reg.agentRegistry}`) - } -} -``` - -## Next steps - - - - Client factory with identity.check() for combined verification. - - diff --git a/sdk/helpers/forward-to-arbiter.mdx b/sdk/helpers/forward-to-arbiter.mdx index 4d9c980..465f09e 100644 --- a/sdk/helpers/forward-to-arbiter.mdx +++ b/sdk/helpers/forward-to-arbiter.mdx @@ -141,9 +141,6 @@ import { ## Next steps - - Build an arbiter that processes forwarded settlements. - See working merchant server examples. diff --git a/sdk/limitations.mdx b/sdk/limitations.mdx deleted file mode 100644 index f1eedc5..0000000 --- a/sdk/limitations.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: "Current Limitations" -description: "Known limitations and constraints in the current SDK" -icon: "circle-info" ---- - -The SDK provides full coverage of core payment flows including authorization, capture, charge, refund, dispute resolution, and evidence submission. This page documents the known limitations. - -## API Constraints - -### Chain configuration - -Use `getChainConfig()` from `@x402r/sdk` with a numeric chain ID: - -```typescript -import { getChainConfig } from '@x402r/sdk' - -// Correct - numeric chain ID -const config = getChainConfig(84532) - -// Incorrect - EIP-155 strings are not accepted -// const config = getChainConfig('eip155:84532') -``` - -Use `toNetworkId(chainId)` and `fromNetworkId(networkId)` to convert between numeric chain IDs and EIP-155 format strings when needed. - -### PaymentInfo must be complete - -Most SDK methods require a complete `PaymentInfo` object. You can use `refund.getStoredPaymentInfo(paymentInfoHash)` to retrieve stored PaymentInfo from the RefundRequest contract, or use the query plugin to look up payments by hash: - -```typescript -// Works - full PaymentInfo -const status = await client.refund?.getStatus(paymentInfo) - -// Some methods accept a hash directly -const request = await client.refund?.getByKey(paymentInfoHash) -``` - -### Event Log Scanning Limits - -The event-based query provider scans `AuthorizeExecuted` and `ChargeExecuted` events using `eth_getLogs`. Base Sepolia RPCs typically limit responses to 10,000 blocks. Configure `eventFromBlock` in your client config to set the scan start: - -```typescript -// Pass eventFromBlock when creating the client to limit scan range -const client = createPayerClient({ - publicClient, - walletClient, - operatorAddress: '0x...', - paymentIndexRecorderHookAddress: '0x...', - eventFromBlock: recentBlockNumber, -}) - -const payments = await client.query?.getPayerPayments(payerAddress) -``` - -### No Express/Hono Middleware - -The `forwardToArbiter()` hook in `@x402r/helpers` is framework-agnostic. There is no dedicated Express or Hono middleware, configure escrow payment options inline and use `forwardToArbiter()` as an `onAfterSettle` hook. - -## Getting Updates - - - - Return to SDK documentation. - - - Working examples for each role. - - - Watch for new SDK releases. - - diff --git a/sdk/merchant.mdx b/sdk/merchant.mdx deleted file mode 100644 index 42ae697..0000000 --- a/sdk/merchant.mdx +++ /dev/null @@ -1,121 +0,0 @@ ---- -title: "Merchant Guide" -description: "Accept a payment into escrow, check state, and capture funds." -icon: "store" ---- - -### Prerequisites - -* A wallet with ETH on Base Sepolia for gas ([faucet](https://www.alchemy.com/faucets/base-sepolia)) -* Node.js 18+ and npm -* A deployed operator with escrow support (see [Deploy an Operator](/sdk/deploy-operator)) - - -There are pre-configured [examples in the x402r-sdk repo](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples), including [merchant examples](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/merchant) and full [scenario scripts](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/scenarios). - - -### 1. Install Dependencies - - -```bash npm -npm install @x402r/sdk -``` -```bash pnpm -pnpm add @x402r/sdk -``` -```bash bun -bun add @x402r/sdk -``` - - -### 2. Create a Merchant Client - -```typescript -import { createPublicClient, createWalletClient, http } from 'viem' -import { baseSepolia } from 'viem/chains' -import { privateKeyToAccount } from 'viem/accounts' -import { createMerchantClient } from '@x402r/sdk' - -const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) - -const merchant = createMerchantClient({ - publicClient: createPublicClient({ chain: baseSepolia, transport: http() }), - walletClient: createWalletClient({ - account, - chain: baseSepolia, - transport: http(), - }), - operatorAddress: '0x...', // from deploy result - escrowPeriodAddress: '0x...', // from deploy result - refundRequestAddress: '0x...', // from deploy result - freezeAddress: '0x...', // from deploy result - refundRequestEvidenceAddress: '0x...', // from deploy result -}) -``` - -### 3. Check Payment State - -```typescript -import type { PaymentInfo } from '@x402r/sdk' - -// paymentInfo comes from the facilitator callback or your payment records -const paymentInfo: PaymentInfo = { /* ... */ } - -const amounts = await merchant.payment.getAmounts(paymentInfo) -console.log('Collected:', amounts.hasCollectedPayment) // true -console.log('Capturable:', amounts.capturableAmount) // 1000000n -console.log('Refundable:', amounts.refundableAmount) // 1000000n - -const inEscrow = await merchant.escrow?.isDuringEscrow(paymentInfo) -console.log('In escrow:', inEscrow) // true -``` - -### 4. Capture Funds After Escrow - - -`capture()` reverts if called during escrow. Check `escrow.isDuringEscrow()` first. Pass a smaller amount to capture partially. - - -```typescript -const releaseTx = await merchant.payment.capture(paymentInfo, 1_000_000n) -console.log('Captured:', releaseTx) - -// Verify -const after = await merchant.payment.getAmounts(paymentInfo) -console.log('Capturable after capture:', after.capturableAmount) // 0n -``` - -### 5. Handle Refund Requests (Optional) - -If a payer disputes, check for pending refund requests: - -```typescript -const hasRefund = await merchant.refund?.has(paymentInfo) - -if (hasRefund) { - const request = await merchant.refund?.get(paymentInfo) - console.log('Refund amount:', request?.amount) - console.log('Status:', request?.status) // 0 = Pending - - // Approve by executing voidPayment (hook auto-approves) - const refundTx = await merchant.payment.voidPayment( - paymentInfo, - request!.amount, - ) - console.log('Refunded:', refundTx) -} -``` - -## Next Steps - - - - Full deployment config, slot details, and preview addresses. - - - How escrow, capture, and void work under the hood. - - - Full scenario scripts to copy from. - - diff --git a/sdk/merchant/getting-started.mdx b/sdk/merchant/getting-started.mdx index dd0ec53..ff9aeab 100644 --- a/sdk/merchant/getting-started.mdx +++ b/sdk/merchant/getting-started.mdx @@ -14,7 +14,7 @@ The full source code for this example is available on [GitHub](https://github.co - Node.js 20+ - A deployed PaymentOperator contract ([Deploy Operator](/sdk/deploy-operator)) -- A running facilitator service ([Facilitator Quickstart](/sdk/facilitator/getting-started)) +- A running facilitator service - Base Sepolia ETH for testing ## Setup @@ -180,7 +180,4 @@ The full source code for this example is available on [GitHub](https://github.co Deploy your own PaymentOperator contract. - - Run your own facilitator service. - diff --git a/sdk/merchant/payment-operations.mdx b/sdk/merchant/payment-operations.mdx deleted file mode 100644 index 97b4bad..0000000 --- a/sdk/merchant/payment-operations.mdx +++ /dev/null @@ -1,201 +0,0 @@ ---- -title: "Payment operations" -description: "Capture funds, charge payments, process refunds, and query escrow state" -icon: "coins" ---- - -Use `createMerchantClient` to capture escrowed funds, charge directly, void or refund, and query operator state. The methods below sit on the `payment` and `operator` action groups. - -## Payment operations - -### payment.capture - -Transfer escrowed funds to the receiver (merchant). The `amount` parameter is required. - -```typescript -// Capture 10 USDC (6 decimals) from escrow -const tx = await merchant.payment.capture(paymentInfo, 10_000_000n) -console.log('Captured:', tx) -``` - -For partial captures, specify a smaller amount. The remaining funds stay in escrow. - -```typescript -// Capture 3 USDC of a 10 USDC escrow -const tx = await merchant.payment.capture(paymentInfo, 3_000_000n) - -// Check what remains -const amounts = await merchant.payment.getAmounts(paymentInfo) -console.log('Remaining in escrow:', amounts.capturableAmount) // 7000000n -``` - - -Always query `payment.getAmounts()` first to determine the available capturable amount. - - -### payment.voidPayment - -Return all escrowed funds to the payer before capture. Void is full-only: it empties the authorization in one transaction. For a partial return, call `payment.capture()` for the part to keep, then void or let the remainder expire. - -```typescript -const tx = await merchant.payment.voidPayment(paymentInfo) -console.log('Voided:', tx) -``` - -### payment.charge - -For non-escrow flows such as subscriptions or session-based payments. Pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`). - -```typescript -const tx = await merchant.payment.charge( - paymentInfo, - 5_000_000n, // 5 USDC - '0xTokenCollector...' as `0x${string}`, // token collector contract - '0xSignatureData...' as `0x${string}`, // authorization data -) -console.log('Charged:', tx) -``` - - -The `charge()` method is designed for recurring payments and session-based billing where funds are not pre-escrowed. - - -### payment.refund - -Refund funds that have already been released. Requires a token collector to source the refund from the merchant's balance. - -```typescript -const tx = await merchant.payment.refund( - paymentInfo, - 5_000_000n, // 5 USDC to refund - '0xTokenCollector...' as `0x${string}`, // sources the refund - '0xSignatureData...' as `0x${string}`, // authorization data -) -console.log('Refund (after capture):', tx) -``` - - -Refunds (after capture) require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer. - - -### payment.approveRefundAllowance - -Approve a refund allowance. This sets how much the refund collector can pull from the merchant for a given token. - -```typescript -const tx = await merchant.payment.approveRefundAllowance( - '0xTokenAddress...' as `0x${string}`, // ERC-20 token - 5_000_000n, // 5 USDC -) -console.log('Allowance approved:', tx) -``` - -### payment.getRefundAllowance - -Check the current refund allowance for a token/owner pair. - -```typescript -const allowance = await merchant.payment.getRefundAllowance( - '0xTokenAddress...' as `0x${string}`, - '0xMerchantAddress...' as `0x${string}`, -) -console.log('Allowance:', allowance) -``` - -## Query methods - -### payment.getAmounts - -Query the current capturable and refundable amounts for a payment. - -```typescript -const amounts = await merchant.payment.getAmounts(paymentInfo) - -console.log('Has collected:', amounts.hasCollectedPayment) -console.log('Capturable:', amounts.capturableAmount) -console.log('Refundable:', amounts.refundableAmount) -``` - -### payment.getState - -Returns a tuple of the payment's lifecycle position. - -```typescript -const [hasCollectedPayment, capturableAmount, refundableAmount] = - await merchant.payment.getState(paymentInfo) -``` - -### operator.getConfig - -Retrieve all slot addresses from the PaymentOperator contract. - -```typescript -const config = await merchant.operator.getConfig() - -console.log('Fee receiver:', config.feeReceiver) -console.log('Fee calculator:', config.feeCalculator) -console.log('Capture condition:', config.captureCondition) -``` - -### operator.getFeeAddresses - -Get the fee-related addresses. - -```typescript -const fees = await merchant.operator.getFeeAddresses() - -console.log('Operator fee calculator:', fees.operatorFeeCalculator) -console.log('Protocol fee config:', fees.protocolFeeConfig) -console.log('Protocol fee calculator:', fees.protocolFeeCalculator) -console.log('Operator fee recipient:', fees.operatorFeeRecipient) -console.log('Protocol fee recipient:', fees.protocolFeeRecipient) -``` - -### operator.calculateFees - -Calculate the full fee breakdown for a payment amount. - -```typescript -const fees = await merchant.operator.calculateFees(paymentInfo, 1_000_000n) - -console.log('Operator fee bps:', fees.operatorFeeBps) -console.log('Protocol fee bps:', fees.protocolFeeBps) -console.log('Total fee bps:', fees.totalFeeBps) -console.log('Operator fee:', fees.operatorFeeAmount) -console.log('Protocol fee:', fees.protocolFeeAmount) -console.log('Total fee:', fees.totalFeeAmount) -console.log('Net amount:', fees.netAmount) -``` - -## Capture vs refund decision flow - -```mermaid -flowchart TD - A[Payment in Escrow] --> B{Check payment.getAmounts} - B --> C{capturableAmount > 0?} - C -->|Yes| D{Has refund request?} - C -->|No| E[Nothing to capture] - D -->|No| F[Safe to capture] - D -->|Yes| G{Approve refund?} - F --> H["payment.capture(paymentInfo, amount)"] - G -->|Yes| I["payment.voidPayment(paymentInfo)"] - G -->|No| J[Deny request, then capture] - J --> H -``` - -## Next steps - - - - Process incoming refund requests with deny workflows. - - - Watch for real-time payment and refund events. - - - Understand the underlying PaymentOperator contract methods. - - - Forward escrow settlements to an arbiter service. - - diff --git a/sdk/merchant/refund-handling.mdx b/sdk/merchant/refund-handling.mdx index 48f1e76..8d3723c 100644 --- a/sdk/merchant/refund-handling.mdx +++ b/sdk/merchant/refund-handling.mdx @@ -253,16 +253,10 @@ sequenceDiagram ## Next steps - - Watch for refund requests in real-time instead of polling. - Capture funds, charge, and query escrow state. RefundRequest contract details and state machine. - - How arbiters process refund requests from the other side. - diff --git a/sdk/merchant/subscriptions.mdx b/sdk/merchant/subscriptions.mdx deleted file mode 100644 index b0be4d1..0000000 --- a/sdk/merchant/subscriptions.mdx +++ /dev/null @@ -1,133 +0,0 @@ ---- -title: "Merchant events" -description: "Subscribe to real-time refund, capture, and fee events" -icon: "bell" ---- - -The merchant client provides real-time event subscriptions through the `watch` action group. Each method returns an unsubscribe function for cleanup. - -## watch.onPayment - -Watch for payment lifecycle events: `AuthorizeExecuted`, `ChargeExecuted`, and `CaptureExecuted` on the PaymentOperator contract. - -```typescript -const unwatch = merchant.watch.onPayment((logs) => { - for (const log of logs) { - console.log('Payment event:', log.eventName) - } -}) - -// Later: stop watching -unwatch() -``` - -### Example: revenue tracking - -```typescript -let totalCaptured = 0n - -const unwatch = merchant.watch.onPayment((logs) => { - for (const log of logs) { - if (log.eventName === 'CaptureExecuted') { - const amount = log.args?.amount ?? 0n - totalCaptured += amount - console.log('Capture: +', amount, 'Total:', totalCaptured) - } - } -}) -``` - -## watch.onRefundRequest - -Watch for refund request lifecycle events on the RefundRequest contract. This is a no-op if `refundRequestAddress` was not provided in the client config. - -```typescript -const unwatch = merchant.watch.onRefundRequest((logs) => { - for (const log of logs) { - console.log('Refund event:', log.eventName) - } -}) - -unwatch() -``` - -### Example: auto-respond to small refund requests - -```typescript -import { RefundRequestStatus } from '@x402r/sdk' - -const AUTO_APPROVE_THRESHOLD = 5_000_000n // 5 USDC - -const unwatch = merchant.watch.onRefundRequest(async (logs) => { - for (const log of logs) { - const amount = log.args?.amount - console.log('New refund request, amount:', amount) - - if (amount && amount < AUTO_APPROVE_THRESHOLD) { - console.log('Auto-approving small refund request') - // Look up the paymentInfo from your database and call - // merchant.payment.voidPayment(paymentInfo) - } else { - console.log('Queuing for manual review') - } - } -}) -``` - -## watch.onRefundExecuted - -Watch for refund execution events: `VoidExecuted` and `RefundExecuted`. - -```typescript -const unwatch = merchant.watch.onRefundExecuted((logs) => { - for (const log of logs) { - console.log('Refund executed:', log.eventName) - } -}) - -unwatch() -``` - -## watch.onFeeDistribution - -Watch for `FeesDistributed` events on the PaymentOperator contract. - -```typescript -const unwatch = merchant.watch.onFeeDistribution((logs) => { - for (const log of logs) { - console.log('Fees distributed:', log) - } -}) - -unwatch() -``` - -## Event types reference - -| Method | Events Watched | Contract | -|--------|---------------|----------| -| `watch.onPayment` | `AuthorizeExecuted`, `ChargeExecuted`, `CaptureExecuted` | PaymentOperator | -| `watch.onRefundRequest` | All RefundRequest ABI events | RefundRequest | -| `watch.onRefundExecuted` | `VoidExecuted`, `RefundExecuted` | PaymentOperator | -| `watch.onFeeDistribution` | `FeesDistributed` | PaymentOperator | - - -For reliable real-time delivery, configure your `publicClient` with a [WebSocket transport](https://viem.sh/docs/clients/transports/websocket). - - -## Next steps - - - - Capture funds, charge, and query escrow state. - - - Learn about dispute resolution from the arbiter perspective. - - - Process refund requests with deny workflows. - - - See how payers subscribe to the same events. - - diff --git a/sdk/overview.mdx b/sdk/overview.mdx index 088e8b7..929c634 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -34,7 +34,7 @@ bun add @x402r/sdk ``` -`@x402r/sdk` is the only package most developers need. It includes role-scoped client factories, 8 action groups (payment, escrow, refund, evidence, freeze, query, operator, watch), an `.extend()` plugin system, and [ERC-8004 helpers](/sdk/helpers/erc8004) for extracting agent identity and reputation data from x402 extension responses. +`@x402r/sdk` is the only package most developers need. It includes role-scoped client factories, 8 action groups (payment, escrow, refund, evidence, freeze, query, operator, watch), an `.extend()` plugin system, and ERC-8004 helpers for extracting agent identity and reputation data from x402 extension responses. For low-level access to contract ABIs and deploy utilities: @@ -84,15 +84,9 @@ bunx @x402r/cli pay [options] Deploy an operator, accept a payment, capture funds from escrow. - - High-level client with role-scoped factories (`createPayerClient`, `createMerchantClient`, `createArbiterClient`) and the generic `createX402r`. - Capture funds from escrow, charge directly, void, and process refunds using `createMerchantClient`. - - Resolve disputes and act as a captureAuthorizer using `createArbiterClient`. - Wallet-agnostic one-shot payments from the command line or scripts. diff --git a/sdk/payer.mdx b/sdk/payer.mdx deleted file mode 100644 index 8466f42..0000000 --- a/sdk/payer.mdx +++ /dev/null @@ -1,150 +0,0 @@ ---- -title: "Payer Guide" -description: "Check payment state, request a refund, freeze a payment, and submit evidence." -icon: "user" ---- - -### Prerequisites - -* A wallet with ETH on Base Sepolia for gas ([faucet](https://www.alchemy.com/faucets/base-sepolia)) -* Node.js 18+ and npm -* An authorized payment on an x402r operator (see [Merchant Guide](/sdk/merchant)) - - -There are pre-configured [examples in the x402r-sdk repo](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples), including [payer examples](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/payer) and a full [dispute resolution scenario](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/scenarios/dispute-resolution.ts). - - -### 1. Install Dependencies - - -```bash npm -npm install @x402r/sdk -``` -```bash pnpm -pnpm add @x402r/sdk -``` -```bash bun -bun add @x402r/sdk -``` - - -### 2. Create a Payer Client - -```typescript -import { createPublicClient, createWalletClient, http } from 'viem' -import { baseSepolia } from 'viem/chains' -import { privateKeyToAccount } from 'viem/accounts' -import { createPayerClient } from '@x402r/sdk' - -const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`) - -const payer = createPayerClient({ - publicClient: createPublicClient({ chain: baseSepolia, transport: http() }), - walletClient: createWalletClient({ - account, - chain: baseSepolia, - transport: http(), - }), - operatorAddress: '0x...', // your operator address - refundRequestAddress: '0x...', // from deploy result - refundRequestEvidenceAddress: '0x...', - escrowPeriodAddress: '0x...', - freezeAddress: '0x...', -}) -``` - - -All payer actions (refund, freeze, evidence) must happen during the escrow period. Once escrow expires, the merchant can capture. - - -### 3. Check Payment State - -```typescript -import type { PaymentInfo } from '@x402r/sdk' - -// paymentInfo is the same struct used during authorization -const paymentInfo: PaymentInfo = { /* ... */ } - -const amounts = await payer.payment.getAmounts(paymentInfo) -console.log('Collected:', amounts.hasCollectedPayment) -console.log('Capturable:', amounts.capturableAmount) -console.log('Refundable:', amounts.refundableAmount) - -const inEscrow = await payer.escrow?.isDuringEscrow(paymentInfo) -console.log('In escrow:', inEscrow) -``` - -### 4. Request a Refund - -Request a refund while the payment is still in escrow: - -```typescript -// Check if a refund request already exists -const hasExisting = await payer.refund?.has(paymentInfo) -if (hasExisting) { - console.log('Refund already requested') -} else { - const tx = await payer.refund?.request(paymentInfo, 1_000_000n) // 1 USDC - console.log('Refund requested:', tx) -} - -// Check refund status -const status = await payer.refund?.getStatus(paymentInfo) -console.log('Status:', status) // 0 = Pending, 1 = Approved, 2 = Denied, 3 = Cancelled, 4 = Refused -``` - -### 5. Freeze a Payment (Optional) - - -`freeze()` blocks the merchant from releasing until the arbiter unfreezes. Only use when you need to prevent a capture during investigation. - - -```typescript -const frozen = await payer.freeze?.isFrozen(paymentInfo) -if (!frozen) { - const tx = await payer.freeze?.freeze(paymentInfo) - console.log('Payment frozen:', tx) -} -``` - -### 6. Submit Evidence (Optional) - -Attach evidence to a refund request. Evidence is stored on-chain as IPFS CIDs: - -```typescript -// Upload your evidence to IPFS first, then submit the CID -const tx = await payer.evidence?.submit(paymentInfo, 'QmYourEvidenceCID...') -console.log('Evidence submitted:', tx) - -// Read back evidence -const count = await payer.evidence?.count(paymentInfo) -console.log('Evidence entries:', count) - -for (let i = 0n; i < count!; i++) { - const entry = await payer.evidence?.get(paymentInfo, i) - console.log(` [${i}] CID: ${entry?.cid} from ${entry?.submitter}`) -} -``` - -### 7. Cancel a Refund Request (Optional) - -If the issue is resolved directly with the merchant: - -```typescript -const cancelTx = await payer.refund?.cancel(paymentInfo) -console.log('Refund cancelled:', cancelTx) -``` - -## Next Steps - - - - What happens after you submit a refund request. - - - Escrow timing, capture, and the payment lifecycle. - - - Full dispute resolution scenario end-to-end. - - From debe731da50a9d6e150e0681fbe6ceb85822218d Mon Sep 17 00:00:00 2001 From: A1igator Date: Tue, 12 May 2026 00:24:00 -0700 Subject: [PATCH 27/37] docs: delete x402-integration/comparison.mdx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comparison page covered the same ground as the authCapture spec's "Key Differences" + "vs Exact Scheme" sections. Dropping the standalone page and folding nothing in (the spec already has what's needed). - Removed page + docs.json nav entry - Removed three dangling Card refs (x402-integration/overview, x402-integration/auth-capture-scheme) - Added /x402-integration/comparison redirect → auth-capture-scheme Co-Authored-By: Claude Opus 4.7 (1M context) --- docs.json | 7 +- x402-integration/auth-capture-scheme.mdx | 6 - x402-integration/comparison.mdx | 376 ----------------------- x402-integration/overview.mdx | 4 - 4 files changed, 5 insertions(+), 388 deletions(-) delete mode 100644 x402-integration/comparison.mdx diff --git a/docs.json b/docs.json index 51d4627..d2fb8a9 100644 --- a/docs.json +++ b/docs.json @@ -17,8 +17,7 @@ "group": "Getting Started", "pages": [ "index", - "x402-integration/overview", - "x402-integration/comparison" + "x402-integration/overview" ] }, { @@ -180,6 +179,10 @@ { "source": "/contracts/recorders/:slug", "destination": "/contracts/hooks/:slug" + }, + { + "source": "/x402-integration/comparison", + "destination": "/x402-integration/auth-capture-scheme" } ] } \ No newline at end of file diff --git a/x402-integration/auth-capture-scheme.mdx b/x402-integration/auth-capture-scheme.mdx index e0d4533..ece0922 100644 --- a/x402-integration/auth-capture-scheme.mdx +++ b/x402-integration/auth-capture-scheme.mdx @@ -403,8 +403,6 @@ The escrow contract enforces invariants on-chain: The `authCapture` scheme adds an authorization step before settlement (or refundability for single-shot). For simple immediate payments where trust and refundability aren't concerns, the `exact` scheme remains more efficient. -See [Comparison](/x402-integration/comparison) for detailed trade-offs. - ## Next Steps @@ -412,10 +410,6 @@ See [Comparison](/x402-integration/comparison) for detailed trade-offs. Understand why escrow is needed for HTTP payments. - - Compare authCapture vs exact schemes in detail. - - Learn about the escrow and captureAuthorizer contracts. diff --git a/x402-integration/comparison.mdx b/x402-integration/comparison.mdx deleted file mode 100644 index 09c576a..0000000 --- a/x402-integration/comparison.mdx +++ /dev/null @@ -1,376 +0,0 @@ ---- -title: "Escrow vs Exact" -description: "Detailed comparison of escrow and exact payment schemes" -icon: "scale-balanced" ---- - -## At a Glance - - - - **Immediate settlement** - - Payment happens instantly when request is made. Simple, fast, minimal overhead. - - Best for trusted services and low-value transactions. - - - - **Deferred settlement** - - Funds locked until conditions are met. Authorization separate from capture. - - Best for high-value, usage-based, or disputed transactions. - - - -## Feature Comparison - -| Feature | exact | escrow | -|---------|-------|--------| -| **Settlement Timing** | Immediate | Deferred (conditional) | -| **Payer Protection** | None (payment final) | Refund possible until capture | -| **Receiver Risk** | No risk (paid upfront) | Must wait for capture | -| **Gas Cost** | Single transaction | Two transactions (auth + capture) | -| **Complexity** | Minimal (direct transfer) | Higher (operator logic) | -| **Variable Pricing** | Not supported | Supported (authorize max, capture actual) | -| **Dispute Resolution** | Not possible | Supported via operator | -| **Multi-Request Sessions** | Every request needs signature | One auth, multiple captures | -| **Trust Required** | High (payment irreversible) | Lower (escrow protects payer) | - -## When to Use Each - -### Use `exact` when: - - - - For small purchases where the cost of a dispute exceeds the transaction value. - - **Example:** $0.01 API call - not worth the escrow overhead - - - - When you trust the service provider and don't need refund protection. - - **Example:** Paying your own infrastructure or established services - - - - Service is delivered instantly and verifiable on the spot. - - **Example:** Static content, database lookups, instant responses - - - - Single transaction is cheaper than two (auth + capture). - - **Example:** High-frequency micro-transactions where gas matters - - - -### Use `escrow` when: - - - - Significant amounts where you need recourse if service fails. - - **Example:** $500 training job - you want protection if it fails - - - - Usage-based billing where you don't know the exact amount upfront. - - **Example:** LLM API calls charged by tokens (authorize $10, use $6.50, refund $3.50) - - - - Tasks that take hours or days to complete. - - **Example:** Video rendering, data processing, training jobs - - - - Services where quality is subjective or verification is needed. - - **Example:** Freelance work, custom deliverables, SLA-based services - - - - Many small requests under one authorization. - - **Example:** 1,000 API calls at $0.01 each = one $10 auth, periodic captures - - - -## Cost Analysis - -### exact Scheme - - -**Gas Cost: 1 transaction** -- ERC-20 transfer: ~50k gas -- **Total: ~50,000 gas** - -**Example (Base, 0.001 gwei gas price):** -- Gas cost: ~$0.00005 -- For $10 payment: 0.0005% overhead - - -### escrow Scheme - - -**Gas Cost: 2 transactions** -- authorize(): ~150k gas -- capture(): ~80k gas -- **Total: ~230,000 gas** - -**Example (Base, 0.001 gwei gas price):** -- Gas cost: ~$0.00023 -- For $10 payment: 0.0023% overhead - - - -**Amortization:** For multi-request sessions, the auth cost is amortized across many captures. 1 auth + 10 captures is cheaper than 10 exact payments. - - -## Security Comparison - -### exact Scheme - -| Risk | Severity | Mitigation | -|------|----------|-----------| -| Service non-delivery | **High** | None - payment is final | -| Overcharging | **Medium** | Verify amount before signing | -| Malicious server | **High** | Trust required | - -**Trust Model:** You must fully trust the service provider. Once payment is sent, you have no recourse. - -### escrow Scheme - -| Risk | Severity | Mitigation | -|------|----------|-----------| -| Service non-delivery | **Low** | Refund before capture | -| Overcharging | **None** | `maxAmount` enforced on-chain | -| Malicious operator | **Medium** | Choose trusted operators, set expiry | -| Operator disappeared | **Low** | Payer can reclaim after `authorizationExpiry` | - -**Trust Model:** You must trust the operator contract logic, but funds are protected by on-chain invariants. - - -**Operator Selection Critical:** The operator contract controls when funds are released. A malicious or buggy operator can lock funds. Always audit operator code or use verified implementations. - - -## User Experience - -### exact Scheme Flow - -``` -1. Server: "Pay $10" -2. Client: [Signs payment] -3. Client: [Sends request with signature] -4. Server: [Receives payment + delivers service] -5. Done -``` - -**Steps:** 3 (request → sign → deliver) - -**Latency:** Single round-trip - -### escrow Scheme Flow - -``` -1. Server: "Authorize up to $10" -2. Client: [Signs authorization] -3. Client: [Sends request with signature] -4. Server: [Locks $10 in escrow + delivers service] -5. Server: [Calls operator to capture actual amount] -6. Done -``` - -**Steps:** 4 (request → sign → deliver → capture) - -**Latency:** Service delivery same as exact, but capture happens async - - -**Perception:** From the user's perspective, escrow and exact feel the same during the request. The capture happens in the background. - - -## Example Scenarios - -### Scenario 1: Simple API Call - -**Service:** Weather API lookup -**Cost:** $0.01 -**Trust:** High (established provider) -**Decision:** ✅ **Use exact** - -Why: Low value, instant delivery, trusted service. Escrow overhead not worth it. - -### Scenario 2: LLM Agent Session - -**Service:** 100 GPT-4 calls -**Cost:** $0.05-$0.20 per call (variable) -**Trust:** Medium (new provider) -**Decision:** ✅ **Use escrow** - -Why: Variable pricing, multiple requests, moderate trust. Authorize $20 max, capture actual usage. - -### Scenario 3: Training Job - -**Service:** Train ML model on GPU cluster -**Cost:** $500 -**Duration:** 48 hours -**Decision:** ✅ **Use escrow** - -Why: High value, long-running, need verification before payment. Capture after job completes successfully. - -### Scenario 4: Freelance Work - -**Service:** Custom logo design -**Cost:** $200 -**Dispute Risk:** High (subjective quality) -**Decision:** ✅ **Use escrow with arbiter** - -Why: Subjective deliverable, need dispute resolution. Arbiter operator releases on approval or mediates disputes. - -### Scenario 5: Micro-Transaction Spam - -**Service:** Rate-limited API (1000 req/sec) -**Cost:** $0.0001 per request -**Volume:** 100,000 requests/day -**Decision:** ✅ **Use escrow with batch captures** - -Why: Too many requests to sign individually. Authorize $10 daily, server batches captures hourly. - -## Performance Comparison - -### Throughput - -| Metric | exact | escrow | -|--------|-------|--------| -| Requests/second | 1000+ | 1000+ (auth), 100+ (capture) | -| On-chain TPS impact | High (every request) | Low (periodic captures) | -| Signature overhead | Per-request | Per-session | - -### Latency - -| Phase | exact | escrow | -|-------|-------|--------| -| Request latency | +50ms (sign + verify) | +50ms (sign + verify) | -| Settlement latency | Immediate | Async (seconds to days) | -| Finality | Instant | After capture | - -## Hybrid Approach - -You can support **both schemes** and let clients choose: - -```json -{ - "x402Version": 2, - "accepts": [ - { - "scheme": "exact", - "network": "eip155:8453", - "amount": "10000000", - "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", - "payTo": "0xReceiver..." - }, - { - "scheme": "authCapture", - "network": "eip155:8453", - "amount": "10000000", - "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", - "payTo": "0xReceiver...", - "maxTimeoutSeconds": 60, - "extra": { - "name": "USDC", - "version": "2", - "captureAuthorizer": "0xCaptureAuthorizer...", - "captureDeadline": 1740758554, - "refundDeadline": 1741276954, - "feeRecipient": "0xFeeRecipient...", - "minFeeBps": 0, - "maxFeeBps": 500, - "autoCapture": false - } - } - ] -} -``` - -**Client decides based on:** -- Transaction value -- Trust level -- Gas price -- Urgency - -## Migration Strategy - -### From exact to authCapture - -Existing `exact` integrations can add `authCapture` support: - -1. Pick a captureAuthorizer (facilitator EOA or arbiter contract) -2. Add an `authCapture` entry to `accepts` array -3. Keep `exact` as fallback -4. Clients upgrade when ready - -### From authCapture to exact - -If escrow proves unnecessary: - -1. Add `exact` to `accepts` array -2. Monitor which scheme clients prefer -3. Deprecate `escrow` if unused - - -**Start with exact, add escrow when needed.** Don't over-engineer. Most simple services work fine with exact. - - -## Decision Tree - -```mermaid -flowchart TD - A{Transaction over $10?} - A -->|Yes| B[Consider escrow] - A -->|No| C{Variable usage?} - C -->|Yes| D[Use escrow] - C -->|No| E{Trust provider?} - E -->|Yes| F[Use exact] - E -->|No| G[Use escrow] - - B --> H{Check factors} - H -->|Long task| D - H -->|Quick task| F - H -->|Dispute risk| G - - style D fill:#f59e0b,stroke:#d97706,color:#fff - style F fill:#4f46e5,stroke:#4338ca,color:#fff - style G fill:#f59e0b,stroke:#d97706,color:#fff - - style A fill:#64748b,stroke:#475569,color:#fff - style B fill:#64748b,stroke:#475569,color:#fff - style C fill:#64748b,stroke:#475569,color:#fff - style E fill:#64748b,stroke:#475569,color:#fff - style H fill:#64748b,stroke:#475569,color:#fff -``` - -## Next Steps - - - - Learn about x402 and why escrow matters. - - - - Complete technical specification. - - - - Understand operator implementations. - - - - Build your first payment flow. - - diff --git a/x402-integration/overview.mdx b/x402-integration/overview.mdx index bfd4acc..c256689 100644 --- a/x402-integration/overview.mdx +++ b/x402-integration/overview.mdx @@ -209,10 +209,6 @@ Smart contract that controls capture/void logic. Different operators enable diff Complete technical specification for the authCapture payment scheme. - - Detailed comparison of escrow vs exact schemes. - - Understand the escrow and operator contracts. From 71256fcadc7c5d8994c8f9b67800d68f7aab4d12 Mon Sep 17 00:00:00 2001 From: A1igator Date: Tue, 12 May 2026 00:31:41 -0700 Subject: [PATCH 28/37] docs(sdk): nav-label disambiguation + typed Parameters/Returns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two remaining review items that apply post-orphan-trim: #6 (docs.json SDK group renames + create-client promotion): - Promote sdk/create-client to second entry under "Getting Started" (was buried under "Reference"). Reference group now just CLI + Examples. - sidebarTitle frontmatter added so nav labels disambiguate: - sdk/merchant/getting-started → "Express server setup" - sdk/merchant/quickstart → "Merchant client" - sdk/helpers/forward-to-arbiter → "forwardToArbiter helper" #4 (typed Returns/Parameters per method, viem-style): - sdk/merchant/quickstart.mdx: added Parameters + Returns sections under each of capture, voidPayment, charge, refund, getAmounts, getState, operator.getConfig, operator.getFeeAddresses, operator.calculateFees. Tables list every arg with type and description; Returns names the typed result + key fields. - sdk/merchant/refund-handling.mdx: same pass across refund.has, refund.getStatus, refund.get, refund.getByKey, refund.getReceiverRequests, refund.getOperatorRequests, refund.deny, payment.voidPayment, freeze.isFrozen, freeze.unfreeze. Existing bottom-of-page summary table kept as quick reference. Skipped #2 (split deploy-operator.mdx into marketplace + delivery sibling pages): adding files contradicts the orphan-trim direction we just landed. Verified: npx mintlify@latest broken-links passes. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs.json | 2 +- sdk/helpers/forward-to-arbiter.mdx | 1 + sdk/merchant/getting-started.mdx | 1 + sdk/merchant/quickstart.mdx | 191 +++++++++++++++++++---------- sdk/merchant/refund-handling.mdx | 158 +++++++++++++++--------- 5 files changed, 227 insertions(+), 126 deletions(-) diff --git a/docs.json b/docs.json index d2fb8a9..4f340f4 100644 --- a/docs.json +++ b/docs.json @@ -94,6 +94,7 @@ "group": "Getting Started", "pages": [ "sdk/overview", + "sdk/create-client", "sdk/deploy-operator" ] }, @@ -118,7 +119,6 @@ "group": "Reference", "pages": [ "sdk/cli", - "sdk/create-client", "sdk/examples" ] } diff --git a/sdk/helpers/forward-to-arbiter.mdx b/sdk/helpers/forward-to-arbiter.mdx index 465f09e..6959f63 100644 --- a/sdk/helpers/forward-to-arbiter.mdx +++ b/sdk/helpers/forward-to-arbiter.mdx @@ -1,5 +1,6 @@ --- title: "forwardToArbiter()" +sidebarTitle: "forwardToArbiter helper" description: "Forward settlement data to an arbiter service for quality evaluation" icon: "arrow-right" --- diff --git a/sdk/merchant/getting-started.mdx b/sdk/merchant/getting-started.mdx index ff9aeab..07e8e2a 100644 --- a/sdk/merchant/getting-started.mdx +++ b/sdk/merchant/getting-started.mdx @@ -1,5 +1,6 @@ --- title: "Merchant Server Quickstart" +sidebarTitle: "Express server setup" description: "Accept escrow-backed refundable payments on your Express server in 5 minutes" icon: "store" --- diff --git a/sdk/merchant/quickstart.mdx b/sdk/merchant/quickstart.mdx index a8a7baa..f29d591 100644 --- a/sdk/merchant/quickstart.mdx +++ b/sdk/merchant/quickstart.mdx @@ -1,5 +1,6 @@ --- title: "Merchant SDK" +sidebarTitle: "Merchant client" description: "Capture funds, charge payments, process refunds, and query escrow state" icon: "rocket" --- @@ -49,149 +50,203 @@ const merchant = createMerchantClient({ }) ``` -## Capture funds from escrow +## payment.capture -Use `payment.capture()` to transfer escrowed funds to the receiver (merchant). The `amount` parameter is required. +Transfer escrowed funds to the receiver. Specify a smaller amount than `paymentInfo.maxAmount` for a partial capture; the remainder stays in escrow. ```typescript -// Capture 10 USDC (6 decimals) from escrow const tx = await merchant.payment.capture(paymentInfo, 10_000_000n) -console.log('Captured:', tx) ``` -For partial captures, specify a smaller amount. The remaining funds stay in escrow. +**Parameters** -```typescript -// Capture 3 USDC of a 10 USDC escrow -const tx = await merchant.payment.capture(paymentInfo, 3_000_000n) -console.log('Partial capture:', tx) +| Name | Type | Description | +|---|---|---| +| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | +| `amount` | `bigint` | Atomic units to capture (must be ≤ `paymentInfo.maxAmount`) | +| `data` | `Hex` _(optional)_ | Pass-through data for the operator's pre/post action plugins | -// Check what remains -const amounts = await merchant.payment.getAmounts(paymentInfo) -console.log('Remaining in escrow:', amounts.capturableAmount) // 7000000n -``` +**Returns** `Promise`, the settlement transaction hash. Always query `payment.getAmounts()` first to determine the available capturable amount. -## Void while in escrow +## payment.voidPayment -Use `payment.voidPayment()` to return all escrowed funds to the payer before capture. Void is full-only: it empties the authorization in one transaction. +Return all escrowed funds to the payer before capture. Full-only: `void()` empties the authorization in one transaction. For a partial return, capture the portion you want to keep first, then void the remainder (or let it expire at `captureDeadline`). ```typescript const tx = await merchant.payment.voidPayment(paymentInfo) -console.log('Voided:', tx) ``` - -For a partial return (capture some, return the rest), call `payment.capture(paymentInfo, amount)` for the part you want to keep. The unused authorization can then be voided, or it will expire at `captureDeadline` and become reclaimable by the payer. - +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | +| `data` | `Hex` _(optional)_ | Pass-through data for the operator's pre/post action plugins | -## Charge directly +**Returns** `Promise`, the void transaction hash. -Use `payment.charge()` for non-escrow flows such as subscriptions or session-based payments. This pulls funds directly from the payer via a token collector (e.g., ERC-3009 `transferWithAuthorization`). +## payment.charge + +Non-escrow settlement for subscriptions or session-based payments. Pulls funds directly from the payer via a token collector (no escrow hold). ```typescript const tx = await merchant.payment.charge( paymentInfo, - 5_000_000n, // 5 USDC - '0xTokenCollector...' as `0x${string}`, // token collector contract - '0xSignatureData...' as `0x${string}`, // authorization data + 5_000_000n, + '0xTokenCollector...' as `0x${string}`, + '0xSignatureData...' as `0x${string}`, ) -console.log('Charged:', tx) ``` - -The `charge()` method is designed for recurring payments and session-based billing where funds are not pre-escrowed. The token collector contract handles the actual token transfer. - +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | +| `amount` | `bigint` | Atomic units to charge | +| `tokenCollector` | `Address` | Canonical token collector for the chosen `assetTransferMethod` | +| `collectorData` | `Hex` | Raw ERC-3009 signature or ABI-encoded Permit2 signature | -## Refund after capture (after capture) +**Returns** `Promise`, the charge transaction hash. -Use `payment.refund()` to refund funds that have already been released. This requires a token collector to source the refund from the merchant's balance. +## payment.refund + +Refund funds that have already been captured. Requires a token collector to pull funds from the merchant's balance. ```typescript const tx = await merchant.payment.refund( paymentInfo, - 5_000_000n, // 5 USDC to refund - '0xTokenCollector...' as `0x${string}`, // sources the refund - '0xSignatureData...' as `0x${string}`, // authorization data + 5_000_000n, + '0xTokenCollector...' as `0x${string}`, + '0xSignatureData...' as `0x${string}`, ) -console.log('Refund (after capture):', tx) ``` +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | +| `amount` | `bigint` | Atomic units to refund to the payer | +| `tokenCollector` | `Address` | Token collector that sources the refund (typically `ReceiverRefundCollector`) | +| `collectorData` | `Hex` | Data passed to the collector (e.g., receiver signature) | + +**Returns** `Promise`, the refund transaction hash. + -Refunds (after capture) require the merchant to have sufficient token balance. The token collector pulls funds from the merchant to return to the payer. +Refunds after capture require the merchant to have sufficient token balance and an approved allowance on the refund collector. -## Query methods - -### payment.getAmounts +## payment.getAmounts Query the current capturable and refundable amounts for a payment. ```typescript const amounts = await merchant.payment.getAmounts(paymentInfo) +``` -console.log('Capturable:', amounts.capturableAmount) -console.log('Refundable:', amounts.refundableAmount) +**Parameters** -if (amounts.capturableAmount > 0n) { - await merchant.payment.capture(paymentInfo, amounts.capturableAmount) -} -``` +| Name | Type | Description | +|---|---|---| +| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | + +**Returns** `Promise`: -### payment.getState +| Field | Type | Description | +|---|---|---| +| `hasCollectedPayment` | `boolean` | Whether the payment is collected on-chain | +| `capturableAmount` | `bigint` | Atomic units still capturable from escrow | +| `refundableAmount` | `bigint` | Atomic units still refundable | -Returns a tuple of the payment's lifecycle position. +## payment.getState + +Returns the payment's lifecycle position as a tuple. There is no `PaymentState` enum. ```typescript const [hasCollectedPayment, capturableAmount, refundableAmount] = await merchant.payment.getState(paymentInfo) ``` -### operator.getConfig +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | + +**Returns** `Promise`, `[hasCollectedPayment, capturableAmount, refundableAmount]`. + +## operator.getConfig Retrieve all slot addresses from the PaymentOperator contract. ```typescript const config = await merchant.operator.getConfig() - -console.log('Fee receiver:', config.feeReceiver) -console.log('Fee calculator:', config.feeCalculator) -console.log('Capture condition:', config.captureCondition) ``` -### operator.getFeeAddresses +**Returns** `Promise`, see `packages/core/src/actions/operator/types.ts`. Key fields: + +| Field | Type | Description | +|---|---|---| +| `escrow` | `Address` | Canonical AuthCaptureEscrow | +| `authorizeCondition` / `authorizeHook` | `Address` | Pre/post slots for `authorize` | +| `chargeCondition` / `chargeHook` | `Address` | Pre/post slots for `charge` | +| `captureCondition` / `captureHook` | `Address` | Pre/post slots for `capture` | +| `voidCondition` / `voidHook` | `Address` | Pre/post slots for `void` | +| `refundCondition` / `refundHook` | `Address` | Pre/post slots for `refund` | +| `feeCalculator` | `Address` | Per-operator fee calculator | +| `feeReceiver` | `Address` | Operator fee recipient | +| `protocolFeeConfig` | `Address` | Protocol fee config contract | -Get the fee-related addresses. +## operator.getFeeAddresses + +Fetch the fee-related addresses (subset of `getConfig` with both operator and protocol resolved). ```typescript const fees = await merchant.operator.getFeeAddresses() - -console.log('Operator fee calculator:', fees.operatorFeeCalculator) -console.log('Protocol fee config:', fees.protocolFeeConfig) -console.log('Protocol fee calculator:', fees.protocolFeeCalculator) -console.log('Operator fee recipient:', fees.operatorFeeRecipient) -console.log('Protocol fee recipient:', fees.protocolFeeRecipient) ``` -### operator.calculateFees +**Returns** `Promise`: + +| Field | Type | Description | +|---|---|---| +| `operatorFeeCalculator` | `Address` | Per-operator calculator | +| `protocolFeeConfig` | `Address` | Protocol fee config contract | +| `protocolFeeCalculator` | `Address` | Protocol-level calculator | +| `operatorFeeRecipient` | `Address` | Where operator fees flow | +| `protocolFeeRecipient` | `Address` | Where protocol fees flow | + +## operator.calculateFees Calculate the full fee breakdown for a payment amount. ```typescript const fees = await merchant.operator.calculateFees(paymentInfo, 1_000_000n) - -console.log('Operator fee bps:', fees.operatorFeeBps) -console.log('Protocol fee bps:', fees.protocolFeeBps) -console.log('Total fee bps:', fees.totalFeeBps) -console.log('Operator fee:', fees.operatorFeeAmount) -console.log('Protocol fee:', fees.protocolFeeAmount) -console.log('Total fee:', fees.totalFeeAmount) -console.log('Net amount:', fees.netAmount) ``` +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | +| `amount` | `bigint` | Atomic units to compute fees for | + +**Returns** `Promise`: + +| Field | Type | Description | +|---|---|---| +| `protocolFeeBps` | `bigint` | Protocol fee in basis points | +| `operatorFeeBps` | `bigint` | Operator fee in basis points | +| `totalFeeBps` | `bigint` | Sum of the two | +| `protocolFeeAmount` | `bigint` | Atomic units of protocol fee | +| `operatorFeeAmount` | `bigint` | Atomic units of operator fee | +| `totalFeeAmount` | `bigint` | Atomic units of total fee | +| `netAmount` | `bigint` | Amount remaining after fees | + ## Capture vs refund decision flow ```mermaid diff --git a/sdk/merchant/refund-handling.mdx b/sdk/merchant/refund-handling.mdx index 8d3723c..cabd3fd 100644 --- a/sdk/merchant/refund-handling.mdx +++ b/sdk/merchant/refund-handling.mdx @@ -10,16 +10,20 @@ The merchant client provides refund management through the `refund` and `freeze` ### refund.has -Check whether a payer has submitted a refund request for a specific payment. +Check whether a refund request exists for a payment. ```typescript const hasRequest = await merchant.refund?.has(paymentInfo) - -if (hasRequest) { - console.log('Refund request exists for this payment') -} ``` +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | + +**Returns** `Promise`. + ### refund.getStatus Retrieve the current status of a refund request. @@ -28,103 +32,132 @@ Retrieve the current status of a refund request. import { RefundRequestStatus } from '@x402r/sdk' const status = await merchant.refund?.getStatus(paymentInfo) - -switch (status) { - case RefundRequestStatus.Pending: - console.log('Awaiting your decision') - break - case RefundRequestStatus.Approved: - console.log('You approved this refund') - break - case RefundRequestStatus.Denied: - console.log('You denied this refund') - break - case RefundRequestStatus.Cancelled: - console.log('Payer cancelled the request') - break - case RefundRequestStatus.Refused: - console.log('Arbiter declined to rule') - break -} ``` +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | + +**Returns** `Promise`, `Pending` \| `Approved` \| `Denied` \| `Cancelled` \| `Refused`. + ### refund.get -Retrieve the complete refund request data, including the amount and status. +Retrieve the complete refund request data. ```typescript const request = await merchant.refund?.get(paymentInfo) - -console.log('Payment hash:', request?.paymentInfoHash) -console.log('Requested amount:', request?.amount) -console.log('Approved amount:', request?.approvedAmount) -console.log('Status:', request?.status) ``` +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | + +**Returns** `Promise`: + +| Field | Type | Description | +|---|---|---| +| `paymentInfoHash` | `Hex` | keccak256 of the payment info struct | +| `amount` | `bigint` | Amount the payer requested | +| `approvedAmount` | `bigint` | Amount actually executed (0 until approved) | +| `status` | `RefundRequestStatus` | Lifecycle state | + ### refund.getByKey Look up a refund request directly by its payment info hash. ```typescript const request = await merchant.refund?.getByKey(paymentInfoHash) -console.log('Amount:', request?.amount) -console.log('Status:', request?.status) ``` +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `paymentInfoHash` | `Hex` | keccak256 of the payment info struct | + +**Returns** `Promise`. + ## Paginated refund request listing ### refund.getReceiverRequests -Retrieve paginated refund request data for a receiver address. +Retrieve paginated refund requests for a receiver address. ```typescript -const requests = await merchant.refund?.getReceiverRequests( - receiverAddress, - 0n, // offset - 10n, // count -) - -for (const request of requests ?? []) { - console.log('Amount:', request.amount, 'Status:', request.status) -} +const requests = await merchant.refund?.getReceiverRequests(receiverAddress, 0n, 10n) ``` +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `receiver` | `Address` | Receiver address to query | +| `offset` | `bigint` | Index offset | +| `count` | `bigint` | Max entries to return | + +**Returns** `Promise`. + ### refund.getOperatorRequests Retrieve paginated refund requests across all payments on an operator. ```typescript -const requests = await merchant.refund?.getOperatorRequests( - merchant.config.operatorAddress, - 0n, - 10n, -) +const requests = await merchant.refund?.getOperatorRequests(operatorAddress, 0n, 10n) ``` +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `operator` | `Address` | Operator address to query | +| `offset` | `bigint` | Index offset | +| `count` | `bigint` | Max entries to return | + +**Returns** `Promise`. + ## Refund request actions ### refund.deny -Deny a pending refund request. This changes the request status to `Denied`. +Deny a pending refund request. Status becomes `Denied`. ```typescript const tx = await merchant.refund?.deny(paymentInfo) -console.log('Refund denied:', tx) ``` +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | + +**Returns** `Promise`. + -If you deny a request, the payer may escalate to an arbiter for dispute resolution. Consider providing a reason off-chain to reduce escalation risk. +If you deny a request, the payer may escalate to an arbiter. Provide a reason off-chain to reduce escalation risk. ### payment.voidPayment -To approve and execute a refund, call `payment.voidPayment()`. This both auto-approves the pending RefundRequest and transfers funds back to the payer. +To approve and execute a refund, call `payment.voidPayment()`. The operator's `VOID_POST_ACTION_HOOK` (RefundRequest) auto-flips the request status to `Approved`. ```typescript const tx = await merchant.payment.voidPayment(paymentInfo) -console.log('Refund approved and executed:', tx) ``` +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | +| `data` | `Hex` _(optional)_ | Pass-through data for pre/post action plugins | + +**Returns** `Promise`. + `voidPayment()` auto-approves the pending RefundRequest. There is no undo. @@ -133,25 +166,36 @@ console.log('Refund approved and executed:', tx) ### freeze.isFrozen -Check whether a payment has been frozen by the payer. Frozen payments cannot be released until unfrozen. +Check whether a payment is frozen. Frozen payments cannot be captured until unfrozen. ```typescript const frozen = await merchant.freeze?.isFrozen(paymentInfo) - -if (frozen) { - console.log('Payment is frozen, cannot capture until unfrozen') -} ``` +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | + +**Returns** `Promise`. + ### freeze.unfreeze Remove a freeze on a payment. Only the receiver (merchant) or an authorized party can unfreeze. ```typescript const tx = await merchant.freeze?.unfreeze(paymentInfo) -console.log('Payment unfrozen:', tx) ``` +**Parameters** + +| Name | Type | Description | +|---|---|---| +| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | + +**Returns** `Promise`. + ## Complete refund workflow Here is a full workflow showing how to detect a refund request, review it, make a decision, and execute the refund if approved. From 2d7c638e7d425661f5ede1230b345ca47d9e2bac Mon Sep 17 00:00:00 2001 From: A1igator Date: Tue, 12 May 2026 00:38:44 -0700 Subject: [PATCH 29/37] docs: address PR #35 re-review (4270067533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regressions from c8a5fc6: - sdk/deploy-operator.mdx: phantom UsdcTvlLimit row replaced with (none); stray comma in env-var Default column replaced with (none) - contracts/conditions/combinators.mdx, conditions/custom.mdx, hooks/combinator.mdx: revert SHORT→LONG name mistake in deploy-input code samples (config.captureCondition → config.capturePreActionCondition; config.authorizeHook → config.authorizePostActionHook). OperatorSlots read shape from getConfig still uses SHORT names everywhere else. Still-stale (in-nav files only): - contracts/conditions/{payer,receiver,always-true}.mdx: dual Sepolia/Mainnet address tables replaced with single CREATE2 canonical address each (matches packages/core/src/config/index.ts). - sdk/delivery-arbiter.mdx: arbiter.payment.capture isn't on the role-narrowed ArbiterClient surface — switch the snippet to createX402r() directly with a one-line note explaining why. - sdk/merchant/refund-handling.mdx: paginated list return type fixed to { keys, total } + getByKey loop; removed inline refund.deny, freeze.unfreeze, and refund.getOperatorRequests sections (not on MerchantClient per packages/sdk/src/types.ts); workflow + mermaid + bottom method table updated accordingly. - sdk/examples.mdx: example Cards repointed to the dirs that actually exist on x402r-sdk@main (payer, merchant, arbiter, scenarios, shared). Dropped facilitator/basic, deploy-operator, servers/*, dev-tools/* cards (those paths 404 on main). Items the review flagged on now-deleted orphan pages (sdk/concepts, sdk/arbiter/*, sdk/client/*, sdk/merchant.mdx, sdk/merchant/payment-operations) are intentionally not reapplied — the files were deleted in commit a8cb139 (orphan trim) and won't be re-added. Verified: npx mintlify@latest broken-links passes. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/conditions/always-true.mdx | 6 +- contracts/conditions/combinators.mdx | 8 +-- contracts/conditions/custom.mdx | 2 +- contracts/conditions/payer.mdx | 6 +- contracts/conditions/receiver.mdx | 6 +- contracts/hooks/combinator.mdx | 2 +- sdk/delivery-arbiter.mdx | 6 +- sdk/deploy-operator.mdx | 4 +- sdk/examples.mdx | 43 ++++------- sdk/merchant/refund-handling.mdx | 102 +++++++++------------------ 10 files changed, 64 insertions(+), 121 deletions(-) diff --git a/contracts/conditions/always-true.mdx b/contracts/conditions/always-true.mdx index ebb7fdc..c02ca00 100644 --- a/contracts/conditions/always-true.mdx +++ b/contracts/conditions/always-true.mdx @@ -8,11 +8,9 @@ icon: "check" AlwaysTrueCondition allows anyone to call the action, no restrictions applied. -**Type:** Singleton (deployed once, reused by all operators) +**Type:** Singleton, CREATE2 (deployed once, reused by all operators) -**Address (Base Sepolia):** `0x785cC83DEa3d46D5509f3bf7496EAb26D42EE610` - -**Address (Base Mainnet):** `0xc9BbA6A2CF9838e7Dd8c19BC8B3BAC620B9D8178` +**Address (all supported chains):** `0x2ef2A6162aEF9Df1022ff51c011af94D99AB4904` ## Logic diff --git a/contracts/conditions/combinators.mdx b/contracts/conditions/combinators.mdx index 83811c0..609e2c1 100644 --- a/contracts/conditions/combinators.mdx +++ b/contracts/conditions/combinators.mdx @@ -19,7 +19,7 @@ const comboAddress = await andConditionFactory.write.deploy([ ]); // Use in operator config -config.captureCondition = comboAddress; +config.capturePreActionCondition = comboAddress; ``` **Example:** Capture requires receiver AND escrow period passed. @@ -34,7 +34,7 @@ const comboAddress = await orConditionFactory.write.deploy([ [RECEIVER_CONDITION, ARBITER_CONDITION] ]); -config.captureCondition = comboAddress; +config.capturePreActionCondition = comboAddress; ``` **Example:** Either receiver or arbiter can capture. @@ -47,7 +47,7 @@ Inverts a condition (`!A`). // Anyone EXCEPT payer can call const comboAddress = await notConditionFactory.write.deploy([PAYER_CONDITION]); -config.captureCondition = comboAddress; +config.capturePreActionCondition = comboAddress; ``` **Example:** Prevent payer from releasing their own payment. @@ -66,7 +66,7 @@ const capturePreActionCondition = await andConditionFactory.write.deploy([ [receiverOrArbiter, ESCROW_PERIOD_ADDRESS] ]); -config.captureCondition = capturePreActionCondition; +config.capturePreActionCondition = capturePreActionConditionAddress; ``` **Logic Tree:** diff --git a/contracts/conditions/custom.mdx b/contracts/conditions/custom.mdx index 4bd3630..7c9229f 100644 --- a/contracts/conditions/custom.mdx +++ b/contracts/conditions/custom.mdx @@ -51,7 +51,7 @@ Usage, deploy via a factory and use in operator config: const businessHours = await timeOfDayConditionFactory.write.deploy([9, 17]); // Use in operator config -config.captureCondition = businessHours; +config.capturePreActionCondition = businessHours; ``` ## Security Checklist diff --git a/contracts/conditions/payer.mdx b/contracts/conditions/payer.mdx index 7de29c7..54c9d9f 100644 --- a/contracts/conditions/payer.mdx +++ b/contracts/conditions/payer.mdx @@ -8,11 +8,9 @@ icon: "user" PayerCondition is a singleton condition that restricts an action to the payment's payer address. -**Type:** Singleton (deployed once, reused by all operators) +**Type:** Singleton, CREATE2 (deployed once, reused by all operators) -**Address (Base Sepolia):** `0xBAF68176FF94CAdD403EF7FbB776bbca548AC09D` - -**Address (Base Mainnet):** `0xb33D6502EdBbC47201cd1E53C49d703EC0a660b8` +**Address (all supported chains):** `0x586486394C38A2a7d36B16a3FDaF366cd202d823` ## Logic diff --git a/contracts/conditions/receiver.mdx b/contracts/conditions/receiver.mdx index 8b3d05a..6abafff 100644 --- a/contracts/conditions/receiver.mdx +++ b/contracts/conditions/receiver.mdx @@ -8,11 +8,9 @@ icon: "store" ReceiverCondition is a singleton condition that restricts an action to the payment's receiver address. -**Type:** Singleton (deployed once, reused by all operators) +**Type:** Singleton, CREATE2 (deployed once, reused by all operators) -**Address (Base Sepolia):** `0x12EDefd4549c53497689067f165c0f101796Eb6D` - -**Address (Base Mainnet):** `0xed02d3E5167BCc9582D851885A89b050AB816a56` +**Address (all supported chains):** `0x321651df4593DA57C413579c5b611D1A90168a3A` ## Logic diff --git a/contracts/hooks/combinator.mdx b/contracts/hooks/combinator.mdx index befd6fd..62a0802 100644 --- a/contracts/hooks/combinator.mdx +++ b/contracts/hooks/combinator.mdx @@ -17,7 +17,7 @@ const comboAddress = await hookCombinatorFactory.write.deploy([ [escrowPeriodAddress, paymentIndexRecorderHookAddress] // Records auth time + payment index ]); -config.authorizeHook = comboAddress; +config.authorizePostActionHook = comboAddress; ``` ## Behavior diff --git a/sdk/delivery-arbiter.mdx b/sdk/delivery-arbiter.mdx index 8cdd449..1cba42d 100644 --- a/sdk/delivery-arbiter.mdx +++ b/sdk/delivery-arbiter.mdx @@ -30,15 +30,17 @@ There is a full [AI garbage detector example](https://github.com/BackTrackCo/arb + The role-narrowed `createArbiterClient` exposes `payment.voidPayment`, `payment.getState`, and `payment.getAmounts`. Capturing requires the full surface, so use `createX402r()` directly: + ```typescript import { createPublicClient, createWalletClient, http } from 'viem' import { baseSepolia } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' - import { createArbiterClient } from '@x402r/sdk' + import { createX402r } from '@x402r/sdk' const account = privateKeyToAccount(process.env.ARBITER_PRIVATE_KEY as `0x${string}`) - const arbiter = createArbiterClient({ + const arbiter = createX402r({ publicClient: createPublicClient({ chain: baseSepolia, transport: http() }), walletClient: createWalletClient({ account, diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index be8db06..0db2a56 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -57,7 +57,7 @@ A complete marketplace operator deployment includes: | Variable | Default | Description | |----------|---------|-------------| - | `PRIVATE_KEY` |, | Deployer wallet (required) | + | `PRIVATE_KEY` | (none) | Deployer wallet (required) | | `ARBITER` | deployer address | Dispute resolver | | `FEE_RECEIVER` | deployer address | Receives operator fees | | `ESCROW_PERIOD` | `604800` (7 days) | Escrow period in seconds | @@ -192,7 +192,7 @@ The deployed marketplace operator has the following slot configuration: | Slot | Contract | Purpose | |---|---|---| -| `AUTHORIZE_PRE_ACTION_CONDITION` | UsdcTvlLimit | Safety limit on authorization | +| `AUTHORIZE_PRE_ACTION_CONDITION` | (none) | Default: anyone with a valid signature can authorize | | `AUTHORIZE_POST_ACTION_HOOK` | EscrowPeriod | Records authorization timestamp | | `CHARGE_PRE_ACTION_CONDITION` | (none) | No restrictions on charge | | `CAPTURE_PRE_ACTION_CONDITION` | EscrowPeriod (or AND(EscrowPeriod, Freeze) if freeze enabled) | Blocks capture during escrow period | diff --git a/sdk/examples.mdx b/sdk/examples.mdx index 7638598..4f2eda8 100644 --- a/sdk/examples.mdx +++ b/sdk/examples.mdx @@ -4,41 +4,32 @@ description: "Runnable examples for every SDK operation." icon: "code" --- -The [x402r-sdk repo](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples) ships runnable examples for every role. Each starts a local Anvil fork, deploys contracts, and runs end-to-end; no wallet or testnet funds needed. +The [x402r-sdk repo](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples) ships runnable example scripts organized by role plus end-to-end scenarios. ## Examples - - Operator-agnostic HTTP service implementing x402's facilitator protocol. Handles signature verification and on-chain settlement for `authCapture`. + + Request a refund, freeze a payment, submit IPFS evidence. Three TypeScript scripts. - - Deploy a complete marketplace or delivery-protection operator via `deployMarketplaceOperator()` / `deployDeliveryProtectionOperator()`. + + Capture from escrow and charge directly. TypeScript scripts plus README. - - Express merchant server using `AuthCaptureServerScheme` and `HTTPFacilitatorClient` to accept refundable payments via x402 middleware. + + Approve a refund, review on-chain evidence, distribute protocol fees. - - Hono merchant server using `AuthCaptureServerScheme` and `HTTPFacilitatorClient` to accept refundable payments via x402 middleware. + + End-to-end runners: happy-path capture, dispute resolution. Wires payer + merchant + arbiter together against a local Anvil fork. - - CLI tool for merchants to capture funds, void / deny / approve refunds, and query escrow state. - - - CLI tool for payers to `pay`, `preview-fee`, request refunds, freeze payments, and check status. - - - CLI tool for arbiters to review cases, deny / refuse / execute refunds, and manage registry. - - - Shared utilities for the CLI examples: `parsePaymentInfo`, `shortAddress`, `formatUSDC`. + + Shared setup utilities: Anvil-fork bootstrap, constants, common types. ## Running examples -Mainnet runs require funded wallets on Base. For local runs, the Anvil-fork scripts seed accounts automatically. +Mainnet runs require funded wallets on Base. The scenario runners use a local Anvil fork and seed accounts automatically. @@ -59,15 +50,7 @@ bun install && bun run build ``` -Then run a per-action example: - -```bash -pnpm example:payer:request-refund -pnpm example:merchant:charge -pnpm example:arbiter:approve-refund -``` - -See the [examples directory README](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples) for the full list of scripts. +See each example directory's README on GitHub for the exact run command for that script. ## Next steps diff --git a/sdk/merchant/refund-handling.mdx b/sdk/merchant/refund-handling.mdx index cabd3fd..a9476ca 100644 --- a/sdk/merchant/refund-handling.mdx +++ b/sdk/merchant/refund-handling.mdx @@ -4,7 +4,9 @@ description: "Process, approve, deny, and manage refund requests as a merchant" icon: "rotate-left" --- -The merchant client provides refund management through the `refund` and `freeze` action groups. The `refund` group requires `refundRequestAddress` in the client config. +The merchant client exposes a read-mostly slice of refund actions plus `freeze.isFrozen`. Writes that change refund-request status (`deny`, `refuse`) and writes that lift a freeze (`unfreeze`) live on `createArbiterClient` or on the full `createX402r()` client. + +Use `createMerchantClient` for queries below; for executing a refund, see [Capture vs void](/sdk/merchant/quickstart#payment-voidpayment), the merchant client's `payment.voidPayment()` auto-flips the request to `Approved` through the `VOID_POST_ACTION_HOOK`. ## Refund request queries @@ -85,61 +87,38 @@ const request = await merchant.refund?.getByKey(paymentInfoHash) ### refund.getReceiverRequests -Retrieve paginated refund requests for a receiver address. +Retrieve paginated refund request keys for this merchant (the receiver). ```typescript -const requests = await merchant.refund?.getReceiverRequests(receiverAddress, 0n, 10n) +const { keys, total } = await merchant.refund?.getReceiverRequests( + receiverAddress, + 0n, + 10n, +) ?? { keys: [], total: 0n } + +for (const hash of keys) { + const request = await merchant.refund?.getByKey(hash) + // ... inspect request.amount, request.status +} ``` **Parameters** | Name | Type | Description | |---|---|---| -| `receiver` | `Address` | Receiver address to query | +| `receiver` | `Address` | Receiver address to query (typically the merchant) | | `offset` | `bigint` | Index offset | | `count` | `bigint` | Max entries to return | -**Returns** `Promise`. - -### refund.getOperatorRequests - -Retrieve paginated refund requests across all payments on an operator. - -```typescript -const requests = await merchant.refund?.getOperatorRequests(operatorAddress, 0n, 10n) -``` - -**Parameters** - -| Name | Type | Description | -|---|---|---| -| `operator` | `Address` | Operator address to query | -| `offset` | `bigint` | Index offset | -| `count` | `bigint` | Max entries to return | +**Returns** `Promise<{ keys: readonly Hex[]; total: bigint }>`. To hydrate each entry, call `refund.getByKey(hash)` per key. -**Returns** `Promise`. + +`getOperatorRequests` (paginated across all payments under an operator) lives on `createArbiterClient`, not on the merchant client. + ## Refund request actions -### refund.deny - -Deny a pending refund request. Status becomes `Denied`. - -```typescript -const tx = await merchant.refund?.deny(paymentInfo) -``` - -**Parameters** - -| Name | Type | Description | -|---|---|---| -| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | - -**Returns** `Promise`. - - -If you deny a request, the payer may escalate to an arbiter. Provide a reason off-chain to reduce escalation risk. - +Approving or denying a request through the operator hook is what the merchant does. Terminal `deny` / `refuse` calls on the RefundRequest contract are scoped to the arbiter role; from a merchant, execute the refund through `payment.voidPayment()` (auto-approves) or signal a refusal off-chain and let the arbiter terminalize. ### payment.voidPayment @@ -180,21 +159,9 @@ const frozen = await merchant.freeze?.isFrozen(paymentInfo) **Returns** `Promise`. -### freeze.unfreeze - -Remove a freeze on a payment. Only the receiver (merchant) or an authorized party can unfreeze. - -```typescript -const tx = await merchant.freeze?.unfreeze(paymentInfo) -``` - -**Parameters** - -| Name | Type | Description | -|---|---|---| -| `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | - -**Returns** `Promise`. + +The merchant client exposes `freeze.isFrozen` only. Lifting a freeze (`unfreeze`) is an arbiter-role action; use `createArbiterClient` or `createX402r()`. + ## Complete refund workflow @@ -240,13 +207,14 @@ async function handleRefundWorkflow( const shouldApprove = request.amount <= amounts.refundableAmount if (shouldApprove) { - // Execute the refund (auto-approves the request) + // Execute the refund (auto-approves the request via VOID_POST_ACTION_HOOK) const tx = await merchant.payment.voidPayment(paymentInfo) console.log('Refund executed:', tx) } else { - // Deny the request - const tx = await merchant.refund?.deny(paymentInfo) - console.log('Denied:', tx) + // The arbiter can terminalize the request via refund.deny / refund.refuse. + // The merchant can simply leave the request Pending and capture as usual, + // or escalate off-chain to the arbiter. + console.log('Declining; arbiter may deny if escalated') } } ``` @@ -273,26 +241,22 @@ sequenceDiagram alt Approve M->>O: payment.voidPayment(paymentInfo) O->>P: Funds returned to payer - else Deny - M->>R: refund.deny(paymentInfo) - Note over P: Payer may escalate to arbiter + else Decline (off-chain) / escalate to arbiter + Note over P,R: Arbiter may terminalize via refund.deny / refund.refuse end ``` ## Method reference | Method | Parameters | Returns | -|--------|-----------|---------| +|---|---|---| | `refund.has` | `paymentInfo` | `boolean` | | `refund.getStatus` | `paymentInfo` | `RefundRequestStatus` | | `refund.get` | `paymentInfo` | `RefundRequestData` | | `refund.getByKey` | `paymentInfoHash` | `RefundRequestData` | -| `refund.deny` | `paymentInfo` | `Hash` | -| `refund.refuse` | `paymentInfo` | `Hash` | -| `refund.getReceiverRequests` | `receiver, offset, count` | `RefundRequestData[]` | -| `refund.getOperatorRequests` | `operator, offset, count` | `RefundRequestData[]` | +| `refund.getReceiverRequests` | `receiver, offset, count` | `{ keys: readonly Hex[]; total: bigint }` | | `freeze.isFrozen` | `paymentInfo` | `boolean` | -| `freeze.unfreeze` | `paymentInfo` | `Hash` | +| `payment.voidPayment` | `paymentInfo, data?` | `Hash` (auto-approves the pending RefundRequest) | ## Next steps From 7d39128964f97edeb4dde8ac4bb4c4bbe7302108 Mon Sep 17 00:00:00 2001 From: A1igator Date: Tue, 12 May 2026 21:39:31 -0700 Subject: [PATCH 30/37] docs: address PR #35 re-review (4278270388) Fixes 14 items from the third review pass: Blockers - sdk/delivery-merchant.mdx: import registerAuthCaptureEvmScheme from /authCapture/server (zero-arg signature) instead of /authCapture/facilitator (which requires signer + networks). - sdk/overview.mdx: drop the commercePayments* import snippet (the constants exist in core/config but are not re-exported from @x402r/core); switch to authCaptureEscrow + tokenCollector which are actually exported. - sdk/deploy-operator.mdx: remove the examples/deploy-operator references (the directory does not exist on x402r-sdk@main). The page now flows directly into the SDK-direct deployment example. Majors - sdk/merchant/getting-started.mdx: drop the broken examples/servers/express GitHub link. - x402-integration/auth-capture-scheme.mdx: repoint CaptureAuthorizer Trust warning from /contracts/overview#payment-operator to /contracts/payment-operator. - sdk/delivery-arbiter.mdx: remove the self-referential Dispute Resolution card (target page was deleted in a8cb139). - sdk/{overview,cli,create-client,deploy-operator}.mdx: repoint href="/sdk/merchant" (folder, not a page) to /sdk/merchant/quickstart. - sdk/examples.mdx: list all four scenarios (happy-path capture, dispute resolution, atomic charge, partial refund flow); soften the mainnet Warning to an Info noting examples run against a local Anvil fork; drop the npm install tab since the SDK uses pnpm workspaces. - sdk/merchant/refund-handling.mdx: add refund.getStoredPaymentInfo, refund.getCancelCount, refund.getCancelledAmount to the method-reference table (all exposed on MerchantClient.refund). - sdk/helpers/forward-to-arbiter.mdx: add x402rDefaults + X402rDefaultsInput to the @x402r/helpers re-export list. Minors - sdk/merchant/refund-handling.mdx: fix the "Capture vs void" link anchor to point at #capture-vs-refund-decision-flow (the prose section), not the API ref heading. - sdk/examples.mdx: soften the payer card's IPFS evidence claim (CID is a hardcoded placeholder; IPFS pinning is the integrator's responsibility). Co-Authored-By: Claude Opus 4.7 (1M context) --- sdk/cli.mdx | 2 +- sdk/create-client.mdx | 2 +- sdk/delivery-arbiter.mdx | 5 +- sdk/delivery-merchant.mdx | 2 +- sdk/deploy-operator.mdx | 63 +----------------------- sdk/examples.mdx | 17 +++---- sdk/helpers/forward-to-arbiter.mdx | 8 +++ sdk/merchant/getting-started.mdx | 4 -- sdk/merchant/refund-handling.mdx | 5 +- sdk/overview.mdx | 19 +++---- x402-integration/auth-capture-scheme.mdx | 2 +- 11 files changed, 33 insertions(+), 96 deletions(-) diff --git a/sdk/cli.mdx b/sdk/cli.mdx index e839082..3d2b96f 100644 --- a/sdk/cli.mdx +++ b/sdk/cli.mdx @@ -192,7 +192,7 @@ The `@x402r/cli` package exports: ## Next steps - + Accept payments and manage escrow releases. diff --git a/sdk/create-client.mdx b/sdk/create-client.mdx index 6b65deb..9aaab42 100644 --- a/sdk/create-client.mdx +++ b/sdk/create-client.mdx @@ -166,7 +166,7 @@ For standalone helpers that extract identity data from x402 extension responses ## Next steps - + Accept payments and capture funds. diff --git a/sdk/delivery-arbiter.mdx b/sdk/delivery-arbiter.mdx index 1cba42d..bce6279 100644 --- a/sdk/delivery-arbiter.mdx +++ b/sdk/delivery-arbiter.mdx @@ -124,13 +124,10 @@ There is a full [AI garbage detector example](https://github.com/BackTrackCo/arb ## Next Steps - + Deploy the operator and configure forwardToArbiter(). - - For human-reviewed disputes instead of automated evaluation. - Runnable examples for every SDK operation. diff --git a/sdk/delivery-merchant.mdx b/sdk/delivery-merchant.mdx index 66ef98b..74410f4 100644 --- a/sdk/delivery-merchant.mdx +++ b/sdk/delivery-merchant.mdx @@ -29,7 +29,7 @@ icon: "store" ```typescript import { forwardToArbiter } from '@x402r/helpers' - import { registerAuthCaptureEvmScheme } from '@x402r/evm/authCapture/facilitator' + import { registerAuthCaptureEvmScheme } from '@x402r/evm/authCapture/server' const resourceServer = new x402ResourceServer(facilitatorConfig) registerAuthCaptureEvmScheme(resourceServer) diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index 0db2a56..f663e0e 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -6,10 +6,6 @@ icon: "rocket" The `@x402r/core` package includes deployment presets that handle the full lifecycle of deploying a PaymentOperator and all its supporting contracts. - - Clone the deploy-operator example and deploy your own operator in minutes. - - ## Presets The SDK ships two deployment presets. Pick the one that matches your use case: @@ -38,62 +34,7 @@ A complete marketplace operator deployment includes: - Node.js 20+, pnpm 9.15+ - A private key with Base Sepolia ETH ([get testnet ETH](https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet)) - - - ```bash - git clone https://github.com/BackTrackCo/x402r-sdk.git - cd x402r-sdk - pnpm install && pnpm build - ``` - - - Copy the example env file and set your private key: - ```bash - cd examples/deploy-operator - cp .env.example .env - ``` - - Edit `.env` with your values. Only `PRIVATE_KEY` is required, everything else has sensible defaults: - - | Variable | Default | Description | - |----------|---------|-------------| - | `PRIVATE_KEY` | (none) | Deployer wallet (required) | - | `ARBITER` | deployer address | Dispute resolver | - | `FEE_RECEIVER` | deployer address | Receives operator fees | - | `ESCROW_PERIOD` | `604800` (7 days) | Escrow period in seconds | - | `FREEZE_DURATION` | `259200` (3 days) | Freeze duration in seconds | - | `FEE_BPS` | `100` (1%) | Operator fee in basis points | - | `NETWORK_ID` | `eip155:84532` | Chain identifier | - | `RPC_URL` | `https://sepolia.base.org` | RPC endpoint | - - - ```bash - pnpm start - ``` - - Or pass env vars inline from the SDK root: - ```bash - PRIVATE_KEY=0x... pnpm tsx examples/deploy-operator/index.ts - ``` - - - The script outputs all deployed contract addresses and BaseScan links. Use the `PaymentOperator` address in your payment payloads: - ```typescript - operator: "0xYourOperatorAddress..." - ``` - - - - -For quick E2E testing with short timers (5-minute escrow, 3-minute freeze), use the short-escrow variant instead: -```bash -PRIVATE_KEY=0x... pnpm tsx examples/deploy-operator/deploy-short-escrow.ts -``` - - -## Using the SDK directly - -If you want to integrate deployment into your own code: +Call `deployMarketplaceOperator` from `@x402r/core` with a viem wallet client. Because every contract uses CREATE2, deploys are idempotent: re-running with the same parameters detects existing contracts and skips them. ```typescript import { createPublicClient, createWalletClient, http } from 'viem'; @@ -327,7 +268,7 @@ console.log('AuthorizeHook:', deployment.authorizeHookAddress) ## Next Steps - + Accept payments, capture funds from escrow. diff --git a/sdk/examples.mdx b/sdk/examples.mdx index 4f2eda8..d50160b 100644 --- a/sdk/examples.mdx +++ b/sdk/examples.mdx @@ -10,7 +10,7 @@ The [x402r-sdk repo](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples - Request a refund, freeze a payment, submit IPFS evidence. Three TypeScript scripts. + Request a refund, freeze a payment, submit on-chain evidence (a placeholder CID; IPFS pinning is the integrator's responsibility). Three TypeScript scripts. Capture from escrow and charge directly. TypeScript scripts plus README. @@ -19,7 +19,7 @@ The [x402r-sdk repo](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples Approve a refund, review on-chain evidence, distribute protocol fees. - End-to-end runners: happy-path capture, dispute resolution. Wires payer + merchant + arbiter together against a local Anvil fork. + End-to-end runners: happy-path capture, dispute resolution, atomic charge, partial refund flow. Wires payer + merchant + arbiter together against a local Anvil fork. Shared setup utilities: Anvil-fork bootstrap, constants, common types. @@ -28,16 +28,11 @@ The [x402r-sdk repo](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples ## Running examples - -Mainnet runs require funded wallets on Base. The scenario runners use a local Anvil fork and seed accounts automatically. - + +All examples run against a local Anvil fork seeded by `shared/anvil-setup.ts`. No mainnet wallet or funding is required. + -```bash npm -git clone https://github.com/BackTrackCo/x402r-sdk.git -cd x402r-sdk -npm install && npm run build -``` ```bash pnpm git clone https://github.com/BackTrackCo/x402r-sdk.git cd x402r-sdk @@ -50,6 +45,8 @@ bun install && bun run build ``` +The SDK uses pnpm workspaces (`pnpm@10.23.0`). The `npm` runtime is fine for application code that consumes published `@x402r/*` packages, but the workspace clone above expects pnpm or a workspace-aware install. + See each example directory's README on GitHub for the exact run command for that script. ## Next steps diff --git a/sdk/helpers/forward-to-arbiter.mdx b/sdk/helpers/forward-to-arbiter.mdx index 6959f63..93bac9a 100644 --- a/sdk/helpers/forward-to-arbiter.mdx +++ b/sdk/helpers/forward-to-arbiter.mdx @@ -139,6 +139,14 @@ import { } from '@x402r/helpers' ``` +And the `x402rDefaults` builder for hand-constructing `extra` in `PaymentRequirements`: + +```typescript +import { type X402rDefaultsInput, x402rDefaults } from '@x402r/helpers' +``` + +`x402rDefaults(input)` returns an `AuthCaptureExtra` populated with sensible defaults, useful when you want to build `PaymentRequirements` outside the merchant client. + ## Next steps diff --git a/sdk/merchant/getting-started.mdx b/sdk/merchant/getting-started.mdx index 07e8e2a..ac31678 100644 --- a/sdk/merchant/getting-started.mdx +++ b/sdk/merchant/getting-started.mdx @@ -7,10 +7,6 @@ icon: "store" This guide walks you through setting up an Express server that accepts x402r escrow-backed payments. By the end, you'll have a paid API endpoint protected by the x402 payment middleware with refundable escrow support. - -The full source code for this example is available on [GitHub](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples/servers/express). - - ## Prerequisites - Node.js 20+ diff --git a/sdk/merchant/refund-handling.mdx b/sdk/merchant/refund-handling.mdx index a9476ca..75a214b 100644 --- a/sdk/merchant/refund-handling.mdx +++ b/sdk/merchant/refund-handling.mdx @@ -6,7 +6,7 @@ icon: "rotate-left" The merchant client exposes a read-mostly slice of refund actions plus `freeze.isFrozen`. Writes that change refund-request status (`deny`, `refuse`) and writes that lift a freeze (`unfreeze`) live on `createArbiterClient` or on the full `createX402r()` client. -Use `createMerchantClient` for queries below; for executing a refund, see [Capture vs void](/sdk/merchant/quickstart#payment-voidpayment), the merchant client's `payment.voidPayment()` auto-flips the request to `Approved` through the `VOID_POST_ACTION_HOOK`. +Use `createMerchantClient` for queries below; for executing a refund, see [Capture vs refund decision flow](/sdk/merchant/quickstart#capture-vs-refund-decision-flow). The merchant client's `payment.voidPayment()` auto-flips the request to `Approved` through the `VOID_POST_ACTION_HOOK`. ## Refund request queries @@ -254,7 +254,10 @@ sequenceDiagram | `refund.getStatus` | `paymentInfo` | `RefundRequestStatus` | | `refund.get` | `paymentInfo` | `RefundRequestData` | | `refund.getByKey` | `paymentInfoHash` | `RefundRequestData` | +| `refund.getStoredPaymentInfo` | `paymentInfoHash` | `PaymentInfo` | | `refund.getReceiverRequests` | `receiver, offset, count` | `{ keys: readonly Hex[]; total: bigint }` | +| `refund.getCancelCount` | `paymentInfo` | `bigint` (number of cancellations on this RefundRequest) | +| `refund.getCancelledAmount` | `paymentInfo, cancelIndex` | `bigint` (amount cancelled at the given index) | | `freeze.isFrozen` | `paymentInfo` | `boolean` | | `payment.voidPayment` | `paymentInfo, data?` | `Hash` (auto-approves the pending RefundRequest) | diff --git a/sdk/overview.mdx b/sdk/overview.mdx index 929c634..cc769bb 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -81,7 +81,7 @@ bunx @x402r/cli pay [options] ### Guides - + Deploy an operator, accept a payment, capture funds from escrow. @@ -118,17 +118,12 @@ Salt namespace: `commerce-payments::v1::`. Import the addresses from `@x402r/core`: ```ts -import { - commercePaymentsAddresses, - commercePaymentsAuthCaptureEscrow, - commercePaymentsErc3009PaymentCollector, - commercePaymentsPermit2PaymentCollector, -} from '@x402r/core'; - -// Convenience bundle -commercePaymentsAddresses.authCaptureEscrow; -commercePaymentsAddresses.erc3009PaymentCollector; -commercePaymentsAddresses.permit2PaymentCollector; +import { authCaptureEscrow, tokenCollector } from '@x402r/core'; + +// AuthCaptureEscrow, canonical across every supported chain. +authCaptureEscrow; +// Primary token collector (currently aliases ERC3009PaymentCollector). +tokenCollector; ``` The primitives are exposed to SDK consumers on the chains listed in `@x402r/core`'s `x402rChains` (Base + Base Sepolia today). The CREATE2 addresses themselves have been prepositioned via CreateX on additional chains and will be enabled in the registry as canonical `base/commerce-payments@v1.0.0` coverage extends. diff --git a/x402-integration/auth-capture-scheme.mdx b/x402-integration/auth-capture-scheme.mdx index ece0922..15c77f8 100644 --- a/x402-integration/auth-capture-scheme.mdx +++ b/x402-integration/auth-capture-scheme.mdx @@ -364,7 +364,7 @@ The escrow contract enforces invariants on-chain: -**CaptureAuthorizer Trust Required:** The captureAuthorizer controls when and how much to capture. Choose carefully and understand the capture policy. See [Operators](/contracts/overview#payment-operator) for examples. +**CaptureAuthorizer Trust Required:** The captureAuthorizer controls when and how much to capture. Choose carefully and understand the capture policy. See [PaymentOperator](/contracts/payment-operator) for examples. ## Error Codes From b26b3ac8f26b49f99352ae402cc5a3075b61b77f Mon Sep 17 00:00:00 2001 From: A1igator Date: Tue, 19 May 2026 15:03:21 -0700 Subject: [PATCH 31/37] docs: apply writing-docs skill quality gates across all pages Two-pass cleanup over the 43 pages on this PR. First pass cleared the writing-docs skill's hard-constraint voice rules; second pass cleared the remaining actionable Vale prose violations. Voice gate (hard constraints) cleared: - em-dashes: 8 -> 0 - first-person plural (we/our/let's): 4 -> 0 - x402r.Slop vocabulary (powerful, robust, seamless, ...): 4 -> 0 - Microsoft.Auto prefix gimmicks (auto-X): 16 -> 0 - Microsoft.Adverbs weasel adverbs (very, simply, just): 14 -> 0 - Microsoft.Dashes hyphen-as-dash: 8 -> 0 - write-good.ThereIs filler sentence starts: 6 -> 0 - Microsoft.Foreign (e.g., etc., via): 32 -> 0 - exclamation marks in prose: stripped Prose gate: - write-good.Passive: 106 -> 1 (the one remaining is the natural technical phrasing "nonce is consumed at settlement" where the actor is implicit) - write-good.TooWordy: 76 -> 12 (all remaining are literal SDK/contract method names: authorize(), evaluate(), Validate) - Microsoft.HeadingPunctuation: 9 -> 0 (sentence case, no ?) Structural gate: - Heading title-case -> sentence case across the branch - Cross-link verification passed (no broken refs) Accuracy fixes: - contracts/periphery/overview.mdx: added missing Permit2PaymentCollector address to the deployments table - sdk/merchant/getting-started.mdx: replaced placeholder env values with real Base Sepolia testnet addresses Remaining Vale hits are deferred to a follow-up config PR: - 269 Vale.Spelling: legitimate domain terms (IHook, ICondition, walletClient, captureAuthorizer, Permit2PaymentCollector, etc.) - 9 Microsoft.HeadingAcronyms: intrinsic protocol acronyms (ERC-3009, EIP-6492, BUSL-1.1, JSON-RPC, CDP, DAO) - 6 Microsoft.Terms: ERC-8004 "agent" vs Microsoft's "personal digital assistant" replacement - 6 proselint.Typography: false-positive multiplication symbol on zero-address hex literals - 6 Microsoft.Quotes + 2 Microsoft.Ellipses: inside a JSON fenced block of intentional spec placeholders Total Vale violations: 609 -> 311 (49% reduction); after the config PR cleanup the genuine-prose-issue count is 0. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/architecture.mdx | 34 +++++++-------- contracts/audits.mdx | 18 ++++---- contracts/conditions/always-true.mdx | 2 +- contracts/conditions/combinators.mdx | 8 ++-- contracts/conditions/custom.mdx | 6 +-- contracts/conditions/escrow-period.mdx | 6 +-- contracts/conditions/freeze.mdx | 2 +- contracts/conditions/overview.mdx | 16 +++---- contracts/conditions/receiver.mdx | 2 +- contracts/conditions/static-address.mdx | 4 +- contracts/examples.mdx | 26 ++++++------ contracts/factories.mdx | 34 +++++++-------- contracts/fees.mdx | 10 ++--- contracts/gas-costs.mdx | 28 ++++++------- contracts/hooks/authorization-time.mdx | 4 +- contracts/hooks/combinator.mdx | 8 ++-- contracts/hooks/custom.mdx | 8 ++-- contracts/hooks/overview.mdx | 42 +++++++++---------- contracts/hooks/payment-index.mdx | 10 ++--- contracts/hooks/refund-request.mdx | 6 +-- contracts/license.mdx | 38 ++++++++--------- contracts/overview.mdx | 18 ++++---- contracts/payment-operator.mdx | 24 +++++------ contracts/periphery/auth-capture-escrow.mdx | 18 ++++---- contracts/periphery/overview.mdx | 5 ++- .../periphery/receiver-refund-collector.mdx | 12 +++--- .../periphery/refund-request-evidence.mdx | 10 ++--- index.mdx | 18 ++++---- roadmap.mdx | 8 ++-- sdk/cli.mdx | 28 ++++++------- sdk/create-client.mdx | 10 ++--- sdk/delivery-arbiter.mdx | 6 +-- sdk/delivery-merchant.mdx | 2 +- sdk/delivery-protection.mdx | 6 +-- sdk/deploy-operator.mdx | 30 ++++++------- sdk/examples.mdx | 6 +-- sdk/helpers/forward-to-arbiter.mdx | 12 +++--- sdk/merchant/getting-started.mdx | 13 +++--- sdk/merchant/quickstart.mdx | 14 +++---- sdk/merchant/refund-handling.mdx | 18 ++++---- sdk/overview.mdx | 14 +++---- x402-integration/auth-capture-scheme.mdx | 34 +++++++-------- x402-integration/overview.mdx | 16 +++---- 43 files changed, 318 insertions(+), 316 deletions(-) diff --git a/contracts/architecture.mdx b/contracts/architecture.mdx index 44bd5f5..3f3282f 100644 --- a/contracts/architecture.mdx +++ b/contracts/architecture.mdx @@ -61,7 +61,7 @@ flowchart TB Or -.->|composes| Cond ``` -For additional visual diagrams, see the [x402r-contracts repository](https://github.com/BackTrackCo/x402r-contracts#architecture). +For more visual diagrams, see the [x402r-contracts repository](https://github.com/BackTrackCo/x402r-contracts#architecture). ## Payment Flow Sequence @@ -72,9 +72,9 @@ For additional visual diagrams, see the [x402r-contracts repository](https://git 3. **Operator** validates fee bounds and stores fees at authorization time 4. **Operator** calls `escrow.authorize()` to lock funds 5. **Operator** calls `AUTHORIZE_POST_ACTION_HOOK` to record timestamp -6. **Escrow period** begins (e.g., 7 days) - if configured -7. After escrow period: **Authorized address(es)** call `operator.capture(paymentInfo, amount)` (e.g., receiver, designated address, or both) -8. **Operator** checks `CAPTURE_PRE_ACTION_CONDITION` (configurable - can include time checks, role checks, etc.) +6. **Escrow period** begins (for example, 7 days) if configured +7. After escrow period: **Authorized addresses** call `operator.capture(paymentInfo, amount)` (for example, receiver, designated address, or both) +8. **Operator** checks `CAPTURE_PRE_ACTION_CONDITION` (configurable, can include time checks or role checks) 9. **Operator** calls `escrow.capture()` to transfer funds to receiver 10. **Operator** accumulates protocol fees for later distribution 11. **Operator** calls `CAPTURE_POST_ACTION_HOOK` to update state @@ -85,11 +85,11 @@ For additional visual diagrams, see the [x402r-contracts repository](https://git 1. **Payer** calls `refundRequest.requestRefund(paymentInfo, amount)` 2. **RefundRequest** creates request with status `Pending` -3. **Designated address** (e.g., arbiter, DAO multisig) reviews dispute +3. **Designated address** (for example, arbiter or DAO multisig) reviews dispute 4. **Designated address** calls `operator.void(paymentInfo)` 5. **Operator** checks `VOID_PRE_ACTION_CONDITION` (configured per operator) 6. **Operator** calls `escrow.void()` to return all escrowed funds to payer -7. **Operator** calls `VOID_POST_ACTION_HOOK` (RefundRequest auto-flips status to `Approved`) +7. **Operator** calls `VOID_POST_ACTION_HOOK` (RefundRequest flips status to `Approved`) 8. Funds transferred back to payer @@ -107,7 +107,7 @@ Refund conditions are configurable. Can be arbiter-only (marketplace), receiver- - **Day 3-6:** Payment frozen, capture blocked - **Day 6:** Freeze expires automatically (or authorized address unfreezes early) - **Day 7:** Escrow period ends -- **Day 7+:** Authorized address(es) can capture (if not frozen) +- **Day 7+:** Authorized addresses can capture (if not frozen) Freeze policies are optional and configurable. Define who can freeze, who can unfreeze, and how long freeze lasts. @@ -121,13 +121,13 @@ Freeze policies are optional and configurable. Define who can freeze, who can un ### Authorization Check (Before Action) -When an action is called (e.g., `capture()`): +When you invoke an action (for example, `capture()`): 1. **Load Condition** - Get the condition address from operator slot -2. **Evaluate Condition** - Call `condition.check(paymentInfo, amount, caller)` - - Check if caller matches required role (e.g., receiver, arbiter) - - Check state (e.g., escrow period passed, not frozen) - - Check other requirements (e.g., time constraints) +2. **Check Condition** - Call `condition.check(paymentInfo, amount, caller)` + - Check if caller matches required role (for example, receiver or arbiter) + - Check state (for example, escrow period passed, not frozen) + - Check other requirements (for example, time constraints) 3. **Result:** - `true` → Proceed to execute action - `false` → Revert with `ConditionNotMet` error @@ -141,7 +141,7 @@ When an action is called (e.g., `capture()`): - If not receiver, checks if caller is arbiter: Yes → PASS - If neither: FAIL -**AndCondition([OrCondition(...), EscrowPeriod])** +**AndCondition([OrCondition, EscrowPeriod])** - First checks OrCondition: PASS (caller is receiver or arbiter) - Then checks EscrowPeriod: PASS (escrow period elapsed) - Both passed → PASS (action allowed) @@ -210,7 +210,7 @@ For a 1000 USDC payment with 3 bps protocol fee + 2 bps operator fee: - **Total Fee:** 0.50 USDC (5 bps) - **Receiver Gets:** 999.50 USDC -Fees accumulate in the operator and are distributed via `distributeFees(token)`. +Fees accumulate in the operator. Anyone can call `distributeFees(token)` to disburse them. **FEE_RECEIVER** can be: - Arbiter address (marketplace with disputes) @@ -224,11 +224,11 @@ Fees accumulate in the operator and are distributed via `distributeFees(token)`. |------|-------------|--------------| | **Payer** | `authorize()`, `freeze()`, `unfreeze()`, `requestRefund()`, `cancelRefundRequest()` | Can only act on own payments | | **Receiver** | `capture()` (if condition allows), `charge()`, `requestRefund()` | Can only act on payments where they are receiver | -| **Designated Address** | Any action per conditions (e.g., `voidPayment()`, `capture()`, `updateStatus()`) | Defined by StaticAddressCondition (arbiter, DAO, service provider, etc.) | +| **Designated Address** | Any action per conditions (for example, `voidPayment()`, `capture()`, or `updateStatus()`) | Defined by StaticAddressCondition (arbiter, DAO, or service provider) | | **Protocol Owner** | `queueCalculator()`, `executeCalculator()`, `queueRecipient()`, `executeRecipient()` | 7-day timelock on ProtocolFeeConfig changes | -"Designated Address" is configured per operator via StaticAddressCondition. Can be: +Each operator sets its "Designated Address" via StaticAddressCondition. Common roles include: - **Arbiter** (marketplace with disputes) - **Service Provider** (subscriptions, APIs) - **DAO Multisig** (governance-controlled) @@ -272,7 +272,7 @@ await protocolFeeConfig.executeRecipient(); ``` -Operator fees are **immutable** - set at deploy time via `IFeeCalculator`. Only protocol fees can be changed (with 7-day timelock). +Operator fees are **immutable**: set at deploy time via `IFeeCalculator`. Only protocol fees can change, and only after a 7-day timelock. ### Two-Step Ownership diff --git a/contracts/audits.mdx b/contracts/audits.mdx index f2a83c8..54c5505 100644 --- a/contracts/audits.mdx +++ b/contracts/audits.mdx @@ -6,16 +6,16 @@ icon: "shield-halved" ## Audit Status -x402r is built on top of the canonical [commerce-payments](https://github.com/base/commerce-payments) protocol from Base. The commerce-payments contracts are professionally audited and are used directly at their universal CREATE2 addresses (no fork). The x402r-specific contracts built on top of them are **not yet audited**. +x402r extends the canonical [commerce-payments](https://github.com/base/commerce-payments) protocol from Base. The commerce-payments contracts have professional audits and run directly at their universal CREATE2 addresses (no fork). The x402r-specific contracts on top of them are **not yet audited**. ### What's Audited (Upstream) -The commerce-payments `AuthCaptureEscrow` contract and its supporting infrastructure (TokenCollectors, TokenStore, Permit2 integration) were audited by: +Auditors covered the commerce-payments `AuthCaptureEscrow` contract and its supporting infrastructure (TokenCollectors, TokenStore, Permit2 integration): - **Spearbit** (2 audits) - **Coinbase Protocol Security** (3 audits) -These audits cover the core escrow lifecycle: authorize, capture, void, reclaim, and refund. Audit reports are available in the [commerce-payments repository](https://github.com/base/commerce-payments). +These audits cover the core escrow lifecycle: `authorize`, `capture`, `void`, `reclaim`, and `refund`. The [commerce-payments repository](https://github.com/base/commerce-payments) hosts the audit reports. ### What's Not Audited @@ -38,7 +38,7 @@ These audits cover the core escrow lifecycle: authorize, capture, void, reclaim, - The condition/hook plugin system is stateless or minimal-state by design, reducing attack surface -Use x402r contracts on mainnet at your own risk. While we've built with security best practices (CEI pattern, reentrancy guards, immutable configuration, timelocked governance), the x402r-specific code has not undergone a formal audit. +Use x402r contracts on mainnet at your own risk. The x402r-specific code follows security best practices (CEI pattern, reentrancy guards, immutable configuration, timelocked governance), but has not undergone a formal audit. ## Security Practices @@ -47,22 +47,22 @@ Even without a formal audit, the x402r contracts follow established security pat - **CEI (Checks-Effects-Interactions)** ordering in all state-changing functions - **ReentrancyGuardTransient** (EIP-1153) on all external entry points -- **Immutable configuration**: operator conditions and fee calculators cannot be changed after deployment +- **Immutable configuration**: deployment locks the operator conditions and fee calculators - **7-day timelock** on protocol fee changes via ProtocolFeeConfig - **2-step ownership transfers** via Solady's Ownable -- **Comprehensive Forge test suite** covering core flows and edge cases +- **Forge test suite** covering core flows and edge cases ## Audit Roadmap -We plan to pursue third-party audits as the contract architecture and use cases stabilize. Priority order: +The plan is to pursue third-party audits as the contract architecture and use cases stabilize. Priority order: 1. **PaymentOperator**: condition dispatch, fee calculation, fee locking, distribution 2. **Plugin system**: conditions, hooks, combinators, and their factories 3. **EscrowPeriod + Freeze**: time-lock enforcement and freeze state management 4. **RefundRequest**: request lifecycle and access control -We'll publish audit reports publicly once completed. +Completed audit reports go public. -If you're interested in integrating x402r and want to discuss the security posture in more detail, or if you've found a vulnerability, reach out at [security@x402r.org](mailto:security@x402r.org). +To discuss the security posture in more detail before integrating, or to report a vulnerability, reach out at [security@x402r.org](mailto:security@x402r.org). diff --git a/contracts/conditions/always-true.mdx b/contracts/conditions/always-true.mdx index c02ca00..77b7aee 100644 --- a/contracts/conditions/always-true.mdx +++ b/contracts/conditions/always-true.mdx @@ -29,7 +29,7 @@ function check(PaymentInfo calldata payment, uint256, address caller) | `AUTHORIZE_PRE_ACTION_CONDITION` | Let anyone create payments (common for marketplace/e-commerce) | -**Use with caution for capture/refund slots.** Setting `CAPTURE_PRE_ACTION_CONDITION` or `VOID_PRE_ACTION_CONDITION` to AlwaysTrueCondition means anyone can capture or refund funds. This is functionally equivalent to leaving the slot as `address(0)` (the default behavior), but makes the intent explicit. +**Use with caution for capture/refund slots.** Setting `CAPTURE_PRE_ACTION_CONDITION` or `VOID_PRE_ACTION_CONDITION` to AlwaysTrueCondition means anyone can capture or refund funds. This matches leaving the slot as `address(0)` (the default behavior), but makes the intent explicit. ## AlwaysTrueCondition vs `address(0)` diff --git a/contracts/conditions/combinators.mdx b/contracts/conditions/combinators.mdx index 609e2c1..ee74b14 100644 --- a/contracts/conditions/combinators.mdx +++ b/contracts/conditions/combinators.mdx @@ -6,7 +6,7 @@ icon: "puzzle-piece" ## Overview -Combinator conditions compose multiple conditions with logical operators. Deploy each via its respective factory. +Combinator conditions compose two or more conditions with logical operators. Deploy each via its respective factory. ## AndCondition @@ -85,7 +85,7 @@ flowchart TD ## Limits -**Max 10 conditions per combinator.** Keep combinators simple, deeply nested trees increase gas costs and make debugging harder. +**Max 10 conditions per combinator.** Keep combinators simple. Nested trees increase gas costs and make debugging harder. ## Gas @@ -100,7 +100,7 @@ OrCondition([A, B]) // ~25K gas per check OrCondition([A, B, C, D]) // ~45K gas per check ``` -Each additional condition adds one external call. Prefer fewer conditions where possible. +Each extra condition adds one external call. Prefer fewer conditions where possible. ## Next Steps @@ -109,7 +109,7 @@ Each additional condition adds one external call. Prefer fewer conditions where Time-based condition for escrow windows. - Block releases when payments are frozen. + Block releases on frozen payments. See combinators in real configurations. diff --git a/contracts/conditions/custom.mdx b/contracts/conditions/custom.mdx index 7c9229f..5549df6 100644 --- a/contracts/conditions/custom.mdx +++ b/contracts/conditions/custom.mdx @@ -6,7 +6,7 @@ icon: "wrench" ## Overview -You can create custom conditions for specialized logic beyond what the built-in conditions provide. Implement the `ICondition` interface and follow the security rules below. +You can create custom conditions for specialized logic beyond what the built-in conditions provide. Build against the `ICondition` interface and follow the security rules below. ## ICondition Rules @@ -63,10 +63,10 @@ Before deploying a custom condition: - [ ] No external calls to untrusted contracts - [ ] No state modifications - [ ] Handles edge cases (zero address, zero amount, uninitialized payments) -- [ ] Comprehensive test coverage with Forge tests +- [ ] Full test coverage with Forge tests -Custom conditions with bugs can lead to **permanently locked funds** (if `check()` always returns `false`) or **unauthorized access** (if `check()` always returns `true`). Test thoroughly on testnet before mainnet deployment. +Custom conditions with bugs can lead to **permanently locked funds** (if `check()` always returns `false`) or **unauthorized access** (if `check()` always returns `true`). Test on Base Sepolia before mainnet deployment. ## Testing diff --git a/contracts/conditions/escrow-period.mdx b/contracts/conditions/escrow-period.mdx index d40fd0d..fc20354 100644 --- a/contracts/conditions/escrow-period.mdx +++ b/contracts/conditions/escrow-period.mdx @@ -8,7 +8,7 @@ icon: "clock" EscrowPeriod is a dual-purpose contract, it functions as both a **hook** and a **condition**: -- **As a hook:** Records the `block.timestamp` when a payment is authorized +- **As a hook:** Records the `block.timestamp` at payment authorization - **As a condition:** Returns `true` only after the escrow period has elapsed Use the **same address** for both `AUTHORIZE_POST_ACTION_HOOK` and `CAPTURE_PRE_ACTION_CONDITION` slots on the operator. @@ -24,7 +24,7 @@ flowchart LR ATR -->|implements| IR[IHook] ``` -EscrowPeriod extends [AuthorizationTimeRecorderHook](/contracts/hooks/authorization-time) and adds `ICondition` implementation. You don't need to deploy AuthorizationTimeRecorderHook separately, use EscrowPeriod directly. +EscrowPeriod extends [AuthorizationTimeRecorderHook](/contracts/hooks/authorization-time) and adds `ICondition` implementation. You don't need to deploy AuthorizationTimeRecorderHook on its own, use EscrowPeriod directly. ## Logic @@ -50,7 +50,7 @@ function isDuringEscrowPeriod( ``` **Checks:** -1. Payment was authorized (has a recorded timestamp) +1. The payment has a recorded authorization timestamp 2. Current time >= authorization time + escrow period ## Deployment diff --git a/contracts/conditions/freeze.mdx b/contracts/conditions/freeze.mdx index b35f469..a0b6644 100644 --- a/contracts/conditions/freeze.mdx +++ b/contracts/conditions/freeze.mdx @@ -6,7 +6,7 @@ icon: "snowflake" ## Overview -Freeze is a standalone condition that blocks capture when a payment is frozen. It manages freeze/unfreeze state with configurable authorization and optional duration-based auto-expiry. +Freeze is a standalone condition that blocks capture on frozen payments. It manages freeze and unfreeze state with configurable authorization and an optional duration-based auto expiry. **Type:** Per-deployment via [FreezeFactory](/contracts/factories) diff --git a/contracts/conditions/overview.mdx b/contracts/conditions/overview.mdx index 60a9266..092ea0b 100644 --- a/contracts/conditions/overview.mdx +++ b/contracts/conditions/overview.mdx @@ -4,13 +4,13 @@ description: "Pluggable condition system for flexible payment authorization and icon: "filter" --- -## What Are Conditions? +## What conditions do -Conditions are pluggable contracts that control who can perform actions on a PaymentOperator. Each operator has **5 condition slots**: one per action: +Conditions are swappable contracts that control who can perform actions on a PaymentOperator. Each operator has **5 condition slots**, one per action: | Slot | Controls | |------|----------| -| `AUTHORIZE_PRE_ACTION_CONDITION` | Who can authorize payments | +| `AUTHORIZE_PRE_ACTION_CONDITION` | Who can create payments | | `CHARGE_PRE_ACTION_CONDITION` | Who can charge partial amounts | | `CAPTURE_PRE_ACTION_CONDITION` | Who can capture funds from escrow | | `VOID_PRE_ACTION_CONDITION` | Who can refund during escrow | @@ -33,13 +33,13 @@ interface ICondition { - `amount`, The amount involved in the action (0 for authorization-only checks like refund request status updates) - `caller`, The address attempting the action -**Return:** `true` if the caller is authorized, `false` otherwise. +**Return:** `true` if the caller can proceed, `false` otherwise. ## Default Behavior -**Condition slot = `address(0)`**: always returns `true` (allow). The action is unrestricted. +**Condition slot = `address(0)`**: always returns `true` (allow). The action has no restrictions. -This means you only need to set conditions for slots you want to restrict. Leave the rest as `address(0)`. +You only need to set conditions for slots you want to restrict. Leave the rest as `address(0)`. ## Singleton vs Per-Deployment @@ -57,7 +57,7 @@ This means you only need to set conditions for slots you want to restrict. Leave - Conditions should be `view` or `pure` to prevent reentrancy attacks - Never make external state-changing calls inside a condition -- Test thoroughly, edge cases in authorization logic can lead to locked funds +- Cover edge cases in tests. Bugs in authorization logic can lock funds ## Configuration Patterns @@ -103,7 +103,7 @@ For complete configuration examples, see the [Examples](/contracts/examples) pag ### Singleton Reuse -Singleton conditions are deployed once and reused by all operators. Reference the existing addresses, don't deploy new instances: +All operators reuse the same singleton conditions. Reference the existing addresses, don't deploy new instances: ```typescript // Good: Reference the singleton address diff --git a/contracts/conditions/receiver.mdx b/contracts/conditions/receiver.mdx index 6abafff..2be6ef6 100644 --- a/contracts/conditions/receiver.mdx +++ b/contracts/conditions/receiver.mdx @@ -30,7 +30,7 @@ The condition compares `caller` against `payment.receiver`, pure computation wit |------|----------| | `CAPTURE_PRE_ACTION_CONDITION` | Let receiver capture funds after escrow | | `CHARGE_PRE_ACTION_CONDITION` | Let receiver charge partial amounts | -| `VOID_PRE_ACTION_CONDITION` | Let receiver voluntarily refund | +| `VOID_PRE_ACTION_CONDITION` | Let receiver issue refunds at their discretion | For capture, ReceiverCondition is often composed with [EscrowPeriod](/contracts/conditions/escrow-period) via [AndCondition](/contracts/conditions/combinators) to ensure the escrow window has passed before the receiver can capture. diff --git a/contracts/conditions/static-address.mdx b/contracts/conditions/static-address.mdx index 32be52c..cf7f822 100644 --- a/contracts/conditions/static-address.mdx +++ b/contracts/conditions/static-address.mdx @@ -49,13 +49,13 @@ const arbiterCondition = await staticAddressConditionFactory.write.deploy([arbit // For subscription service provider const providerCondition = await staticAddressConditionFactory.write.deploy([serviceProviderAddress]); -// For DAO governance — same address = same deterministic address (idempotent) +// For DAO governance, same address produces the same deterministic deployment (idempotent) const daoCondition = await staticAddressConditionFactory.write.deploy([daoMultisigAddress]); ``` ## Gas -**Cost:** Minimal — `view` function with a single `immutable` read (compiled as a constant in bytecode, not a storage read). +**Cost:** Minimal. `view` function with a single `immutable` read (compiled as a constant in bytecode, not a storage read). ## Next Steps diff --git a/contracts/examples.mdx b/contracts/examples.mdx index 96ac6e5..d69db30 100644 --- a/contracts/examples.mdx +++ b/contracts/examples.mdx @@ -5,7 +5,7 @@ icon: "code" --- -The configuration examples below use simplified pseudo-code (e.g., `new StaticAddressCondition(...)`, `new AndCondition(...)`) to illustrate the logical composition of conditions. In practice, deploy conditions via their respective [factory contracts](/contracts/factories) using viem. See the [Deploy an operator guide](/sdk/deploy-operator) for executable code. +The configuration examples below use simplified pseudo-code (for example, `new StaticAddressCondition(args)` or `new AndCondition([list])`) to illustrate how conditions compose. In practice, deploy conditions via their respective [factory contracts](/contracts/factories) using viem. See the [Deploy an operator guide](/sdk/deploy-operator) for executable code. ## Example 1: Standard E-Commerce with 7-Day Escrow @@ -28,7 +28,7 @@ The configuration examples below use simplified pseudo-code (e.g., `new StaticAd const freezePolicy = await freezePolicyFactory.deploy( PAYER_CONDITION, // Only payer can freeze arbiterCondition.address, // Only arbiter can unfreeze - 3 * 24 * 60 * 60 // 3 days (auto-expires) + 3 * 24 * 60 * 60 // 3 days (expires automatically) ); ``` @@ -126,7 +126,7 @@ sequenceDiagram ## Example 2: Instant Payment (Using Charge) -**Use Case:** Digital goods or services where immediate payment to seller is expected. +**Use Case:** Digital goods or services where the seller expects immediate payment. ### Configuration @@ -160,11 +160,11 @@ Buyer approves tokens → Seller calls charge() → Funds transferred in one tx ``` **Trade-offs:** -- ✅ Single transaction - no separate authorize step -- ✅ Instant delivery for digital goods -- ✅ Better UX - seller gets paid immediately -- ❌ No buyer protection escrow period -- ❌ Payment immediately moves to after capture state +- Single transaction, no separate `authorize` step +- Instant delivery for digital goods +- Better UX: seller gets paid immediately +- No buyer protection escrow period +- Payment moves to the captured state right away --- @@ -368,7 +368,7 @@ Factory-level fees still apply (MAX_TOTAL_FEE_RATE and PROTOCOL_FEE_PERCENTAGE). ## Example 6: Receiver-Initiated Refunds -**Use Case:** Receiver can voluntarily offer refunds (e.g., return policy). +**Use Case:** Receiver can offer refunds (for example, a return policy). ### Configuration @@ -515,7 +515,7 @@ sequenceDiagram ## Example 8: DAO Treasury Controlled -**Use Case:** DAO manages grant releases via multisig governance. No arbiter needed - DAO is the authority. +**Use Case:** DAO manages grant releases via multisig governance. No arbiter needed: the DAO is the authority. ### Configuration @@ -744,8 +744,8 @@ Before deploying, verify: - [ ] Fee rates are competitive and sustainable - [ ] Protocol fee percentage is reasonable - [ ] Tested on testnet with same configuration -- [ ] Arbiter address is controlled (preferably multisig) -- [ ] Condition contracts are verified on block explorer +- [ ] A trusted party controls the arbiter address (preferably multisig) +- [ ] Verify condition contracts on the block explorer --- @@ -827,7 +827,7 @@ Test your configuration handles: - Freeze expiry - Refunds in both states - Fee distribution -- Multiple partial charges +- Partial charges followed by capture diff --git a/contracts/factories.mdx b/contracts/factories.mdx index 34b6624..1cf82a2 100644 --- a/contracts/factories.mdx +++ b/contracts/factories.mdx @@ -8,7 +8,7 @@ icon: "industry" x402r uses the factory pattern with CREATE2 for gas-efficient, deterministic contract deployments. Factories enable on-demand instance creation with predictable addresses. -## Why Factories? +## Why factories @@ -19,17 +19,17 @@ x402r uses the factory pattern with CREATE2 for gas-efficient, deterministic con - Multiple instances can share immutable configuration: + Many instances can share immutable configuration: - Lower deployment costs - Consistent behavior across instances - Centralized ownership control - Calling factory with same parameters returns existing contract: - - Safe to call multiple times + Calling a factory with the same parameters returns the existing contract: + - Safe to call again - No duplicate deployments - - Automatic deduplication + - Built-in deduplication @@ -78,10 +78,10 @@ function deployOperator( ``` **Parameters (in config):** -- `feeReceiver` - Who receives operator fees (arbiter, service provider, treasury, etc.) +- `feeReceiver` - Who receives operator fees (arbiter, service provider, or treasury) - `authorizePreActionCondition` through `refundPostActionHook` - 10-slot configuration -**Note:** `maxFeeBps` and `protocolFeePct` are set at factory level (shared across all operators) +**Note:** the factory sets `maxFeeBps` and `protocolFeePct` (shared across all operators) **Returns:** Address of deployed operator (or existing if already deployed) @@ -220,7 +220,7 @@ function deploy( ``` **Parameters:** -- `escrowPeriod` - Duration in seconds (e.g., `7 * 24 * 60 * 60` for 7 days) +- `escrowPeriod` - Duration in seconds (for example, `7 * 24 * 60 * 60` for 7 days) - `authorizedCodehash` - Runtime codehash of authorized caller (`bytes32(0)` = operator-only) **Returns:** Address of deployed EscrowPeriod contract @@ -293,7 +293,7 @@ const config = { ## Freeze Factory -Deploys `Freeze` condition contracts that block capture when a payment is frozen. +Deploys `Freeze` condition contracts that block capture when the payer freezes a payment. ### Contract Address @@ -311,8 +311,8 @@ function deploy( ``` **Parameters:** -- `freezeCondition` - ICondition that authorizes freeze calls (e.g., PayerCondition) -- `unfreezeCondition` - ICondition that authorizes unfreeze calls (e.g., PayerCondition, ArbiterCondition) +- `freezeCondition` - ICondition that gates freeze calls (for example, PayerCondition) +- `unfreezeCondition` - ICondition that gates unfreeze calls (for example, PayerCondition or ArbiterCondition) - `freezeDuration` - How long freeze lasts in seconds (`0` = permanent until unfrozen) - `escrowPeriodContract` - Address of EscrowPeriod contract (`address(0)` = freeze unconstrained by time) @@ -362,7 +362,7 @@ Use these pre-deployed condition contracts: - Payer can freeze, arbiter can unfreeze (or auto-expires after 3 days): + Payer can freeze, arbiter can unfreeze (or it expires after 3 days): ```typescript const freeze = await freezeFactory.deploy( @@ -375,7 +375,7 @@ Use these pre-deployed condition contracts: - Receiver can freeze, arbiter can unfreeze (or auto-expires after 5 days): + Receiver can freeze, arbiter can unfreeze (or it expires after 5 days): ```typescript const freeze = await freezeFactory.deploy( @@ -437,7 +437,7 @@ Freeze duration should balance payer protection with receiver UX. Too long and r ## Factory Ownership -All factories are owned by a multisig wallet for security. +A multisig wallet owns all factories for security. ### Owner Capabilities @@ -447,7 +447,7 @@ Factory owners can: - Transfer ownership (2-step process) Factory owners **cannot:** -- Modify deployed instances +- Change deployed instances - Pause or stop operations - Access funds in deployed operators @@ -568,11 +568,11 @@ Deploy on testnet with same configuration before mainnet: const testHash = await testnetFactory.write.deployOperator([config]); // ... test thoroughly ... -// Deploy on mainnet with identical config (same address!) +// Deploy on mainnet with identical config (same address) const mainnetHash = await mainnetFactory.write.deployOperator([config]); ``` -### 4. Document Your Config +### 4. Document your config Keep a record of your deployed configurations: diff --git a/contracts/fees.mdx b/contracts/fees.mdx index bb4110e..0ff5358 100644 --- a/contracts/fees.mdx +++ b/contracts/fees.mdx @@ -6,7 +6,7 @@ icon: "coins" ## Overview -x402r uses an **additive modular** fee system: `totalFee = protocolFee + operatorFee`. Each layer is independently configurable, and fees are split between a shared protocol recipient and a per-operator fee recipient. +x402r uses an **additive modular** fee system: `totalFee = protocolFee + operatorFee`. Each layer is independently configurable, and the operator splits fees between a shared protocol recipient and a per-operator fee recipient. ## Fee Architecture @@ -104,7 +104,7 @@ const sameAddress = await staticFeeCalculatorFactory.write.deploy([250]); ## Fee Locking -Fees are **locked at authorization time** to prevent protocol fee changes from breaking already-authorized payments. +The operator **locks fees at authorization time** so later protocol fee changes don't break already-authorized payments. ```solidity struct AuthorizedFees { @@ -139,7 +139,7 @@ This ensures payers always know the fee range they're agreeing to. ## Fee Distribution -Fees accumulate in the operator contract and are distributed via `distributeFees()`: +Fees accumulate in the operator contract. Call `distributeFees()` to disburse them: ```solidity // Anyone can call to distribute fees for a token @@ -157,7 +157,7 @@ operator.distributeFees(usdcAddress); 6. Reset accumulated tracking to 0 -`distributeFees()` is permissionless, anyone can trigger distribution. This prevents fees from being stuck in the operator. +`distributeFees()` is permissionless, anyone can trigger distribution. This stops fees from accumulating indefinitely in the operator. ## ProtocolFeeConfig @@ -201,7 +201,7 @@ await protocolFeeConfig.executeRecipient(); ``` -Operator fees are **immutable**: set at deploy time via `IFeeCalculator` and `FEE_RECEIVER`. Only protocol fees can be changed (with 7-day timelock). Already-authorized payments use locked fee rates regardless. +Operator fees are **immutable**: set at deploy time via `IFeeCalculator` and `FEE_RECEIVER`. Only protocol fees support updates (with 7-day timelock). Already-authorized payments use locked fee rates regardless. ### Disabling Protocol Fees diff --git a/contracts/gas-costs.mdx b/contracts/gas-costs.mdx index cc2e13f..509a305 100644 --- a/contracts/gas-costs.mdx +++ b/contracts/gas-costs.mdx @@ -6,12 +6,12 @@ icon: "gas-pump" ## Overview -x402r adds escrow, refund windows, and dispute resolution on top of the [Commerce Payments Protocol](https://github.com/base/commerce-payments). Here you'll find the **measured gas cost** of every on-chain operation so you can evaluate the overhead. +x402r adds escrow, refund windows, and dispute resolution on top of the [Commerce Payments Protocol](https://github.com/base/commerce-payments). Below you'll find the **measured gas cost** of every on-chain operation so you can weigh the overhead. All numbers are from Foundry simulations (`forge test`) with optimizer enabled (200 runs, via IR). The benchmark test is at [`test/gas/GasBenchmark.t.sol`](https://github.com/BackTrackCo/x402r-contracts/blob/main/test/gas/GasBenchmark.t.sol). -The buyer never pays gas. They only sign an off-chain ERC-3009 authorization. All on-chain transactions are submitted by the facilitator, merchant, or other parties. +The buyer never pays gas. They only sign an off-chain ERC-3009 authorization. The facilitator, merchant, or another party submits every on-chain transaction. ## What you'll pay on Base @@ -26,11 +26,11 @@ Disputes are rare and add < $0.005 with off-chain resolution, see [Dispute Path] ## Happy Path -The happy path has **2 on-chain transactions**: `authorize` (at purchase time) and `capture` (after the escrow period expires). +The happy path has **2 on-chain transactions**: `authorize` (at checkout) and `capture` (after the escrow period expires). | Operation | Gas | vs transfer | Who Calls | When | |-----------|-----|------------|-----------|------| -| `authorize()` | 181,544 | 17.6x | Facilitator | At purchase (HTTP 402 settlement) | +| `authorize()` | 181,544 | 17.6x | Facilitator | At checkout (HTTP 402 settlement) | | `capture()` | 150,262 | 14.6x | Anyone | After escrow period expires | The **vs transfer** column shows multiples of a cold ERC-20 `transfer()` (10,305 gas), the absolute floor for moving tokens on-chain. @@ -40,12 +40,12 @@ In production, the merchant typically calls `capture()`, but the function has no An escrow authorization is inherently more work than a raw ERC-20 transfer: it validates payment info, checks fee bounds, locks fees, transfers tokens into escrow, and records state. The per-plugin section below shows exactly where the gas goes. -**Facilitators: set a gas limit.** The facilitator pays gas for `authorize()`, but the operator chooses which conditions and hooks are configured. Each plugin slot adds cost, and custom plugins can run arbitrary computation. Simulate the transaction with `eth_estimateGas` before submitting and reject operators whose `authorize()` exceeds a reasonable threshold (e.g., 300,000 gas). The full x402r configuration uses ~181,000, anything significantly above that warrants investigation. +**Facilitators: set a gas limit.** The facilitator pays gas for `authorize()`, but the operator chooses which conditions and hooks to run. Each plugin slot adds cost, and custom plugins can run arbitrary computation. Simulate the transaction with `eth_estimateGas` before submitting and reject operators whose `authorize()` exceeds a reasonable threshold (for example, 300,000 gas). The full x402r configuration uses around 181,000 gas; anything well above that warrants investigation. ## Per-Plugin Gas Costs -The PaymentOperator is configured with pluggable conditions (checked before an action) and hooks (called after). You choose which plugins to use. Here's the marginal cost of each, measured by diffing adjacent configurations. +The PaymentOperator runs with pluggable conditions (checked before an action) and hooks (called after). You choose which plugins to use. Here's the marginal cost of each, measured by diffing adjacent configurations. ### authorize() @@ -70,16 +70,16 @@ The EscrowPeriod hook is the single most expensive plugin on `authorize` because | + Freeze + AndCondition | 142,961 | **+20,441** | AndCondition combinator loop + `Freeze.check()` reads `frozenUntil[hash]` + internal `isDuringEscrowPeriod()` | -**Simple conditions are nearly free.** `ReceiverCondition` and `PayerCondition` cost ~4,500 gas, they only compare calldata fields. Cross-contract conditions like `EscrowPeriod` cost ~5,500 due to a cold SLOAD. The `Freeze` condition is the most expensive single condition (+20,441) because of the AndCondition combinator overhead, its own `frozenUntil` storage read, and an internal escrow period check. +**Simple conditions are close to free.** `ReceiverCondition` and `PayerCondition` cost around 4,500 gas; they only compare calldata fields. Cross-contract conditions like `EscrowPeriod` cost around 5,500 because of a cold SLOAD. The `Freeze` condition is the most expensive single condition (+20,441) because of the AndCondition combinator overhead, its own `frozenUntil` storage read, and an internal escrow period check. ## Dispute Path -These operations only happen when a payment is disputed. Most payments never touch this path. +These operations only happen when a buyer disputes a payment. Most payments never touch this path. ### Off-chain resolution -The refund request, evidence submission, and arbiter approval can all happen off-chain. The only on-chain steps are `freeze()` (to lock the payment during the escrow window) and `voidPayment()` (to return funds). The arbiter never submits a transaction, their approval is an EIP-712 signature that anyone can relay. +The refund request, evidence submission, and arbiter approval can all happen off-chain. The only on-chain steps are `freeze()` (to lock the payment during the escrow window) and `voidPayment()` (to return funds). The arbiter never submits a transaction; their approval is an EIP-712 signature that anyone can relay. | On-chain step | Gas | vs transfer | Who Calls | |--------------|-----|------------|-----------| @@ -104,10 +104,10 @@ If the parties choose to handle the dispute fully on-chain instead: | `refund()` | 54,467 | 5.3x | Anyone | Pulls funds from merchant wallet via ReceiverRefundCollector | | **Total** | **1,078,145** | **104.6x** | | | -This total includes the happy path steps (`authorize` + `capture`) since those have already been paid. The dispute-only overhead is 746,339 gas (< $0.02 on Base). +This total includes the happy path steps (`authorize` + `capture`) since those already ran. The dispute-only overhead is 746,339 gas (< $0.02 on Base). -`requestRefund()` at 421,689 gas is the most expensive operation because it writes to **multiple storage mappings** for indexing: +`requestRefund()` at 421,689 gas is the most expensive operation because it writes to **five storage mappings** for indexing: - Refund request data (status, amount, payment hash) - Payer index (`payerRefundRequests[payer][n]`) @@ -123,7 +123,7 @@ This indexing enables efficient off-chain queries but costs more gas upfront. On | Scenario | Gas | vs transfer | Cost on Base | |----------|-----|------------|-------------| | ERC-20 transfer (baseline) | 10,305 | 1x | < $0.001 | -| Commerce Payments escrow, no operator (authorize + capture) | 144,718 | 14.0x | < $0.005 | +| Commerce Payments escrow, no operator (`authorize` + `capture`) | 144,718 | 14.0x | < $0.005 | | + PaymentOperator layer, no plugins | 195,176 | 18.9x | < $0.005 | | + fees | 252,941 | 24.5x | < $0.005 | | + fees + simple condition | 257,391 | 25.0x | < $0.005 | @@ -134,12 +134,12 @@ This indexing enables efficient off-chain queries but costs more gas upfront. On The full x402r happy path uses ~32x the gas of a single ERC-20 transfer, but on Base L2, the absolute cost stays under a penny. The overhead comes from escrow validation, fee locking, cross-contract storage writes, and condition checks, all detailed in the per-plugin breakdown above. -All numbers above assume one payment per transaction. Batching multiple operations in a single transaction (via a multicall contract) can reduce per-payment costs by 37–80% due to warm EVM access, contract addresses and shared storage only need to be loaded once. The benchmark test includes warm measurements for reference. +All numbers above assume one payment per transaction. Batching operations in a single transaction (via a multicall contract) can reduce per-payment costs by 37 to 80 percent thanks to warm EVM access; contract addresses and shared storage only load once. The benchmark test includes warm measurements for reference. - How protocol and operator fees are calculated and distributed + How the operator calculates and distributes protocol and operator fees How conditions, hooks, and escrow fit together diff --git a/contracts/hooks/authorization-time.mdx b/contracts/hooks/authorization-time.mdx index 4bf9e74..758bf50 100644 --- a/contracts/hooks/authorization-time.mdx +++ b/contracts/hooks/authorization-time.mdx @@ -6,10 +6,10 @@ icon: "clock" ## Overview -AuthorizationTimeRecorderHook stores the `block.timestamp` when a payment is authorized. This timestamp is used by time-based conditions like [EscrowPeriod](/contracts/conditions/escrow-period). +AuthorizationTimeRecorderHook stores `block.timestamp` at the moment the operator authorizes a payment. Time-based conditions like [EscrowPeriod](/contracts/conditions/escrow-period) read this timestamp to gate later actions. -[EscrowPeriod](/contracts/conditions/escrow-period) **extends** AuthorizationTimeRecorderHook and adds `ICondition` implementation. For escrow enforcement, use EscrowPeriod directly instead of deploying AuthorizationTimeRecorderHook separately. +[EscrowPeriod](/contracts/conditions/escrow-period) **extends** AuthorizationTimeRecorderHook and adds an `ICondition` implementation. For escrow enforcement, use EscrowPeriod directly instead of deploying AuthorizationTimeRecorderHook on its own. ## State diff --git a/contracts/hooks/combinator.mdx b/contracts/hooks/combinator.mdx index 62a0802..ce17955 100644 --- a/contracts/hooks/combinator.mdx +++ b/contracts/hooks/combinator.mdx @@ -1,12 +1,12 @@ --- title: "HookCombinator" -description: "Chain multiple hooks into a single operator slot for composite state tracking" +description: "Chain hooks into a single operator slot for composite state tracking" icon: "layer-group" --- ## Overview -HookCombinator chains multiple hooks into one, calling each in sequence. Since each operator slot accepts only one hook address, use HookCombinator when you need multiple hooks for the same action. +HookCombinator chains hooks into one, invoking each in sequence. Each operator slot accepts only one hook address, so use HookCombinator when you need more than one hook on the same action. ## Deployment @@ -22,14 +22,14 @@ config.authorizePostActionHook = comboAddress; ## Behavior -- Hooks are called in the order provided +- The combinator invokes hooks in the order provided - **If any hook reverts, all revert**: the entire recording is atomic - Each hook receives the same `paymentInfo`, `amount`, and `caller` parameters ## Limits -**Max 10 hooks per combinator.** Each additional hook adds ~1k gas overhead for the delegation call. +**Max 10 hooks per combinator.** Each extra hook adds ~1k gas overhead for the delegation call. ## Gas diff --git a/contracts/hooks/custom.mdx b/contracts/hooks/custom.mdx index 2d0d016..b1f1048 100644 --- a/contracts/hooks/custom.mdx +++ b/contracts/hooks/custom.mdx @@ -6,9 +6,9 @@ icon: "wrench" ## Overview -You can create custom hooks for specialized tracking beyond what the built-in hooks provide. Implement the `IHook` interface and extend `BaseHook` for operator access control. +You can build custom hooks for specialized tracking beyond what the built-in hooks provide. Use the `IHook` interface and extend `BaseHook` for operator access control. -## IHook Interface +## `IHook` interface ```solidity interface IHook { @@ -105,10 +105,10 @@ contract ReleaseCountHookTest is Test { - [ ] Extends `BaseHook` for operator access control - [ ] Handles edge cases (zero amounts, duplicate records) - [ ] Gas-efficient storage layout -- [ ] Comprehensive test coverage +- [ ] Full test coverage across the public surface -Unlike conditions, hooks **do modify state**. Ensure proper access control via `BaseHook` to prevent unauthorized writes. +Unlike conditions, hooks **do mutate state**. Use `BaseHook` for access control to keep unauthorized writes out. ## Next Steps diff --git a/contracts/hooks/overview.mdx b/contracts/hooks/overview.mdx index 7426cc7..3bc4666 100644 --- a/contracts/hooks/overview.mdx +++ b/contracts/hooks/overview.mdx @@ -4,13 +4,13 @@ description: "State recording system for tracking payment lifecycle events" icon: "database" --- -## What Are Hooks? +## What are hooks -Hooks are pluggable contracts that update state **after** an action successfully executes on a PaymentOperator. Each operator has **5 hook slots**: one per action: +Hooks are pluggable contracts that update state **after** an action successfully executes on a PaymentOperator. Each operator has **5 hook slots**, one per action: -| Slot | Records After | +| Slot | Records after | |------|---------------| -| `AUTHORIZE_POST_ACTION_HOOK` | Authorization (e.g., timestamp) | +| `AUTHORIZE_POST_ACTION_HOOK` | Authorization (for example, timestamp) | | `CHARGE_POST_ACTION_HOOK` | Charge event | | `CAPTURE_POST_ACTION_HOOK` | Capture from escrow | | `VOID_POST_ACTION_HOOK` | Void | @@ -33,51 +33,51 @@ interface IHook { - `amount`, The amount involved in the action - `caller`, The address that executed the action (msg.sender on operator) -## Default Behavior +## Default behavior -**Hook slot = `address(0)`**: no-op (does nothing). No state is recorded. +**Hook slot = `address(0)`**: no-op (does nothing). The operator records no state for that slot. -This means you only need to set hooks for slots where you want state tracking. Leave the rest as `address(0)`. +Set hooks only on the slots where you want state tracking. Leave the rest as `address(0)`. ## BaseHook All built-in hooks extend `BaseHook`, which verifies that the caller is an authorized operator. This prevents unauthorized contracts from writing state. -## Choosing a Recording Strategy +## Choosing a recording strategy Not every payment needs on-chain hooks. Choose based on your use case: -### Events Only (~0 Extra Gas) +### Events only (~0 extra gas) -The operator already emits events (`AuthorizeExecuted`, `CaptureExecuted`, etc.) for every action. If you only need payment history for analytics or display, skip hooks entirely and index events off-chain. +The operator already emits an event for every action (`AuthorizeExecuted`, `CaptureExecuted`, and the same shape for the rest). If you only need payment history for analytics or display, skip hooks entirely and index events off-chain. -**Best for:** Micropayments, high-volume payments where gas overhead matters, simple UIs. +**Best for:** micropayments, high-volume payments where gas overhead matters, simple UIs. -### Events + Subgraph (Best Queries) +### Events plus subgraph (richest queries) Index operator events with a subgraph for rich queries (payment history by payer, receiver, status, date range). No on-chain hook gas cost. -**Best for:** Analytics dashboards, payment history, multi-payment queries. +**Best for:** analytics dashboards, payment history, cross-payment queries. -**Trade-off:** Requires subgraph infrastructure (semi-centralized). +**Trade-off:** requires subgraph infrastructure (semi-centralized). -### On-Chain Hooks (~20k Gas per Write) +### On-chain hooks (~20k gas per write) -Use hooks when you need **on-chain reads**: other contracts or conditions that depend on recorded state. [EscrowPeriod](/contracts/conditions/escrow-period) is the most common example: it records authorization time so the capture condition can check if the escrow window has passed. +Use hooks when you need **on-chain reads**: other contracts or conditions that depend on recorded state. [EscrowPeriod](/contracts/conditions/escrow-period) is the most common example. It records authorization time so the capture condition can check if the escrow window has passed. -**Best for:** Escrow enforcement, dispute evidence, decentralized frontends, on-chain composability. +**Best for:** escrow enforcement, dispute evidence, decentralized frontends, on-chain composability. **Trade-off:** ~20k gas per `SSTORE` operation. -### Decision Table +### Decision table -| Need | Strategy | Hook Slots | +| Need | Strategy | Hook slots | |------|----------|---------------| | Payment history for UI | Events only | `address(0)` | | Rich queries, analytics | Events + Subgraph | `address(0)` | | Time-locked releases | On-chain | [EscrowPeriod](/contracts/conditions/escrow-period) on `AUTHORIZE_POST_ACTION_HOOK` | | On-chain payment index | On-chain | [PaymentIndexRecorderHook](/contracts/hooks/payment-index) | -| Multiple data points | On-chain | [HookCombinator](/contracts/hooks/combinator) | +| Many data points | On-chain | [HookCombinator](/contracts/hooks/combinator) | For most configurations, you only need a hook on the `AUTHORIZE_POST_ACTION_HOOK` slot (for [EscrowPeriod](/contracts/conditions/escrow-period)). Leave other hook slots as `address(0)`. @@ -93,7 +93,7 @@ For most configurations, you only need a hook on the `AUTHORIZE_POST_ACTION_HOOK Index payments for on-chain queries. - Chain multiple hooks into one slot. + Chain hooks into one slot. Build your own hook. diff --git a/contracts/hooks/payment-index.mdx b/contracts/hooks/payment-index.mdx index 8bf53e6..58e42d7 100644 --- a/contracts/hooks/payment-index.mdx +++ b/contracts/hooks/payment-index.mdx @@ -1,20 +1,20 @@ --- title: "PaymentIndexRecorderHook" -description: "Index payments by sequential count for on-chain queries and multiple refund requests" +description: "Index payments by sequential count for on-chain queries and repeated refund requests" icon: "list-ol" --- ## Overview -PaymentIndexRecorderHook records a sequential index for each payment action, enabling multiple refund requests per payment. Each time `record()` is called, the index increments by 1. +PaymentIndexRecorderHook records a sequential index for each payment action, so one payment can support more than one refund request. Every `record()` call increments the index by 1. -## When to Use +## When to use - You need on-chain payment lookups **without a subgraph** -- You need to support **multiple refund requests** per payment (each keyed by nonce) +- You need to support **more than one refund request** per payment (each keyed by nonce) - Other contracts need to read the payment index on-chain -**Skip when:** You're using a subgraph for payment queries, the subgraph can derive indexes from events without the on-chain gas cost. +**Skip when:** you're using a subgraph for payment queries, since the subgraph can derive indexes from events without the on-chain gas cost. ## State diff --git a/contracts/hooks/refund-request.mdx b/contracts/hooks/refund-request.mdx index f79df80..0eda209 100644 --- a/contracts/hooks/refund-request.mdx +++ b/contracts/hooks/refund-request.mdx @@ -46,7 +46,7 @@ icon: "rotate-left" ## Request status states -Each payment supports one refund request, keyed by `paymentInfoHash`. Requesting again is only allowed after the prior request was cancelled. +Each payment supports one refund request, keyed by `paymentInfoHash`. The payer may only request again after cancelling the prior request. ```mermaid stateDiagram-v2 @@ -80,7 +80,7 @@ function requestRefund( **Parameters:** - `paymentInfo`: PaymentInfo struct -- `amount`: amount being requested for refund (uint120) +- `amount`: refund amount the payer is asking for (uint120) **Reverts** if a non-cancelled request already exists, if the payment is unknown to the canonical escrow, or if `amount == 0`. @@ -112,7 +112,7 @@ function refuse(AuthCaptureEscrow.PaymentInfo calldata paymentInfo) external ### Approval -There is no `updateStatus` / explicit `approve` entrypoint. Approval happens automatically when the arbiter executes the refund through the operator (`operator.void()` or `operator.refund()`), which fires `VOID_POST_ACTION_HOOK` / `REFUND_POST_ACTION_HOOK` and flips status to `Approved`. +The contract has no `updateStatus` or explicit `approve` entrypoint. Approval happens automatically when the arbiter executes the refund through the operator (`operator.void()` or `operator.refund()`), which fires `VOID_POST_ACTION_HOOK` / `REFUND_POST_ACTION_HOOK` and flips status to `Approved`. ### Query helpers diff --git a/contracts/license.mdx b/contracts/license.mdx index ebd763e..27b7721 100644 --- a/contracts/license.mdx +++ b/contracts/license.mdx @@ -4,15 +4,15 @@ description: "BUSL-1.1 licensing terms, permissions, and restrictions for x402r icon: "scale-balanced" --- -## Why BUSL-1.1? +## Why BUSL-1.1 -We want the code to be fully readable and usable on-chain — you can integrate with deployed x402r contracts, build on top of them, and inspect every line of source. The license protects against forks that compete with or commoditize the protocol (e.g., stripping fees and redeploying), so that protocol fees can continue funding development, audits, and new features for everyone building on x402r. +The code is fully readable and usable on-chain. You can integrate with deployed x402r contracts, build on top of them, and inspect every line of source. The license protects against forks that compete with or commoditize the protocol (for example, stripping fees and redeploying), so that protocol fees can keep funding development, audits, and new features for everyone building on x402r. -BUSL-1.1 protects against that while keeping the code open. After the Change Date, everything converts to MIT and is fully permissionless. This is the same approach used by Uniswap, Aave, and other major DeFi protocols. +BUSL-1.1 protects against that while keeping the code open. After the Change Date, everything converts to MIT and is fully permissionless. Uniswap, Aave, and other major DeFi protocols use the same approach. ## License Terms -All Solidity source files in `x402r-contracts/src/` are licensed under the **Business Source License 1.1**. +The **Business Source License 1.1** covers all Solidity source files in `x402r-contracts/src/`. | Parameter | Value | |-----------|-------| @@ -24,41 +24,41 @@ All Solidity source files in `x402r-contracts/src/` are licensed under the **Bus | **Change Date** | December 9, 2029 | | **Change License** | MIT License | -## What Can You Do? +## What you can do -### On-Chain +### On-chain -You can freely interact with x402r contracts that are already deployed: +You can interact with deployed x402r contracts: -- **Integrate** — call x402r contracts from your own contracts or dApps -- **Build** — create applications, services, and protocols on top of x402r -- **Deploy via factories** — use x402r's official factories (e.g., `PaymentOperatorFactory`, `EscrowPeriodFactory`) to deploy your own operator instances with your own configuration +- **Integrate**: call x402r contracts from your own contracts or dApps +- **Build**: create applications, services, and protocols on top of x402r +- **Deploy via factories**: use x402r's official factories (for example, `PaymentOperatorFactory` and `EscrowPeriodFactory`) to deploy your own operator instances with your own configuration -### Off-Chain +### Off-chain -You can freely work with the source code: +You can work with the source code: - **Read and learn** from the code -- **Fork and modify** for local development and testing +- **Fork and adapt** for local development and testing - **Redistribute** the source code -- **Deploy locally** — spin up Anvil, Hardhat, or any local/test environment for integration testing +- **Deploy locally**: spin up Anvil, Hardhat, or any local or test environment for integration testing -### The One Restriction +### The one restriction -Do not deploy x402r contracts outside of the official factories — whether modified or unmodified. +Do not deploy x402r contracts outside of the official factories, whether modified or unmodified. Deploying through x402r's factories is the intended path and is always allowed. What you cannot do: - Take the source code and deploy your own instances outside of the factories -- Deploy a modified fork (e.g., removing fees, changing parameters) to any production chain -- Remove or modify the license notice +- Deploy a modified fork (for example, removing fees or changing parameters) to any production chain +- Remove or change the license notice Deploying to local chains, testnets, and private forks for **development and testing** is fine. ### Change Date -On **December 9, 2029** (or 4 years after the first public release of each version, whichever comes first), the license automatically converts to the **MIT License** — at which point you can deploy, fork, and do anything you want. +On **December 9, 2029** (or 4 years after the first public release of each version, whichever comes first), the license automatically converts to the **MIT License**, at which point you can deploy, fork, and do anything you want. ## Summary diff --git a/contracts/overview.mdx b/contracts/overview.mdx index f54c090..db337bf 100644 --- a/contracts/overview.mdx +++ b/contracts/overview.mdx @@ -4,7 +4,7 @@ description: "Introduction to x402r smart contracts and their relationship to co icon: "circle-info" --- -## What is x402r? +## What is x402r x402r is a smart contract extension for HTTP-native refundable payments. It builds on the canonical [Commerce Payments Protocol](https://github.com/base/commerce-payments) to add dispute resolution, escrow periods, and refund capabilities. @@ -79,7 +79,7 @@ x402r extends commerce-payments with flexible payment capabilities: **Hooks (IHook)** - State updates after actions **10-slot configuration per operator:** -- 5 condition slots (before action): authorize, charge, capture, void, refund +- 5 condition slots (before action): `authorize`, `charge`, `capture`, `void`, `refund` - 5 hook slots (after action): state tracking for each action **Benefits:** @@ -92,12 +92,12 @@ x402r extends commerce-payments with flexible payment capabilities: **EscrowPeriod** - Combined hook and condition that tracks authorization time and enforces escrow period -**Freeze** - Standalone condition that blocks capture when payment is frozen (with configurable freeze/unfreeze authorization) +**Freeze** - Standalone condition that blocks capture while a freeze remains active (with configurable freeze/unfreeze authorization) **Key features:** -- Configurable escrow periods (e.g., 7 days, 14 days) +- Configurable escrow periods (for example, 7 days or 14 days) - Payer-initiated freezes to stop suspicious releases -- Time-limited freeze durations (e.g., 3 days) +- Time-limited freeze durations (for example, 3 days) - MEV protection via private mempool support - Composable via `AndCondition([escrowPeriod, freeze])` @@ -158,7 +158,7 @@ All factories use **universal CREATE2 addresses**: same address on every support Most contracts are immutable to prevent rug pulls and ensure trustlessness: - **PaymentOperator** - Cannot pause or upgrade -- **EscrowPeriod** - Cannot modify escrow period +- **EscrowPeriod** - Cannot change escrow period - **Freeze** - Cannot change freeze rules after deployment Protocol fee configuration is mutable via `ProtocolFeeConfig` (with 7-day timelock). Operator fees are immutable. @@ -172,17 +172,17 @@ Protocol fee configuration is mutable via `ProtocolFeeConfig` (with 7-day timelo -- Conditions can be combined with And/Or/Not logic +- Conditions compose with And/Or/Not logic - Operators can share condition implementations - Factories enable on-demand instance deployment -- Stateless conditions work across multiple operators +- Stateless conditions work across many operators - Reentrancy guards on all state changes - 7-day timelock on protocol fee changes - Two-step ownership transfers -- Comprehensive event logging for monitoring +- Detailed event logging for monitoring ## Architecture Overview diff --git a/contracts/payment-operator.mdx b/contracts/payment-operator.mdx index 396435b..4e772cd 100644 --- a/contracts/payment-operator.mdx +++ b/contracts/payment-operator.mdx @@ -10,7 +10,7 @@ The main payment operator contract with pluggable conditions for flexible author - **Type:** Operator instance (one per fee recipient + configuration) - **Deployment:** Via PaymentOperatorFactory -- **Immutability:** Cannot be paused or upgraded +- **Immutability:** No pause switch, no upgrade path - **Configuration:** 10 slots for conditions and hooks - **Use Cases:** Marketplace, subscriptions, streaming, grants, custom flows @@ -46,7 +46,7 @@ mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees; -1. **AUTHORIZE_POST_ACTION_HOOK** - Record authorization (e.g., timestamp) +1. **AUTHORIZE_POST_ACTION_HOOK** - Record authorization (for example, timestamp) 2. **CHARGE_POST_ACTION_HOOK** - Record charge event 3. **CAPTURE_POST_ACTION_HOOK** - Record capture 4. **VOID_POST_ACTION_HOOK** - Record void @@ -78,7 +78,7 @@ function authorize( **Flow:** 1. Check `AUTHORIZE_PRE_ACTION_CONDITION` (if set) -2. Validate fee bounds compatibility +2. Check fee bounds compatibility 3. Store fees at authorization time (prevents protocol fee changes from breaking capture) 4. Call `escrow.authorize()` 5. Call `AUTHORIZE_POST_ACTION_HOOK` (if set) @@ -87,7 +87,7 @@ function authorize( **Access:** Controlled by `AUTHORIZE_PRE_ACTION_CONDITION` (default: anyone) -**Authorization Expiry:** The `PaymentInfo` struct includes an `authorizationExpiry` field (from base commerce-payments). Set this to `type(uint48).max` for no expiry, or specify a timestamp to allow the payer to reclaim funds after expiry. This is useful for subscription-based payments where you want to limit the authorization window. +**Authorization Expiry:** The `PaymentInfo` struct includes an `authorizationExpiry` field (from base commerce-payments). Set this to `type(uint48).max` for no expiry, or specify a timestamp to let the payer reclaim funds after expiry. Subscription-based payments use this to bound the authorization window. ### charge() @@ -111,7 +111,7 @@ function charge( **Flow:** 1. Check `CHARGE_PRE_ACTION_CONDITION` (if set) -2. Validate fee bounds compatibility +2. Check fee bounds compatibility 3. Call `escrow.charge()` - funds go directly to receiver 4. Accumulate protocol fees for later distribution 5. Call `CHARGE_POST_ACTION_HOOK` (if set) @@ -185,12 +185,12 @@ function void( -For a partial return, call `capture()` for the portion to keep. The unused authorization can then be voided, or it will become reclaimable by the payer after `captureDeadline`. +For a partial return, call `capture()` for the amount to keep. Then `void()` the unused authorization, or let the payer reclaim it after `captureDeadline`. ### refund() -Refunds payment after it has been released (captured). +Refunds a payment after capture (after the receiver has the funds). ```solidity function refund( @@ -205,7 +205,7 @@ function refund( - `paymentInfo` - Payment info struct - `amount` - Amount to refund to payer - `tokenCollector` - Address of the token collector that will source the refund -- `collectorData` - Data to pass to the token collector (e.g., signatures) +- `collectorData` - Data to pass to the token collector (for example, signatures) **Flow:** 1. Check `REFUND_PRE_ACTION_CONDITION` (if set) @@ -213,7 +213,7 @@ function refund( 3. Call `REFUND_POST_ACTION_HOOK` (if set) 4. Emit `RefundExecuted` -**Access:** Controlled by `REFUND_PRE_ACTION_CONDITION`. Permission is also enforced by the token collector (e.g., receiver must have approved it, or collectorData contains receiver's signature). +**Access:** Controlled by `REFUND_PRE_ACTION_CONDITION`. The token collector also enforces permission (for example, the receiver must have approved it, or `collectorData` contains the receiver's signature). **Marketplace example:** StaticAddressCondition(arbiter) - post-delivery disputes @@ -251,7 +251,7 @@ For a 1000 USDC payment: - **Receiver Gets:** 999.50 USDC **Fee Locking:** -Fees are calculated and stored at `authorize()` time in `authorizedFees[hash]`. This prevents protocol fee changes from breaking capture of already-authorized payments. +The operator calculates fees at `authorize()` time and stores them in `authorizedFees[hash]`. This stops later protocol fee changes from breaking capture of already-authorized payments. ```solidity struct AuthorizedFees { @@ -269,7 +269,7 @@ mapping(bytes32 paymentInfoHash => AuthorizedFees) public authorizedFees; ### Fee Distribution -Fees accumulate in the operator contract and are distributed via `distributeFees(token)`: +Fees accumulate in the operator contract. Call `distributeFees(token)` to disburse them: ```solidity // Anyone can call to distribute fees for a token @@ -301,7 +301,7 @@ Protocol fee changes require 7-day timelock. Operator fees are immutable (set at - **ReentrancyGuardTransient** - EIP-1153 transient storage for gas-efficient reentrancy protection - **Ownership** - Solady's Ownable with 2-step transfer - **Timelock** - 7-day delay on protocol fee changes (operator fees are immutable) -- **Immutable Core** - Escrow, conditions, and fee configuration cannot be changed +- **Immutable Core** - Escrow, conditions, and fee configuration stay fixed after deployment ## Next Steps diff --git a/contracts/periphery/auth-capture-escrow.mdx b/contracts/periphery/auth-capture-escrow.mdx index 7a641f3..f1de345 100644 --- a/contracts/periphery/auth-capture-escrow.mdx +++ b/contracts/periphery/auth-capture-escrow.mdx @@ -10,7 +10,7 @@ x402r builds on the canonical [Commerce Payments Protocol](https://github.com/ba - **ERC3009PaymentCollector**: collects funds via signed ERC-3009 `receiveWithAuthorization`. - **Permit2PaymentCollector**: collects funds via Uniswap Permit2 `permitTransferFrom`. -All three are deployed at universal CREATE2 addresses (same address on every supported chain). +All three sit at universal CREATE2 addresses (same address on every supported chain). | Contract | Canonical address | |---|---| @@ -53,7 +53,7 @@ stateDiagram-v2 #### authorize() -Pulls funds into escrow via the token collector. Called by the `captureAuthorizer` (typically the facilitator EOA, or a smart contract acting as captureAuthorizer). +Pulls funds into escrow via the token collector. Only the `captureAuthorizer` (typically a facilitator EOA, or a smart contract acting as captureAuthorizer) can call it. ```solidity function authorize( @@ -123,14 +123,14 @@ function refund( ### Access control -Lifecycle actions (`authorize`, `charge`, `capture`, `void`, `refund`) check `msg.sender` against `PaymentInfo.operator` (the captureAuthorizer). `reclaim` is permissionless after `captureDeadline`. +Lifecycle actions (`authorize`, `charge`, `capture`, `void`, `refund`) check `msg.sender` against `PaymentInfo.operator` (the captureAuthorizer). Anyone can call `reclaim` after `captureDeadline`. -There is no escrow-wide "operator whitelist", access is per-payment via the signed `PaymentInfo`. +The escrow has no global "operator whitelist." Access is per-payment, governed by the signed `PaymentInfo`. ### Security features - **Replay prevention**: each payment has a unique nonce derived from `(chainId, escrowAddress, paymentInfoHash)`, consumed on-chain at settlement -- **Fee bounds enforcement**: `minFeeBps` / `maxFeeBps` / `feeReceiver` in `PaymentInfo` are signed by the client; the escrow rejects out-of-bounds captures/charges +- **Fee bounds enforcement**: the client signs `minFeeBps` / `maxFeeBps` / `feeReceiver` in `PaymentInfo`; the escrow rejects out-of-bounds captures/charges - **Expiry ordering**: contract enforces `preApprovalExpiry <= authorizationExpiry <= refundExpiry` - **Reentrancy protection** on all state-changing entry points @@ -148,8 +148,8 @@ The escrow calls the token collector during `authorize()` or `charge()`, passing - **ERC-3009 `receiveWithAuthorization()`**: gasless token transfers via signed messages - **EIP-6492 support**: handles smart wallet clients with deployment bytecode in signatures (via `ERC6492SignatureHandler`) -- **Nonce-based replay protection**: each authorization can only be used once; the nonce is the payer-agnostic `PaymentInfo` hash -- **Deadline-based expiry**: `validBefore` (typically `now + maxTimeoutSeconds`) prevents stale authorizations +- **Nonce-based replay protection**: each authorization can run only once; the nonce is the payer-agnostic `PaymentInfo` hash +- **Deadline-based expiry**: `validBefore` (typically `now + maxTimeoutSeconds`) blocks stale authorizations ### ERC-3009 signature @@ -174,8 +174,8 @@ The authCapture scheme uses `receiveWithAuthorization` (not `transferWithAuthori ## Permit2PaymentCollector -Collects ERC-20 tokens via Uniswap Permit2 `permitTransferFrom`. Used when `assetTransferMethod === "permit2"` in the scheme `extra`. Any ERC-20 the payer has approved Permit2 for becomes spendable through this collector. +Collects ERC-20 tokens through Uniswap Permit2 `permitTransferFrom`. The operator selects this collector when `assetTransferMethod === "permit2"` in the scheme `extra`. Any ERC-20 the payer has approved Permit2 for becomes spendable through this collector. -The client signs a Permit2 `PermitTransferFrom`; the merchant address is bound through the deterministic nonce, so no separate witness struct is needed. +The client signs a Permit2 `PermitTransferFrom`; the deterministic nonce binds the merchant address, removing the need for a separate witness struct. See the [authCapture Scheme Specification](/x402-integration/auth-capture-scheme) for the full Permit2 wire format. diff --git a/contracts/periphery/overview.mdx b/contracts/periphery/overview.mdx index 074373e..195fad4 100644 --- a/contracts/periphery/overview.mdx +++ b/contracts/periphery/overview.mdx @@ -4,9 +4,9 @@ description: "Supporting contracts that extend the PaymentOperator: escrow, refu icon: "puzzle-piece" --- -## What Are Periphery Contracts? +## What are periphery contracts -Periphery contracts support the [PaymentOperator](/contracts/payment-operator) but are not the operator itself. They handle escrow storage, token collection, refund workflows, and evidence submission. +Periphery contracts support the [PaymentOperator](/contracts/payment-operator) without being the operator itself. They handle escrow storage, token collection, refund workflows, and evidence submission. ## Contract Map @@ -28,6 +28,7 @@ All periphery contracts use **universal CREATE2 addresses**: the same address on |----------|---------| | AuthCaptureEscrow | `0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff` | | ERC3009PaymentCollector | `0x0E3dF9510de65469C4518D7843919c0b8C7A7757` | +| Permit2PaymentCollector | `0x992476B9Ee81d52a5BdA0622C333938D0Af0aB26` | | ProtocolFeeConfig | `0xBe2d24614F339a1eB103A399F93AA2a39Ca815Bc` | | ReceiverRefundCollector | `0x88C9826dFA17Ad9d3a726015C667dD995394D341` | | RefundRequestEvidence | `0x4089A5A853e9eF35f504B842795fB272dF69c739` | diff --git a/contracts/periphery/receiver-refund-collector.mdx b/contracts/periphery/receiver-refund-collector.mdx index 38aa5c7..4c1eb2f 100644 --- a/contracts/periphery/receiver-refund-collector.mdx +++ b/contracts/periphery/receiver-refund-collector.mdx @@ -12,18 +12,18 @@ icon: "arrow-rotate-left" ## Features -- **Refunds (after capture)** - Pulls funds from the receiver's wallet after funds have already been released -- **Receiver approval required** - The receiver must have approved the collector contract or provided a signature -- **Operator integration** - Called by `operator.refund()` via the token collector interface +- **Refunds (after capture)** - Pulls funds from the receiver's wallet after the escrow has already released them +- **Receiver approval required** - The receiver must approve the collector contract or supply a signature +- **Operator integration** - `operator.refund()` invokes the collector through the token collector interface -## How It Works +## How it works -After funds have been released to the receiver (state: `Captured`), refunds require pulling tokens back from the receiver's wallet. The `ReceiverRefundCollector` handles this by: +Once the escrow has released funds to the receiver (state: `Captured`), refunding the payer requires pulling tokens back out of the receiver's wallet. The `ReceiverRefundCollector` handles that flow: 1. Operator calls `refund(paymentInfo, amount, receiverRefundCollector, collectorData)` 2. The collector transfers tokens from the receiver to the escrow contract 3. The escrow contract returns tokens to the payer -The receiver must have pre-approved the `ReceiverRefundCollector` for token transfers, or the `collectorData` must contain a valid receiver signature authorizing the refund. +The receiver must pre-approve the `ReceiverRefundCollector` for token transfers, or `collectorData` must carry a valid receiver signature authorizing the refund. diff --git a/contracts/periphery/refund-request-evidence.mdx b/contracts/periphery/refund-request-evidence.mdx index ba70164..ab92352 100644 --- a/contracts/periphery/refund-request-evidence.mdx +++ b/contracts/periphery/refund-request-evidence.mdx @@ -13,12 +13,12 @@ icon: "file-lines" ## Features - **IPFS CID storage** - Stores content hashes on-chain for evidence trails -- **EIP-712 signature approval** - Arbiter can approve refunds via off-chain signatures (gas-free for arbiters) -- **Evidence indexing** - Evidence is indexed by payment and submitting party +- **EIP-712 signature approval** - Arbiter approves refunds with off-chain signatures (gas-free for arbiters) +- **Evidence indexing** - The contract indexes evidence by payment and submitting party - **Multi-party submission** - Both payer and receiver can submit evidence -## How It Works +## How it works -When a refund is disputed, parties submit evidence (documents, screenshots, logs) to IPFS and record the CID on-chain. The arbiter reviews evidence off-chain and submits an EIP-712 approval signature that anyone can relay. +When a payer or receiver disputes a refund, each side submits evidence (documents, screenshots, logs) to IPFS and records the CID on-chain. The arbiter reviews evidence off-chain and produces an EIP-712 approval signature that anyone can relay. -This keeps dispute resolution costs low, the arbiter never needs to submit an on-chain transaction. +This keeps dispute resolution costs low, since the arbiter never has to submit an on-chain transaction. diff --git a/index.mdx b/index.mdx index 7d9f63b..72f9d97 100644 --- a/index.mdx +++ b/index.mdx @@ -6,15 +6,15 @@ icon: "house" **x402r** is a refundable payments protocol extension for [x402](https://www.x402.org/). It enables secure, reversible transactions with built-in buyer protection through smart contract escrow on Base. -## Why x402r? +## Why x402r Standard x402 payments are immediate and irreversible. x402r adds: -- **Escrow deposits**: Funds held in smart contracts until conditions are met -- **Refund windows**: Configurable time periods for buyers to request refunds -- **Dispute resolution**: Arbiter system for handling contested transactions +- **Escrow deposits**: smart contracts hold funds until conditions clear +- **Refund windows**: configurable time periods for buyers to request refunds +- **Dispute resolution**: arbiter system for handling contested transactions -## How It Works +## How it works ```mermaid sequenceDiagram @@ -38,7 +38,7 @@ sequenceDiagram end ``` -## Who Is This For? +## Who this is for @@ -46,7 +46,7 @@ sequenceDiagram -## Get Started +## Get started @@ -77,9 +77,9 @@ x402r consists of these core components: All protocol contracts use universal CREATE2 addresses, same address on every supported chain. -## Supported Networks +## Supported networks -Today, the supported chains in `@x402r/core` are **Base** and **Base Sepolia**. Additional EVMs will be added as canonical `base/commerce-payments@v1.0.0` coverage extends. +Today, the supported chains in `@x402r/core` are **Base** and **Base Sepolia**. More EVMs land as canonical `base/commerce-payments@v1.0.0` coverage extends. | Network | Chain ID | Status | |---|---|---| diff --git a/roadmap.mdx b/roadmap.mdx index 99c00ea..dfa84b6 100644 --- a/roadmap.mdx +++ b/roadmap.mdx @@ -28,7 +28,7 @@ icon: "map" - Bond-based disputes - Multi-level verification (L1 schema/auto, L2 AI review, L3 human arbitration) with per-tier `MaxAmountCondition` exposure caps -- Multiple arbiters per operator and arbiter marketplace +- Many arbiters per operator and an arbiter marketplace - After-capture arbitration handling - Reputation system for clients, merchants, and arbiters (via ERC-8004) - Token wrapper for enforced refund protection @@ -37,7 +37,7 @@ icon: "map" ## Solana Pilot (Alpha) -Solana support is **alpha/experimental**. The Anchor programs and TypeScript mechanism package are implemented, but the pilot is **unaudited** and not yet deployed to mainnet-beta. Use at your own risk; flag any production use to the team first. +Solana support is **alpha/experimental**. The Anchor programs and TypeScript mechanism package exist, but no audit has run, and the pilot has not yet shipped to mainnet-beta. Use at your own risk; flag any production use to the team first. ### Shipped @@ -94,11 +94,11 @@ Other shipped capabilities: ## Contract Status -All EVM contracts use **universal CREATE2 addresses**: when a chain is supported, the address is the same as on every other supported chain. The canonical commerce-payments primitives come from [`base/commerce-payments@v1.0.0`](https://github.com/base/commerce-payments/releases/tag/v1.0.0). +All EVM contracts use **universal CREATE2 addresses**: when x402r supports a chain, the address matches every other supported chain. The canonical commerce-payments primitives come from [`base/commerce-payments@v1.0.0`](https://github.com/base/commerce-payments/releases/tag/v1.0.0). ### Supported chains -Today: **Base mainnet** and **Base Sepolia**. Additional EVMs will be added as canonical `commerce-payments@v1.0.0` coverage from Base extends. +Today: **Base mainnet** and **Base Sepolia**. More EVMs land as canonical `commerce-payments@v1.0.0` coverage from Base extends. Solana support is alpha and not yet on mainnet-beta. See [Solana Pilot (Alpha)](#solana-pilot-alpha). diff --git a/sdk/cli.mdx b/sdk/cli.mdx index 3d2b96f..ad9d522 100644 --- a/sdk/cli.mdx +++ b/sdk/cli.mdx @@ -4,7 +4,7 @@ description: "One-shot command-line tool for paying x402 endpoints. Wallet-agnos icon: "terminal" --- -`@x402r/cli` makes a single x402 payment from the command line. You point it at a URL, provide a signer, and get back the response body plus a settlement transaction hash. +`@x402r/cli` makes a single x402 payment from the command line. You point it at an address, provide a signer, and get back the response body plus a settlement transaction hash. The CLI carries zero provider SDK dependencies. Raw private keys, JSON-RPC signers (Privy, Turnkey, Fireblocks, Safe), and custom signer modules all work through the same interface. @@ -22,7 +22,7 @@ bunx @x402r/cli pay [options] ``` -No project install required. Pin the version (e.g. `@x402r/cli@0.2.0`) for reproducible agent workflows. +No project install required. Pin the version (for example, `@x402r/cli@0.2.0`) for reproducible scripted workflows. ### Usage @@ -30,32 +30,32 @@ No project install required. Pin the version (e.g. `@x402r/cli@0.2.0`) for repro x402r pay [signer flags] [--chain ] [--rpc ] [--max-amount N] [--json] ``` -If the URL does not return HTTP 402, the CLI short-circuits and prints the response body with exit code 0. No payment is made. +If the endpoint does not return HTTP 402, the CLI short-circuits and prints the response body with exit code 0. The CLI sends no payment. ### Signer configuration -Exactly one signer source must be configured. CLI flags take precedence over environment variables. If zero or multiple sources are detected, the CLI exits with code 6. +Configure exactly one signer source. CLI flags take precedence over environment variables. If the CLI finds zero or more than one source, it exits with code 6. -| Source | Flag | Env var | +| Source | Flag | Environment variable | |--------|------|---------| | Raw private key | `--key 0x...` | `PRIVATE_KEY` | | Remote JSON-RPC | `--signer-url ` and `--signer-address 0x...` | `SIGNER_URL` and `SIGNER_ADDRESS` | | Custom module | `--signer-module ` | `SIGNER_MODULE` | -Environment variable names are unprefixed to match Foundry, Hardhat, and x402-reference conventions. +Environment variable names use no `X402R_` prefix to match Foundry, Hardhat, and x402-reference conventions. ### Request options | Flag | Description | |------|-------------| -| `--chain ` | Select a specific `accepts[]` entry when the merchant offers multiple chains. Required when there are multiple options. | -| `--rpc ` | Override the RPC URL for on-chain reads. Required for chain IDs not in `viem/chains`. | +| `--chain ` | Select a specific `accepts[]` entry when the merchant offers more than one chain. Required when more than one option exists. | +| `--rpc ` | Override the RPC endpoint for on-chain reads. Required for chain IDs not in `viem/chains`. | | `--max-amount ` | Refuse to pay more than `n` atomic token units. Exits with code 3 if the price exceeds this. | | `--json` | Emit a single JSON envelope to stdout instead of plain text. | ### Supported chains -The CLI auto-detects the chain from the 402 response's `accepts[].network` field. Any EVM chain known to `viem/chains` is supported (Base, Base Sepolia, Ethereum, Arbitrum, Optimism, and others). For unknown chain IDs, pass `--rpc ` to provide an RPC endpoint. +The CLI reads the chain from the 402 response's `accepts[].network` field. Any EVM chain known to `viem/chains` works, including Base and Base Sepolia. For chain IDs `viem/chains` does not recognize, pass `--rpc ` with an RPC endpoint. ### Exit codes @@ -67,7 +67,7 @@ The CLI auto-detects the chain from the 402 response's `accepts[].network` field | 3 | Price exceeds `--max-amount` | | 4 | Signature rejected | | 5 | Settlement failed (merchant error after payment, or facilitator error) | -| 6 | Signer resolution failed (none, multiple, or partially configured) | +| 6 | Signer resolution failed (none, more than one, or incomplete configuration) | ### Examples @@ -84,7 +84,7 @@ Any endpoint that speaks `eth_signTypedData_v4` works: Privy wallet RPC, Turnkey ```bash npx @x402r/cli pay https://api.example.com/paid-endpoint \ --signer-url https://signer.example/rpc \ - --signer-address 0xYourAddress... + --signer-address 0x586486394C38A2a7d36B16a3FDaF366cd202d823 ``` #### Custom module (Privy) @@ -145,11 +145,11 @@ With `--json`, the CLI writes a single JSON envelope to stdout: } ``` -The `signer` field is omitted when the URL returned a non-402 response (no payment was made). +The CLI drops the `signer` field when the endpoint returned a non-402 response (the CLI sent no payment). ### Signer module contract -A custom signer module must default-export a factory function with the signature `() => Promise`. The returned object must be a viem `Account` with at least `address` and `signTypedData`. The CLI only needs typed-data signatures, transaction broadcasting is handled by the facilitator. +A custom signer module must default-export a factory function with the signature `() => Promise`. The returned object must be a viem `Account` with at least `address` and `signTypedData`. The CLI only needs typed-data signatures; the facilitator broadcasts the transaction. ### Programmatic usage @@ -179,7 +179,7 @@ The `@x402r/cli` package exports: | Export | Type | Description | |--------|------|-------------| -| `pay` | function | Execute a one-shot payment against a URL | +| `pay` | function | Execute a one-shot payment against an endpoint | | `resolveSigner` | function | Resolve a signer from flags and environment variables | | `CliError` | class | Base error class with typed exit codes | | `NetworkError` | class | Exit code 1 | diff --git a/sdk/create-client.mdx b/sdk/create-client.mdx index 9aaab42..0a9b979 100644 --- a/sdk/create-client.mdx +++ b/sdk/create-client.mdx @@ -51,19 +51,19 @@ Type narrowing is a DX convenience, not a security boundary. On-chain [condition | `publicClient` | `PublicClient` | Yes | viem public client for reads | | `walletClient` | `WalletClient` | No | Required for writes. Role presets throw without it. | | `operatorAddress` | `Address` | Yes | Your deployed PaymentOperator | -| `chainId` | `number` | No | Auto-detected from `publicClient.chain` | -| `network` | `string` | No | EIP-155 network ID (e.g., `'eip155:84532'`). Alternative to `chainId`. | +| `chainId` | `number` | No | Resolves from `publicClient.chain` when omitted | +| `network` | `string` | No | EIP-155 network ID (for example, `'eip155:84532'`). Alternative to `chainId`. | | `escrowPeriodAddress` | `Address` | No | Activates `escrow` group | | `refundRequestAddress` | `Address` | No | Activates `refund` group | | `refundRequestEvidenceAddress` | `Address` | No | Activates `evidence` group (requires `refundRequestAddress`) | | `freezeAddress` | `Address` | No | Activates `freeze` group | | `paymentIndexRecorderHookAddress` | `Address` | No | Activates `query` group | -| `paymentStore` | `PaymentStore` | No | Pluggable storage for payment lookups | +| `paymentStore` | `PaymentStore` | No | Custom storage layer for payment lookups | | `eventFromBlock` | `bigint` | No | Starting block for event-based payment lookups | ### Action Groups -| Group | Methods | Requirement | +| Group | Methods | Required config | |-------|---------|-------------| | `payment` | 9 | Always available | | `operator` | 8 | Always available | @@ -157,7 +157,7 @@ interface CheckAgentResult { } ``` -When `reviewers` is omitted or empty, `reputation` is `null` and only on-chain verification runs. +If you omit `reviewers` or pass an empty array, `reputation` is `null` and only on-chain verification runs. For standalone helpers that extract identity data from x402 extension responses without a client instance, use the `extractArbiterIdentity`, `extractReputationRegistrations`, and `fetchArbiterIdentity` exports from `@x402r/sdk`. diff --git a/sdk/delivery-arbiter.mdx b/sdk/delivery-arbiter.mdx index bce6279..58af2a4 100644 --- a/sdk/delivery-arbiter.mdx +++ b/sdk/delivery-arbiter.mdx @@ -11,7 +11,7 @@ icon: "shield-check" * Operator and escrow addresses from the [Merchant Setup](/sdk/delivery-merchant) -There is a full [AI garbage detector example](https://github.com/BackTrackCo/arbiter-examples) that implements this pattern with heuristic + LLM evaluation. +The [AI garbage detector example](https://github.com/BackTrackCo/arbiter-examples) implements this pattern end-to-end with heuristic plus LLM evaluation. @@ -92,10 +92,10 @@ There is a full [AI garbage detector example](https://github.com/BackTrackCo/arb - The `evaluate()` function is where your logic lives. It could be: + The `evaluate()` function is where your logic lives. It can run: - **Heuristic checks**: HTTP status code, response size, content-type validation - - **AI evaluation**: send response body to an LLM and ask "is this a valid response?" + - **AI judgment**: send response body to an LLM and ask "is this a valid response?" - **Schema validation**: check if the response matches an expected JSON schema ```typescript diff --git a/sdk/delivery-merchant.mdx b/sdk/delivery-merchant.mdx index 74410f4..543d292 100644 --- a/sdk/delivery-merchant.mdx +++ b/sdk/delivery-merchant.mdx @@ -7,7 +7,7 @@ icon: "store" ### Prerequisites * A deployed delivery protection operator (see [Deploy an Operator](/sdk/deploy-operator#delivery-protection-operator)) -* An arbiter service URL (see [Arbiter Setup](/sdk/delivery-arbiter)) +* An arbiter service endpoint (see [Arbiter Setup](/sdk/delivery-arbiter)) diff --git a/sdk/delivery-protection.mdx b/sdk/delivery-protection.mdx index 7876853..7c15d2d 100644 --- a/sdk/delivery-protection.mdx +++ b/sdk/delivery-protection.mdx @@ -4,9 +4,9 @@ description: "Automated quality verification for every transaction." icon: "shield-check" --- -In the delivery protection model, the arbiter evaluates every transaction automatically. The arbiter or a satisfied payer can capture funds. If the arbiter issues a FAIL verdict, it can trigger an immediate refund without waiting for escrow expiry. If nobody acts, funds auto-refund to the payer after escrow expires. +In the delivery protection model, the arbiter evaluates every transaction. The arbiter or a satisfied payer can capture funds. If the arbiter issues a FAIL verdict, it can trigger an immediate refund without waiting for escrow expiry. If nobody acts, funds return to the payer once escrow expires. -This is different from the [marketplace model](/sdk/overview) where the merchant releases funds and the arbiter only gets involved when a payer files a dispute. +This differs from the [marketplace model](/sdk/overview) where the merchant releases funds and the arbiter only gets involved when a payer files a dispute. | | Marketplace | Delivery Protection | |---|---|---| @@ -17,7 +17,7 @@ This is different from the [marketplace model](/sdk/overview) where the merchant | Contracts deployed | ~8 (Operator, EscrowPeriod, RefundRequest, Evidence, Freeze, etc.) | 6 (Operator, EscrowPeriod, SAC, 2x OrCondition, HookCombinator) | | Deploy preset | `deployMarketplaceOperator()` | `deployDeliveryProtectionOperator()` | -Use this when every response needs automated quality checks: AI content verification, garbage detection, schema validation. +Use this when every response needs programmatic quality checks: AI content verification, garbage detection, schema validation. diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index f663e0e..d568759 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -15,7 +15,7 @@ The SDK ships two deployment presets. Pick the one that matches your use case: | `deployMarketplaceOperator` | General marketplace with dispute resolution | Yes (optional) | Yes (optional) | Yes | | `deployDeliveryProtectionOperator` | Garbage detection / delivery verification | No | No | No | -All contracts are deployed via factories using CREATE2, so identical configurations produce identical addresses across deployments. +All contracts ship via CREATE2 factories, so identical configurations produce identical addresses across deployments. ## Marketplace operator @@ -24,7 +24,7 @@ A complete marketplace operator deployment includes: 1. **EscrowPeriod**: Records authorization time, enforces waiting period before capture 2. **Freeze**: Allows payer to freeze payment during escrow, receiver to unfreeze 3. **ReceiverCondition**: Gates voids to the merchant (receiver) -4. **RefundRequest (IHook)**: Wired as `voidPostActionHook`, auto-approves pending refund requests during `voidPayment()` +4. **RefundRequest (`IHook`)**: Wired as `voidPostActionHook`, flips pending refund requests to `Approved` during `voidPayment()` 5. **StaticFeeCalculator**: Optional operator fee (basis points) 6. **PaymentOperator**: The main contract tying everything together @@ -32,9 +32,9 @@ A complete marketplace operator deployment includes: **Prerequisites:** - Node.js 20+, pnpm 9.15+ -- A private key with Base Sepolia ETH ([get testnet ETH](https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet)) +- A private key with Base Sepolia ETH ([get Sepolia ETH](https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet)) -Call `deployMarketplaceOperator` from `@x402r/core` with a viem wallet client. Because every contract uses CREATE2, deploys are idempotent: re-running with the same parameters detects existing contracts and skips them. +Call `deployMarketplaceOperator` from `@x402r/core` with a viem wallet client. Because every contract uses CREATE2, deploys are idempotent: re-running with the same parameters reuses any existing contract at the predicted address and skips it. ```typescript import { createPublicClient, createWalletClient, http } from 'viem'; @@ -78,10 +78,10 @@ console.log('Existing (reused):', result.summary.existingCount); | Option | Type | Description | |--------|------|-------------| -| `chainId` | `number` | Target chain ID (e.g., `84532` for Base Sepolia) | +| `chainId` | `number` | Target chain ID (for example, `84532` for Base Sepolia) | | `feeReceiver` | `Address` | Address that receives operator fees | | `arbiter` | `Address` | Arbiter address for dispute resolution | -| `escrowPeriodSeconds` | `bigint` | Escrow waiting period (e.g., `604800n` for 7 days) | +| `escrowPeriodSeconds` | `bigint` | Escrow waiting period (for example, `604800n` for 7 days) | | `freezeDurationSeconds` | `bigint` | How long freezes last. Default: `0n` (permanent until unfrozen) | | `operatorFeeBps` | `bigint` | Fee in basis points. Default: `0n` (no fee). `100n` = 1% | | `authorizedCodehash` | `Hex` | Optional. Restricts which contract codehashes can record. Defaults to `bytes32(0)` (no restriction) | @@ -108,7 +108,7 @@ interface MarketplaceOperatorDeployment { ``` -Because all contracts use CREATE2, redeploying with the same parameters is idempotent: existing contracts are detected and skipped. The `summary` tells you what was new vs reused. +Because all contracts use CREATE2, redeploying with the same parameters is idempotent. The tooling skips any contract that already exists at the predicted address. The `summary` tells you what was new vs reused. ## Preview addresses (no deploy) @@ -118,7 +118,7 @@ import { previewMarketplaceOperator } from '@x402r/core' const preview = await previewMarketplaceOperator(publicClient, { chainId: 84532, - feeReceiver: '0xYourAddress...', + feeReceiver: account.address, arbiter: '0xArbiterAddress...', escrowPeriodSeconds: 604800n, }) @@ -147,22 +147,22 @@ The deployed marketplace operator has the following slot configuration: ## Network support -Today, deployment is supported on the chains in `@x402r/core`'s `x402rChains`: +Today, the deploy presets target the chains in `@x402r/core`'s `x402rChains`: | Network | Chain ID | EIP-155 ID | |---|---|---| | Base | 8453 | `eip155:8453` | | Base Sepolia | 84532 | `eip155:84532` | -Additional EVMs will be added as canonical `base/commerce-payments@v1.0.0` coverage extends. +More EVM chains land as canonical `base/commerce-payments@v1.0.0` coverage extends. -Deployment requires gas fees. Ensure your wallet has ETH on the target network. On Base Sepolia, you can get testnet ETH from [Base network faucets](https://docs.base.org/base-chain/tools/network-faucets). +Deployment requires gas fees. Ensure your wallet has ETH on the target network. On Base Sepolia, you can fund a wallet from [Base network faucets](https://docs.base.org/base-chain/tools/network-faucets). ## Delivery Protection Operator -For automated quality verification (AI garbage detection, schema validation), use the delivery protection preset. No RefundRequest, Evidence, or Freeze contracts. The arbiter or payer can capture funds, and the arbiter can issue immediate refunds without waiting for escrow expiry. +For programmatic quality verification (AI garbage detection, schema validation), use the delivery protection preset. No RefundRequest, Evidence, or Freeze contracts. The arbiter or payer can capture funds, and the arbiter can issue immediate refunds without waiting for escrow expiry. ```typescript import { createPublicClient, createWalletClient, http } from 'viem'; @@ -205,10 +205,10 @@ console.log('AuthorizeHook:', deployment.authorizeHookAddress) | `chainId` | `number` | Target chain | | `arbiter` | `Address` | Arbiter address for capture and refund decisions | | `feeReceiver` | `Address` | Receives protocol fees | -| `escrowPeriodSeconds` | `bigint` | Verification window before auto-refund | +| `escrowPeriodSeconds` | `bigint` | Verification window before automatic refund | | `authorizedCodehash` | `Hex` | Override the default `hookCombinatorCodehash`. Optional | | `paymentIndexRecorderHookAddress` | `Address` | Override the default PaymentIndexRecorderHook. Pass `zeroAddress` to skip on-chain payment indexing. Optional | -| `allowArbiterRefund` | `boolean` | Allow arbiter to refund immediately during escrow. Default: `false` | +| `allowArbiterRefund` | `boolean` | Lets the arbiter refund immediately during escrow. Default: `false` | @@ -233,7 +233,7 @@ console.log('AuthorizeHook:', deployment.authorizeHookAddress) Deploys 6 contracts by default: EscrowPeriod, StaticAddressCondition(arbiter), OrCondition(release), OrCondition(refund), HookCombinator, and the Operator. If you pass `paymentIndexRecorderHookAddress: zeroAddress`, the HookCombinator is skipped (5 contracts). - Redeploying with the same parameters is idempotent (CREATE2). It detects existing contracts and skips them. + Redeploying with the same parameters is idempotent (CREATE2). The tooling reuses any contract that already exists at the predicted address. diff --git a/sdk/examples.mdx b/sdk/examples.mdx index d50160b..490030f 100644 --- a/sdk/examples.mdx +++ b/sdk/examples.mdx @@ -4,13 +4,13 @@ description: "Runnable examples for every SDK operation." icon: "code" --- -The [x402r-sdk repo](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples) ships runnable example scripts organized by role plus end-to-end scenarios. +The [x402r-sdk repository](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples) ships runnable example scripts organized by role plus end-to-end scenarios. ## Examples - Request a refund, freeze a payment, submit on-chain evidence (a placeholder CID; IPFS pinning is the integrator's responsibility). Three TypeScript scripts. + Request a refund, freeze a payment, submit on-chain evidence (a placeholder CID; the integrator owns IPFS pinning). Three TypeScript scripts. Capture from escrow and charge directly. TypeScript scripts plus README. @@ -29,7 +29,7 @@ The [x402r-sdk repo](https://github.com/BackTrackCo/x402r-sdk/tree/main/examples ## Running examples -All examples run against a local Anvil fork seeded by `shared/anvil-setup.ts`. No mainnet wallet or funding is required. +All examples run against a local Anvil fork seeded by `shared/anvil-setup.ts`. You do not need a mainnet wallet or funding. diff --git a/sdk/helpers/forward-to-arbiter.mdx b/sdk/helpers/forward-to-arbiter.mdx index 93bac9a..b298754 100644 --- a/sdk/helpers/forward-to-arbiter.mdx +++ b/sdk/helpers/forward-to-arbiter.mdx @@ -9,7 +9,7 @@ The `forwardToArbiter()` function creates an `onAfterSettle` hook that forwards - Only fires for successful **`authCapture`** scheme settlements - POSTs `{ responseBody, transaction, paymentPayload }` to `{arbiterUrl}/verify` -- Errors are silently caught so an unavailable arbiter cannot break the payment flow +- The hook catches errors internally so an unreachable arbiter cannot break the payment flow ## Usage @@ -36,7 +36,7 @@ function forwardToArbiter( | Parameter | Type | Description | |---|---|---| -| `arbiterUrl` | `string` | Base URL of your arbiter service (e.g. `http://arbiter:3001`) | +| `arbiterUrl` | `string` | Base endpoint of your arbiter service (for example, `http://arbiter:3001`) | | `options` | `ForwardToArbiterOptions` | Optional configuration (see below) | ### Options @@ -74,7 +74,7 @@ When an `authCapture` settlement succeeds, the hook POSTs the following JSON to } ``` -There is no nested `paymentInfo` in the payload. The arbiter reconstructs `PaymentInfo` from `accepted.extra` + `payload.salt` + the recovered `payer` + the top-level requirements (`payTo`, `asset`, `amount`), the same way the facilitator does at settlement. +The payload does not nest a `paymentInfo` object. The arbiter reconstructs `PaymentInfo` from `accepted.extra` + `payload.salt` + the recovered `payer` + the top-level requirements (`payTo`, `asset`, `amount`), the same way the facilitator does at settlement. The arbiter can also resolve the `PaymentInfo` indirectly: derive `paymentInfoHash` from the wire data, then read the on-chain `PaymentInfo` from the escrow if it stores it, or simply forward the wire payload back into your own facilitator to verify. @@ -82,7 +82,7 @@ The arbiter can also resolve the `PaymentInfo` indirectly: derive `paymentInfoHa ## Error handling -By default, fetch errors are logged with `console.warn`. You can override this with a custom handler: +By default, the hook logs fetch errors with `console.warn`. Override this with a custom handler: ```typescript import { forwardToArbiter } from '@x402r/helpers' @@ -96,11 +96,11 @@ const resourceServer = new x402ResourceServer(facilitatorClient) ) ``` -Errors are wrapped in an `X402rError` with the arbiter URL and request details for easier debugging. +The hook wraps each error in an `X402rError` carrying the arbiter endpoint and request details for easier debugging. ## Skipped scenarios -The hook silently returns without making a request when: +The hook returns without making a request when: - The settlement was not successful (`context.result.success === false`) - The scheme is not `authCapture` diff --git a/sdk/merchant/getting-started.mdx b/sdk/merchant/getting-started.mdx index ac31678..4cae0b6 100644 --- a/sdk/merchant/getting-started.mdx +++ b/sdk/merchant/getting-started.mdx @@ -29,10 +29,11 @@ This guide walks you through setting up an Express server that accepts x402r esc Create a `.env` file in the project root: ```bash - ADDRESS=0xYourMerchantAddress - OPERATOR_ADDRESS=0xYourPaymentOperatorAddress + # Replace ADDRESS with your merchant address. + ADDRESS=0x321651df4593DA57C413579c5b611D1A90168a3A + # Replace OPERATOR_ADDRESS with the operator you deployed. + OPERATOR_ADDRESS=0xa0d4734842df1690a5B33Cb21828c946e39D55a2 FACILITATOR_URL=http://localhost:4022 - ``` @@ -160,9 +161,9 @@ This guide walks you through setting up an Express server that accepts x402r esc ## How it works -- **`extra` config** declares the captureAuthorizer, capture/refund deadlines, fee recipient, and fee bounds. The canonical `AuthCaptureEscrow` and token collector addresses are universal CREATE2 deploys, so they do not need to be repeated per-route. -- **`AuthCaptureServerScheme`** registers the authCapture payment scheme with the x402 resource server so it can validate authCapture-backed payments. -- **`paymentMiddleware`** intercepts requests, checks for a valid payment header, and returns 402 if no payment is provided. +- **`extra` config** declares the captureAuthorizer, capture/refund deadlines, fee recipient, and fee bounds. The canonical `AuthCaptureEscrow` and token collector addresses are universal CREATE2 deploys, so routes do not need to repeat them. +- **`AuthCaptureServerScheme`** registers the authCapture payment scheme with the x402 resource server so it can verify authCapture-backed payments. +- **`paymentMiddleware`** intercepts requests, checks for a valid payment header, and returns 402 when the caller has not provided one. - **`HTTPFacilitatorClient`** connects to the facilitator service that verifies and settles payments on-chain. ## Next Steps diff --git a/sdk/merchant/quickstart.mdx b/sdk/merchant/quickstart.mdx index f29d591..8ada49d 100644 --- a/sdk/merchant/quickstart.mdx +++ b/sdk/merchant/quickstart.mdx @@ -52,7 +52,7 @@ const merchant = createMerchantClient({ ## payment.capture -Transfer escrowed funds to the receiver. Specify a smaller amount than `paymentInfo.maxAmount` for a partial capture; the remainder stays in escrow. +Transfer escrowed funds to the receiver. Specify a smaller amount than `paymentInfo.maxAmount` for a partial capture; the rest stays in escrow. ```typescript const tx = await merchant.payment.capture(paymentInfo, 10_000_000n) @@ -74,7 +74,7 @@ Always query `payment.getAmounts()` first to determine the available capturable ## payment.voidPayment -Return all escrowed funds to the payer before capture. Full-only: `void()` empties the authorization in one transaction. For a partial return, capture the portion you want to keep first, then void the remainder (or let it expire at `captureDeadline`). +Return all escrowed funds to the payer before capture. Full-only: `void()` empties the authorization in one transaction. For a partial return, capture the share you want to keep first, then void the rest (or let it expire at `captureDeadline`). ```typescript const tx = await merchant.payment.voidPayment(paymentInfo) @@ -115,7 +115,7 @@ const tx = await merchant.payment.charge( ## payment.refund -Refund funds that have already been captured. Requires a token collector to pull funds from the merchant's balance. +Refund funds the merchant has already captured. Requires a token collector to pull funds from the merchant's balance. ```typescript const tx = await merchant.payment.refund( @@ -133,12 +133,12 @@ const tx = await merchant.payment.refund( | `paymentInfo` | `PaymentInfo` | Full struct identifying the payment | | `amount` | `bigint` | Atomic units to refund to the payer | | `tokenCollector` | `Address` | Token collector that sources the refund (typically `ReceiverRefundCollector`) | -| `collectorData` | `Hex` | Data passed to the collector (e.g., receiver signature) | +| `collectorData` | `Hex` | Data passed to the collector (for example, the receiver signature) | **Returns** `Promise`, the refund transaction hash. -Refunds after capture require the merchant to have sufficient token balance and an approved allowance on the refund collector. +Refunds after capture require the merchant to hold enough token balance and to grant an allowance on the refund collector. ## payment.getAmounts @@ -159,13 +159,13 @@ const amounts = await merchant.payment.getAmounts(paymentInfo) | Field | Type | Description | |---|---|---| -| `hasCollectedPayment` | `boolean` | Whether the payment is collected on-chain | +| `hasCollectedPayment` | `boolean` | Whether the on-chain escrow holds the payment | | `capturableAmount` | `bigint` | Atomic units still capturable from escrow | | `refundableAmount` | `bigint` | Atomic units still refundable | ## payment.getState -Returns the payment's lifecycle position as a tuple. There is no `PaymentState` enum. +Returns the payment's lifecycle position as a tuple. The SDK exposes no `PaymentState` enum. ```typescript const [hasCollectedPayment, capturableAmount, refundableAmount] = diff --git a/sdk/merchant/refund-handling.mdx b/sdk/merchant/refund-handling.mdx index 75a214b..2166425 100644 --- a/sdk/merchant/refund-handling.mdx +++ b/sdk/merchant/refund-handling.mdx @@ -4,9 +4,9 @@ description: "Process, approve, deny, and manage refund requests as a merchant" icon: "rotate-left" --- -The merchant client exposes a read-mostly slice of refund actions plus `freeze.isFrozen`. Writes that change refund-request status (`deny`, `refuse`) and writes that lift a freeze (`unfreeze`) live on `createArbiterClient` or on the full `createX402r()` client. +The merchant client exposes a read-heavy slice of refund actions plus `freeze.isFrozen`. Writes that change refund-request status (`deny`, `refuse`) and writes that lift a freeze (`unfreeze`) live on `createArbiterClient` or on the full `createX402r()` client. -Use `createMerchantClient` for queries below; for executing a refund, see [Capture vs refund decision flow](/sdk/merchant/quickstart#capture-vs-refund-decision-flow). The merchant client's `payment.voidPayment()` auto-flips the request to `Approved` through the `VOID_POST_ACTION_HOOK`. +Use `createMerchantClient` for queries below; for executing a refund, see [Capture vs refund decision flow](/sdk/merchant/quickstart#capture-vs-refund-decision-flow). The merchant client's `payment.voidPayment()` flips the request to `Approved` through the `VOID_POST_ACTION_HOOK`. ## Refund request queries @@ -118,11 +118,11 @@ for (const hash of keys) { ## Refund request actions -Approving or denying a request through the operator hook is what the merchant does. Terminal `deny` / `refuse` calls on the RefundRequest contract are scoped to the arbiter role; from a merchant, execute the refund through `payment.voidPayment()` (auto-approves) or signal a refusal off-chain and let the arbiter terminalize. +Approving or denying a request through the operator hook is what the merchant does. Terminal `deny` and `refuse` calls on the RefundRequest contract belong to the arbiter role; from a merchant, execute the refund through `payment.voidPayment()` (which flips the request to `Approved`) or signal a refusal off-chain and let the arbiter terminalize it. ### payment.voidPayment -To approve and execute a refund, call `payment.voidPayment()`. The operator's `VOID_POST_ACTION_HOOK` (RefundRequest) auto-flips the request status to `Approved`. +To approve and execute a refund, call `payment.voidPayment()`. The operator's `VOID_POST_ACTION_HOOK` (RefundRequest) flips the request status to `Approved`. ```typescript const tx = await merchant.payment.voidPayment(paymentInfo) @@ -138,14 +138,14 @@ const tx = await merchant.payment.voidPayment(paymentInfo) **Returns** `Promise`. -`voidPayment()` auto-approves the pending RefundRequest. There is no undo. +`voidPayment()` flips the pending RefundRequest to `Approved`. This action cannot be undone. ## Freeze management ### freeze.isFrozen -Check whether a payment is frozen. Frozen payments cannot be captured until unfrozen. +Check whether a freeze currently holds a payment. The escrow blocks capture on a frozen payment until the arbiter unfreezes it. ```typescript const frozen = await merchant.freeze?.isFrozen(paymentInfo) @@ -165,7 +165,7 @@ The merchant client exposes `freeze.isFrozen` only. Lifting a freeze (`unfreeze` ## Complete refund workflow -Here is a full workflow showing how to detect a refund request, review it, make a decision, and execute the refund if approved. +A full workflow that detects a refund request, reviews it, makes a decision, and executes the refund when approved. ```typescript import { createMerchantClient, RefundRequestStatus } from '@x402r/sdk' @@ -207,7 +207,7 @@ async function handleRefundWorkflow( const shouldApprove = request.amount <= amounts.refundableAmount if (shouldApprove) { - // Execute the refund (auto-approves the request via VOID_POST_ACTION_HOOK) + // Execute the refund (the VOID_POST_ACTION_HOOK flips the request to Approved) const tx = await merchant.payment.voidPayment(paymentInfo) console.log('Refund executed:', tx) } else { @@ -259,7 +259,7 @@ sequenceDiagram | `refund.getCancelCount` | `paymentInfo` | `bigint` (number of cancellations on this RefundRequest) | | `refund.getCancelledAmount` | `paymentInfo, cancelIndex` | `bigint` (amount cancelled at the given index) | | `freeze.isFrozen` | `paymentInfo` | `boolean` | -| `payment.voidPayment` | `paymentInfo, data?` | `Hash` (auto-approves the pending RefundRequest) | +| `payment.voidPayment` | `paymentInfo, data?` | `Hash` (flips the pending RefundRequest to `Approved`) | ## Next steps diff --git a/sdk/overview.mdx b/sdk/overview.mdx index cc769bb..af3e984 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -16,9 +16,9 @@ Three roles interact with the protocol: ### Two Operator Models -**Marketplace** (`deployMarketplaceOperator`): The merchant releases funds after escrow. If the payer contests, they file a refund request and an arbiter resolves it. Use this for general commerce where most transactions are uncontested. +**Marketplace** (`deployMarketplaceOperator`): The merchant releases funds after escrow. If the payer contests, they file a refund request and an arbiter resolves it. Use this for general commerce where most transactions clear without dispute. -**Delivery Protection** (`deployDeliveryProtectionOperator`): The arbiter evaluates every transaction automatically. The arbiter or a satisfied payer can capture funds. On a FAIL verdict, the arbiter can trigger an immediate refund. If nobody acts, funds auto-refund after escrow. Use this for AI content verification, schema validation, or automated quality checks. +**Delivery Protection** (`deployDeliveryProtectionOperator`): The arbiter evaluates every transaction. The arbiter or a satisfied payer can capture funds. On a FAIL verdict, the arbiter can trigger an immediate refund. If nobody acts, funds return to the payer once escrow expires. Use this for AI content verification, schema validation, or quality checks. ### Packages @@ -34,7 +34,7 @@ bun add @x402r/sdk ``` -`@x402r/sdk` is the only package most developers need. It includes role-scoped client factories, 8 action groups (payment, escrow, refund, evidence, freeze, query, operator, watch), an `.extend()` plugin system, and ERC-8004 helpers for extracting agent identity and reputation data from x402 extension responses. +`@x402r/sdk` is the only package most developers need. It includes role-scoped client factories, 8 action groups (payment, escrow, refund, evidence, freeze, query, operator, watch), an `.extend()` plugin system, and ERC-8004 helpers that extract on-chain identity and reputation data from x402 extension responses. For low-level access to contract ABIs and deploy utilities: @@ -94,9 +94,9 @@ bunx @x402r/cli pay [options] ## Network support -All x402r-authored contracts use universal CREATE2 addresses: when a chain is supported, the address is the same as on every other supported chain. +All x402r-authored contracts use universal CREATE2 addresses: every supported chain resolves to the same address as every other supported chain. -Today, the supported chains in `@x402r/core` are **Base** and **Base Sepolia**. Additional EVMs will be added as canonical `base/commerce-payments@v1.0.0` coverage extends. +Today, the supported chains in `@x402r/core` are **Base** and **Base Sepolia**. More EVM chains land as canonical `base/commerce-payments@v1.0.0` coverage extends. | Chain | Chain ID | USDC | |---|---|---| @@ -105,7 +105,7 @@ Today, the supported chains in `@x402r/core` are **Base** and **Base Sepolia**. ### commerce-payments v1 primitives -The `base/commerce-payments@v1.0.0` primitives are redeployed at canonical CREATE2 addresses via CreateX permissionless salts. Each contract has the same address on every supported chain. +The `base/commerce-payments@v1.0.0` primitives ship at canonical CREATE2 addresses via CreateX permissionless salts. Each contract resolves to the same address on every supported chain. | Contract | Address | |----------|---------| @@ -126,4 +126,4 @@ authCaptureEscrow; tokenCollector; ``` -The primitives are exposed to SDK consumers on the chains listed in `@x402r/core`'s `x402rChains` (Base + Base Sepolia today). The CREATE2 addresses themselves have been prepositioned via CreateX on additional chains and will be enabled in the registry as canonical `base/commerce-payments@v1.0.0` coverage extends. +The SDK exposes these primitives on the chains listed in `@x402r/core`'s `x402rChains` (Base + Base Sepolia today). CreateX salts already reserve the CREATE2 addresses on more chains, and the registry enables each one as canonical `base/commerce-payments@v1.0.0` coverage extends. diff --git a/x402-integration/auth-capture-scheme.mdx b/x402-integration/auth-capture-scheme.mdx index 15c77f8..aed1348 100644 --- a/x402-integration/auth-capture-scheme.mdx +++ b/x402-integration/auth-capture-scheme.mdx @@ -27,7 +27,7 @@ AUTHORIZE -> RESOURCE DELIVERED -> CAPTURE / VOID -> (REFUND) - Client authorization is submitted, funds locked in escrow via `AuthCaptureEscrow.authorize()`. The token collector executes the client's signature (ERC-3009 `receiveWithAuthorization` or Permit2 `permitTransferFrom`) to pull tokens into escrow. + The facilitator submits the client's authorization, locking funds in escrow via `AuthCaptureEscrow.authorize()`. The token collector executes the client's signature (ERC-3009 `receiveWithAuthorization` or Permit2 `permitTransferFrom`) to pull tokens into escrow. @@ -55,7 +55,7 @@ CHARGE -> RESOURCE DELIVERED -> (REFUND) - Client authorization is submitted, funds sent directly to receiver via `AuthCaptureEscrow.charge()`. No escrow hold. + The facilitator submits the client's authorization, sending funds directly to the receiver via `AuthCaptureEscrow.charge()`. No escrow hold. @@ -134,7 +134,7 @@ sequenceDiagram ## CaptureAuthorizer -The **captureAuthorizer** is the address authorized to authorize/capture/void/refund/charge a payment. The escrow contract gates those operations on `msg.sender`. 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 **captureAuthorizer** is the address that may call `authorize`, `capture`, `void`, `refund`, or `charge` on a payment. The escrow contract gates those operations on `msg.sender`. In x402's facilitator-submits flow that means either the facilitator's EOA, or any smart contract that ends up calling the escrow (for example, an arbiter contract with dispute logic or a multisig). | Use Case | CaptureAuthorizer | |---|---| @@ -176,7 +176,7 @@ Server sends this to request payment: } ``` -A server MAY list multiple `accepts[]` entries with different `assetTransferMethod` values so clients can pick the method matching their token approvals. +A server MAY list more than one `accepts[]` entry with different `assetTransferMethod` values so clients can pick the method matching their token approvals. ### PaymentPayload: EIP-3009 (default) @@ -239,7 +239,7 @@ When `extra.assetTransferMethod === "permit2"`, the client signs a Permit2 `Perm } ``` -The merchant address is bound through the deterministic nonce (no witness struct). +The deterministic nonce binds the merchant address (no witness struct). ## Field Reference @@ -247,14 +247,14 @@ The merchant address is bound through the deterministic nonce (no witness struct | Field | Type | Description | |---|---|---| -| `name` | string | EIP-712 token-domain name (e.g., `"USDC"`). Used for ERC-3009 signing only. | -| `version` | string | EIP-712 token-domain version (e.g., `"2"`). | -| `captureAuthorizer` | address | Address authorized to authorize/capture/void/refund/charge. Committed on-chain as `PaymentInfo.operator`. | +| `name` | string | EIP-712 token-domain name (for example, `"USDC"`). Used for ERC-3009 signing only. | +| `version` | string | EIP-712 token-domain version (for example, `"2"`). | +| `captureAuthorizer` | address | Address that may call `authorize`, `capture`, `void`, `refund`, or `charge`. Committed on-chain as `PaymentInfo.operator`. | | `captureDeadline` | uint48 | Absolute Unix seconds: capture must occur before this. Encoded as `authorizationExpiry`. | | `refundDeadline` | uint48 | Absolute Unix seconds: refunds allowed until this. Encoded as `refundExpiry`. | | `feeRecipient` | address | Fee recipient. Set to `address(0)` to let the captureAuthorizer specify any non-zero recipient at capture/charge time. | -| `minFeeBps` | uint16 | Minimum fee in basis points the captureAuthorizer must take. `0` = no minimum. | -| `maxFeeBps` | uint16 | Maximum fee in basis points the captureAuthorizer can take. | +| `minFeeBps` | uint16 | Lowest fee in basis points the captureAuthorizer must take. `0` = no floor. | +| `maxFeeBps` | uint16 | Highest fee in basis points the captureAuthorizer can take. | ### Optional Extra Fields @@ -264,19 +264,19 @@ The merchant address is bound through the deterministic nonce (no witness struct | `assetTransferMethod` | `"eip3009"` \| `"permit2"` | Which token collector to use. | `"eip3009"` | -**Fee Configuration:** Fees are enforced on-chain in the `PaymentInfo` struct. The escrow rejects captures/charges that fall outside `[minFeeBps, maxFeeBps]`. If `feeRecipient` is non-zero, the actual fee recipient at capture/charge must match. +**Fee Configuration:** The escrow enforces fees on-chain via the `PaymentInfo` struct. The escrow rejects captures/charges that fall outside `[minFeeBps, maxFeeBps]`. If `feeRecipient` is non-zero, the actual fee recipient at capture/charge must match. ## Nonce Derivation -The signature nonce is the payer-agnostic `PaymentInfo` hash. Payer is zeroed; everything else is the values that will appear on-chain. +The signature nonce is the payer-agnostic `PaymentInfo` hash. The encoding zeros out the payer; every other field carries the value 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. +The `salt` field enforces freshness: each signing call generates a fresh `bytes32` salt, so two payers signing concurrently produce distinct nonces with no collision risk. ## Verification Logic @@ -312,7 +312,7 @@ For smart wallet clients, the signature may be EIP-6492 wrapped (containing depl ## PaymentInfo Struct -This is the on-chain Solidity struct. The `payer` field is not included in the JSON payload, it is derived from the signature recovery at settlement time. Wire-format `extra` uses spec-level field names; the on-chain struct keeps canonical names so the EIP-712 typehash matches the AuthCaptureEscrow contract byte-for-byte. +This is the on-chain Solidity struct. The JSON payload omits the `payer` field; the facilitator recovers it from the signature at settlement time. Wire-format `extra` uses spec-level field names; the on-chain struct keeps canonical names so the EIP-712 typehash matches the AuthCaptureEscrow contract byte-for-byte. ```solidity struct PaymentInfo { @@ -347,7 +347,7 @@ The escrow contract enforces invariants on-chain: - Settlement amount is capped by client-signed `maxAmount`. Attempting to exceed the limit reverts. + The client-signed `maxAmount` caps the settlement amount. Attempts to exceed the limit revert. @@ -364,7 +364,7 @@ The escrow contract enforces invariants on-chain: -**CaptureAuthorizer Trust Required:** The captureAuthorizer controls when and how much to capture. Choose carefully and understand the capture policy. See [PaymentOperator](/contracts/payment-operator) for examples. +**CaptureAuthorizer Trust Required:** The captureAuthorizer controls when and how much to capture. Choose with intent and understand the capture policy. See [PaymentOperator](/contracts/payment-operator) for examples. ## Error Codes @@ -407,7 +407,7 @@ The `authCapture` scheme adds an authorization step before settlement (or refund - Understand why escrow is needed for HTTP payments. + Understand why HTTP payments need escrow. diff --git a/x402-integration/overview.mdx b/x402-integration/overview.mdx index c256689..2abe94a 100644 --- a/x402-integration/overview.mdx +++ b/x402-integration/overview.mdx @@ -12,23 +12,23 @@ Think of it as "Stripe for the programmable internet" - agents, robots, and auto ## Payment Schemes -X402 v2 supports multiple payment schemes: +X402 v2 supports two payment schemes: - Immediate settlement - payment happens instantly when request is made. + Immediate settlement - payment clears the moment the client sends the request. **Best for:** Simple purchases, low-value transactions, trusted services - Deferred settlement - funds locked until conditions are met. + Deferred settlement - funds stay locked until conditions clear. **Best for:** High-value transactions, usage-based billing, long-running tasks -## Why Escrow? +## Why Escrow The `exact` scheme works well for immediate-delivery payments, but creates friction for: @@ -39,7 +39,7 @@ The `exact` scheme works well for immediate-delivery payments, but creates frict Client pays $500 → Server crashes → Money lost ``` -**Escrow Solution:** Funds held until work is verified +**Escrow Solution:** Escrow holds funds until the captureAuthorizer verifies the work ``` Client authorizes $500 → Work completes → Operator releases → Server receives @@ -53,7 +53,7 @@ Consider an LLM agent making API calls: - Can't pay exact amount in advance - Server needs guarantee of payment -**Escrow Solution:** Authorize max amount, capture actual usage +**Escrow Solution:** Lock a max amount, capture actual usage ``` Client authorizes $10 → Uses $6.50 → Operator captures $6.50 → Refund $3.50 @@ -80,10 +80,10 @@ Agent makes 1,000 API calls at $0.01 each = 1,000 signatures + 1,000 on-chain transactions ``` -**Escrow Solution:** One authorization, multiple captures +**Escrow Solution:** One authorization, many captures ``` -Client authorizes $10 once → Server tracks usage → Periodic batch capture +Client authorizes $10 once → Server tracks usage → Server batches captures on a schedule ``` ## How x402r Extends X402 From 524a0f5d3bad660f62a14aded604ed097122a1d2 Mon Sep 17 00:00:00 2001 From: A1igator Date: Wed, 20 May 2026 23:16:51 -0700 Subject: [PATCH 32/37] docs: address PR #35 re-review (4325579854) + auth-capture rename Round-1 code-accuracy fixes from the fourth-pass audit, plus the spec/code rename from authCapture to auth-capture (kebab-case scheme literal, error codes, package paths). Code accuracy: - AuthCaptureServerScheme -> AuthCaptureEvmScheme with .register(networkId, ...) across merchant/getting-started, helpers/forward-to-arbiter, delivery-merchant, delivery-arbiter. - forwardToArbiter payload shape rewritten to { responseBody, transaction, paymentInfoWire }; arbiter consumer uses PaymentInfo.fromWire(...). - Drop fabricated PaymentState enum from contracts/overview; key-methods block matches real escrow ABI. - IHook.run 4-arg signature across hooks/overview, custom, authorization-time, payment-index, combinator; record() mentions in gas-costs and escrow-period corrected. - ICondition.check 4-arg signature across all 8 conditions pages plus architecture.mdx. - void() (not voidPayment()) in contract-API contexts: architecture.mdx, examples.mdx, gas-costs.mdx. - capture() gains data param in payment-operator. - reclaim() corrected to payer-only in periphery/auth-capture-escrow (security-relevant: was framed as permissionless). - FreezeFactory.deploy(...) 4-arg call site + correct "default-allow" comment in examples.mdx. - factories.mdx salt-key block uses LONG OperatorConfig names with abi.encode (was SHORT names with abi.encodePacked). - Event signatures (AuthorizeExecuted/CaptureExecuted/VoidExecuted/ RefundExecuted/FeesDistributed) corrected in architecture.mdx (no timestamp on Authorize/Capture, no amount on Void, second uint on FeesDistributed is operatorAmount). Other fixes: - sdk/cli adds --asset-transfer-method flag. - factories '0x...' placeholder replaced with the real factory address. - sdk/overview duplicate Merchant card removed. - hooks/payment-index rewritten to actual PaymentIndexRecorderHook surface (drops fabricated 3-arg requestRefund with nonce). Roadmap removed (page deleted, nav group dropped from docs.json). auth-capture rename (matches upstream scheme PR): - Scheme literal "authCapture" -> "auth-capture" across all code blocks and prose mentions of the scheme value. - Import path @x402r/evm/authCapture/server -> @x402r/evm/auth-capture/server. - Error codes invalid_authCapture_extra / invalid_authCapture_signature -> invalid_auth_capture_extra / invalid_auth_capture_signature. - Page title, nav group label, and prose updated to "auth-capture". - PascalCase identifiers (AuthCaptureEvmScheme, AuthCaptureEscrow) and SCREAMING_SNAKE constants preserved per the scheme PR. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/architecture.mdx | 18 +-- contracts/conditions/always-true.mdx | 2 +- contracts/conditions/custom.mdx | 13 +- contracts/conditions/escrow-period.mdx | 5 +- contracts/conditions/freeze.mdx | 3 +- contracts/conditions/overview.mdx | 8 +- contracts/conditions/payer.mdx | 2 +- contracts/conditions/receiver.mdx | 2 +- contracts/conditions/static-address.mdx | 2 +- contracts/examples.mdx | 53 ++++---- contracts/factories.mdx | 24 ++-- contracts/gas-costs.mdx | 6 +- contracts/hooks/authorization-time.mdx | 13 +- contracts/hooks/combinator.mdx | 2 +- contracts/hooks/custom.mdx | 82 +++++++----- contracts/hooks/overview.mdx | 6 +- contracts/hooks/payment-index.mdx | 62 +++++---- contracts/overview.mdx | 12 +- contracts/payment-operator.mdx | 4 +- contracts/periphery/auth-capture-escrow.mdx | 8 +- docs.json | 8 +- roadmap.mdx | 134 -------------------- sdk/cli.mdx | 1 + sdk/delivery-arbiter.mdx | 13 +- sdk/delivery-merchant.mdx | 36 ++++-- sdk/examples.mdx | 2 +- sdk/helpers/forward-to-arbiter.mdx | 53 ++++---- sdk/merchant/getting-started.mdx | 12 +- sdk/overview.mdx | 9 +- x402-integration/auth-capture-scheme.mdx | 32 ++--- x402-integration/overview.mdx | 6 +- 31 files changed, 268 insertions(+), 365 deletions(-) delete mode 100644 roadmap.mdx diff --git a/contracts/architecture.mdx b/contracts/architecture.mdx index 3f3282f..acab0b8 100644 --- a/contracts/architecture.mdx +++ b/contracts/architecture.mdx @@ -124,13 +124,13 @@ Freeze policies are optional and configurable. Define who can freeze, who can un When you invoke an action (for example, `capture()`): 1. **Load Condition** - Get the condition address from operator slot -2. **Check Condition** - Call `condition.check(paymentInfo, amount, caller)` +2. **Check Condition** - Call `condition.check(paymentInfo, amount, caller, data)` - Check if caller matches required role (for example, receiver or arbiter) - Check state (for example, escrow period passed, not frozen) - Check other requirements (for example, time constraints) 3. **Result:** - `true` → Proceed to execute action - - `false` → Revert with `ConditionNotMet` error + - `false` → Revert with `PreActionConditionNotMet` error 4. **Execute Action** - Call escrow method 5. **Call Hook** - Run the matching `*_POST_ACTION_HOOK` after successful execution @@ -224,7 +224,7 @@ Fees accumulate in the operator. Anyone can call `distributeFees(token)` to disb |------|-------------|--------------| | **Payer** | `authorize()`, `freeze()`, `unfreeze()`, `requestRefund()`, `cancelRefundRequest()` | Can only act on own payments | | **Receiver** | `capture()` (if condition allows), `charge()`, `requestRefund()` | Can only act on payments where they are receiver | -| **Designated Address** | Any action per conditions (for example, `voidPayment()`, `capture()`, or `updateStatus()`) | Defined by StaticAddressCondition (arbiter, DAO, or service provider) | +| **Designated Address** | Any action per conditions (for example, `void()`, `capture()`, or `refund()`) | Defined by StaticAddressCondition (arbiter, DAO, or service provider) | | **Protocol Owner** | `queueCalculator()`, `executeCalculator()`, `queueRecipient()`, `executeRecipient()` | 7-day timelock on ProtocolFeeConfig changes | @@ -289,14 +289,14 @@ Ownership transfers use Solady's Ownable pattern: ```solidity // Payment lifecycle (PaymentOperator events) -event AuthorizeExecuted(bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount, uint256 timestamp); -event ChargeExecuted(bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount, uint256 timestamp); -event CaptureExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, uint256 amount, uint256 timestamp); -event VoidExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, address indexed payer, uint256 amount); -event RefundExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, address indexed payer, uint256 amount); +event AuthorizeExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount); +event ChargeExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount); +event CaptureExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount); +event VoidExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver); +event RefundExecuted(AuthCaptureEscrow.PaymentInfo paymentInfo, bytes32 indexed paymentInfoHash, address indexed payer, address indexed receiver, uint256 amount); // Fee distribution -event FeesDistributed(address indexed token, uint256 protocolAmount, uint256 arbiterAmount); +event FeesDistributed(address indexed token, uint256 protocolAmount, uint256 operatorAmount); event OperatorDeployed(address indexed operator, address indexed deployer, address indexed feeReceiver); // Freeze state (Freeze contract events) diff --git a/contracts/conditions/always-true.mdx b/contracts/conditions/always-true.mdx index 77b7aee..a942e04 100644 --- a/contracts/conditions/always-true.mdx +++ b/contracts/conditions/always-true.mdx @@ -15,7 +15,7 @@ AlwaysTrueCondition allows anyone to call the action, no restrictions applied. ## Logic ```solidity -function check(PaymentInfo calldata payment, uint256, address caller) +function check(PaymentInfo calldata payment, uint256, address caller, bytes calldata) external pure returns (bool) { return true; diff --git a/contracts/conditions/custom.mdx b/contracts/conditions/custom.mdx index 5549df6..43cd16b 100644 --- a/contracts/conditions/custom.mdx +++ b/contracts/conditions/custom.mdx @@ -13,11 +13,9 @@ You can create custom conditions for specialized logic beyond what the built-in From the `ICondition.sol` NatSpec: 1. **MUST NOT revert**: return `false` to deny, never `revert` -2. **Should be `view` or `pure`**: no state-changing operations -3. **No external calls**: avoid calling other contracts to prevent reentrancy risks -4. **Return `true` to allow, `false` to deny** +2. **Return `true` to allow, `false` to deny** -The operator converts a `false` return into a `ConditionNotMet` revert. +The operator converts a `false` return into a `PreActionConditionNotMet` revert. Prefer `view` or `pure` implementations to keep call sites cheap and gas predictable. ## Example: TimeOfDayCondition @@ -36,7 +34,8 @@ contract TimeOfDayCondition is ICondition { function check( PaymentInfo calldata payment, uint256, - address caller + address caller, + bytes calldata ) external view returns (bool) { uint256 hour = (block.timestamp / 3600) % 24; return hour >= startHour && hour < endHour; @@ -84,13 +83,13 @@ contract TimeOfDayConditionTest is Test { function test_allowsDuringBusinessHours() public { // Set block.timestamp to 10 AM UTC vm.warp(10 * 3600); - assertTrue(condition.check(paymentInfo, 0, caller)); + assertTrue(condition.check(paymentInfo, 0, caller, "")); } function test_deniesOutsideBusinessHours() public { // Set block.timestamp to 8 PM UTC vm.warp(20 * 3600); - assertFalse(condition.check(paymentInfo, 0, caller)); + assertFalse(condition.check(paymentInfo, 0, caller, "")); } } ``` diff --git a/contracts/conditions/escrow-period.mdx b/contracts/conditions/escrow-period.mdx index fc20354..f5fa15e 100644 --- a/contracts/conditions/escrow-period.mdx +++ b/contracts/conditions/escrow-period.mdx @@ -33,7 +33,8 @@ EscrowPeriod extends [AuthorizationTimeRecorderHook](/contracts/hooks/authorizat function check( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, uint256, - address + address, + bytes calldata ) external view returns (bool allowed) { return !isDuringEscrowPeriod(paymentInfo); } @@ -92,7 +93,7 @@ For freeze functionality, deploy a separate [Freeze](/contracts/conditions/freez ## Gas -**Cost:** ~20k gas per `record()` call (one `SSTORE` for the timestamp). The `check()` call is a `view` function with one `SLOAD`. +**Cost:** ~20k gas per `run()` call (one `SSTORE` for the timestamp). The `check()` call is a `view` function with one `SLOAD`. ## Next Steps diff --git a/contracts/conditions/freeze.mdx b/contracts/conditions/freeze.mdx index a0b6644..b4b879a 100644 --- a/contracts/conditions/freeze.mdx +++ b/contracts/conditions/freeze.mdx @@ -23,7 +23,8 @@ Freeze is a standalone condition that blocks capture on frozen payments. It mana function check( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, uint256, - address + address, + bytes calldata ) external view returns (bool allowed) { return !isFrozen(paymentInfo); } diff --git a/contracts/conditions/overview.mdx b/contracts/conditions/overview.mdx index 092ea0b..3d302d6 100644 --- a/contracts/conditions/overview.mdx +++ b/contracts/conditions/overview.mdx @@ -23,7 +23,8 @@ interface ICondition { function check( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, uint256 amount, - address caller + address caller, + bytes calldata data ) external view returns (bool allowed); } ``` @@ -32,6 +33,7 @@ interface ICondition { - `paymentInfo`, The payment information struct - `amount`, The amount involved in the action (0 for authorization-only checks like refund request status updates) - `caller`, The address attempting the action +- `data`, Arbitrary data forwarded from the caller (signatures, proofs, attestations) **Return:** `true` if the caller can proceed, `false` otherwise. @@ -117,14 +119,14 @@ Prefer stateless conditions when possible: ```solidity // Stateless: No storage reads (pure) -function check(PaymentInfo calldata payment, uint256, address caller) +function check(PaymentInfo calldata payment, uint256, address caller, bytes calldata) external pure returns (bool) { return caller == payment.receiver; // Pure computation } // Stateful: Storage reads cost gas (view) -function check(PaymentInfo calldata payment, uint256, address caller) +function check(PaymentInfo calldata payment, uint256, address caller, bytes calldata) external view returns (bool) { return allowList[caller]; // SLOAD costs gas diff --git a/contracts/conditions/payer.mdx b/contracts/conditions/payer.mdx index 54c9d9f..cbe29b7 100644 --- a/contracts/conditions/payer.mdx +++ b/contracts/conditions/payer.mdx @@ -15,7 +15,7 @@ PayerCondition is a singleton condition that restricts an action to the payment' ## Logic ```solidity -function check(PaymentInfo calldata payment, uint256, address caller) +function check(PaymentInfo calldata payment, uint256, address caller, bytes calldata) external pure returns (bool) { return caller == payment.payer; diff --git a/contracts/conditions/receiver.mdx b/contracts/conditions/receiver.mdx index 2be6ef6..364ae2f 100644 --- a/contracts/conditions/receiver.mdx +++ b/contracts/conditions/receiver.mdx @@ -15,7 +15,7 @@ ReceiverCondition is a singleton condition that restricts an action to the payme ## Logic ```solidity -function check(PaymentInfo calldata payment, uint256, address caller) +function check(PaymentInfo calldata payment, uint256, address caller, bytes calldata) external pure returns (bool) { return caller == payment.receiver; diff --git a/contracts/conditions/static-address.mdx b/contracts/conditions/static-address.mdx index cf7f822..020ba6c 100644 --- a/contracts/conditions/static-address.mdx +++ b/contracts/conditions/static-address.mdx @@ -20,7 +20,7 @@ contract StaticAddressCondition is ICondition { DESIGNATED_ADDRESS = _designatedAddress; } - function check(PaymentInfo calldata payment, uint256, address caller) + function check(PaymentInfo calldata payment, uint256, address caller, bytes calldata) external view returns (bool) { return caller == DESIGNATED_ADDRESS; diff --git a/contracts/examples.mdx b/contracts/examples.mdx index d69db30..74f84ea 100644 --- a/contracts/examples.mdx +++ b/contracts/examples.mdx @@ -22,29 +22,25 @@ The configuration examples below use simplified pseudo-code (for example, `new S ``` - - ```typescript - // Payer can freeze, arbiter can unfreeze (or wait for expiry) - const freezePolicy = await freezePolicyFactory.deploy( - PAYER_CONDITION, // Only payer can freeze - arbiterCondition.address, // Only arbiter can unfreeze - 3 * 24 * 60 * 60 // 3 days (expires automatically) - ); - ``` - - - + ```typescript // 7-day escrow period (combined hook + condition) const escrowPeriod = await escrowPeriodFactory.deploy( 7 * 24 * 60 * 60, // 7 days zeroHash // bytes32(0) = operator-only ); + ``` + - // Freeze condition linked to escrow period + + ```typescript + // Payer can freeze, arbiter can unfreeze (or wait for 3-day expiry), + // linked to the 7-day escrow period. const freeze = await freezeFactory.deploy( - freezePolicy, - escrowPeriod // Link to escrow period for time constraint + PAYER_CONDITION, // freezeCondition: only payer can freeze + arbiterCondition.address, // unfreezeCondition: only arbiter can unfreeze + 3 * 24 * 60 * 60, // freezeDuration: 3 days (auto-expires; 0 = permanent) + escrowPeriod // escrowPeriodContract: restricts freeze() to the escrow window (address(0) = unconstrained) ); ``` @@ -178,21 +174,20 @@ Buyer approves tokens → Seller calls charge() → Funds transferred in one tx // Deploy arbiter condition const arbiterCondition = await new StaticAddressCondition(arbiterAddress); -// Receiver can freeze for 5 days, arbiter can unfreeze -const freezePolicy = await freezePolicyFactory.deploy( - RECEIVER_CONDITION, // Receiver can freeze (product defect) - arbiterCondition.address, // Arbiter can unfreeze (dispute resolution) - 5 * 24 * 60 * 60 // 5 days (auto-expires) -); - // 14-day escrow (shipping + inspection) const escrowPeriod = await escrowPeriodFactory.deploy( 14 * 24 * 60 * 60, // 14 days zeroHash // bytes32(0) = operator-only ); -// Deploy Freeze linked to escrow period -const freeze = await freezeFactory.deploy(freezePolicy, escrowPeriod); +// Receiver freeze (product defect), arbiter unfreeze (dispute resolution), +// linked to the 14-day escrow period. +const freeze = await freezeFactory.deploy( + RECEIVER_CONDITION, // freezeCondition + arbiterCondition.address, // unfreezeCondition + 5 * 24 * 60 * 60, // freezeDuration: 5 days + escrowPeriod // escrowPeriodContract +); // Receiver OR Arbiter can capture (after escrow + not frozen) const capturePreActionCondition = await new AndCondition([ @@ -246,8 +241,8 @@ sequenceDiagram Arbiter->>Arbiter: Investigates Note over Buyer,Escrow: Day 16 - Resolution - Arbiter->>Operator: voidPayment(paymentId) - Operator->>Escrow: void(paymentId) + Arbiter->>Operator: void(paymentInfo, data) + Operator->>Escrow: void(paymentInfo) Escrow->>Buyer: Full refund ``` @@ -432,8 +427,8 @@ sequenceDiagram Note over Buyer,Escrow: Day 5 - Return Request Buyer->>Seller: Requests return Note over Seller: Approves return - Seller->>Operator: voidPayment(paymentId) - Operator->>Escrow: void(paymentId) + Seller->>Operator: void(paymentInfo, data) + Operator->>Escrow: void(paymentInfo) Escrow->>Buyer: Full refund Note over Buyer,Escrow: Buyer returns product ``` @@ -460,7 +455,7 @@ const config = { chargePostActionHook: '0x0000000000000000000000000000000000000000', capturePreActionCondition: providerCondition.address,// Provider releases capturePostActionHook: '0x0000000000000000000000000000000000000000', - voidPreActionCondition: '0x0000000000000000000000000000000000000000', // No refunds (no arbiter) + voidPreActionCondition: '0x0000000000000000000000000000000000000000', // Open-access: anyone can call void() (`address(0)` = default-allow) voidPostActionHook: '0x0000000000000000000000000000000000000000', refundPreActionCondition: '0x0000000000000000000000000000000000000000', refundPostActionHook: '0x0000000000000000000000000000000000000000' diff --git a/contracts/factories.mdx b/contracts/factories.mdx index 1cf82a2..28fd3ce 100644 --- a/contracts/factories.mdx +++ b/contracts/factories.mdx @@ -123,7 +123,7 @@ import { base } from 'viem/chains'; import { privateKeyToAccount } from 'viem/accounts'; import { paymentOperatorAbi } from '@x402r/core'; -const FACTORY_ADDRESS = '0x...'; // Replace with actual factory address +const FACTORY_ADDRESS = '0xa0d4734842df1690a5B33Cb21828c946e39D55a2'; const account = privateKeyToAccount('0x...'); const walletClient = createWalletClient({ @@ -488,19 +488,19 @@ Each factory uses different salt strategies: **PaymentOperatorFactory:** ```solidity -bytes32 key = keccak256(abi.encodePacked( +bytes32 key = keccak256(abi.encode( config.feeReceiver, config.feeCalculator, - config.authorizeCondition, - config.authorizeHook, - config.chargeCondition, - config.chargeHook, - config.captureCondition, - config.captureHook, - config.voidCondition, - config.voidHook, - config.refundCondition, - config.refundHook + config.authorizePreActionCondition, + config.authorizePostActionHook, + config.chargePreActionCondition, + config.chargePostActionHook, + config.capturePreActionCondition, + config.capturePostActionHook, + config.voidPreActionCondition, + config.voidPostActionHook, + config.refundPreActionCondition, + config.refundPostActionHook )); ``` diff --git a/contracts/gas-costs.mdx b/contracts/gas-costs.mdx index 509a305..348a198 100644 --- a/contracts/gas-costs.mdx +++ b/contracts/gas-costs.mdx @@ -54,7 +54,7 @@ The PaymentOperator runs with pluggable conditions (checked before an action) an | Commerce Payments escrow (no operator) | 78,353 |: | Raw `AuthCaptureEscrow.authorize()`: validates payment, escrows tokens via `PreApprovalPaymentCollector` | | + PaymentOperator layer | 117,250 | **+38,897** | Operator dispatch, plugin slot checks, access control: all conditions, hooks, and fee calculator set to `address(0)` | | + Fee calculation | 135,961 | **+18,711** | `StaticFeeCalculator`: calculates protocol + operator fees, validates bounds, locks fees in `authorizedFees[hash]` | -| + EscrowPeriod hook | 162,744 | **+26,783** | `EscrowPeriod.record()`: stores `authorizationTime[hash] = block.timestamp` (cold SSTORE to cross-contract slot) | +| + EscrowPeriod hook | 162,744 | **+26,783** | `EscrowPeriod.run()`: stores `authorizationTime[hash] = block.timestamp` (cold SSTORE to cross-contract slot) | The EscrowPeriod hook is the single most expensive plugin on `authorize` because it writes to a new storage slot in the EscrowPeriod contract. @@ -79,12 +79,12 @@ These operations only happen when a buyer disputes a payment. Most payments neve ### Off-chain resolution -The refund request, evidence submission, and arbiter approval can all happen off-chain. The only on-chain steps are `freeze()` (to lock the payment during the escrow window) and `voidPayment()` (to return funds). The arbiter never submits a transaction; their approval is an EIP-712 signature that anyone can relay. +The refund request, evidence submission, and arbiter approval can all happen off-chain. The only on-chain steps are `freeze()` (to lock the payment during the escrow window) and `void()` (to return funds). The arbiter never submits a transaction; their approval is an EIP-712 signature that anyone can relay. | On-chain step | Gas | vs transfer | Who Calls | |--------------|-----|------------|-----------| | `freeze()` | 44,651 | 4.3x | Buyer | -| `voidPayment()` | 65,924 | 6.4x | Anyone | +| `void()` | 65,924 | 6.4x | Anyone | | **Total** | **110,575** | **10.7x** | | Total dispute cost on Base with off-chain resolution: **< $0.005**. diff --git a/contracts/hooks/authorization-time.mdx b/contracts/hooks/authorization-time.mdx index 758bf50..1991213 100644 --- a/contracts/hooks/authorization-time.mdx +++ b/contracts/hooks/authorization-time.mdx @@ -22,12 +22,13 @@ mapping(bytes32 paymentInfoHash => uint256 authorizedAt) public authorizationTim ```solidity // Called after authorize() -function record( +function run( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, - uint256 amount, - address caller + uint256 /* amount */, + address /* caller */, + bytes calldata /* data */ ) external { - bytes32 hash = escrow.getHash(paymentInfo); + bytes32 hash = _verifyAndHash(paymentInfo); authorizationTimes[hash] = block.timestamp; } @@ -39,13 +40,15 @@ function getAuthorizationTime( } ``` +`amount`, `caller`, and `data` are unused; they exist to satisfy `IHook.run`. + ## When to Use Use AuthorizationTimeRecorderHook directly only if you need authorization timestamps **without** escrow period enforcement. For most use cases, [EscrowPeriod](/contracts/conditions/escrow-period) is the better choice since it includes this hook plus time-lock condition logic. ## Gas -**Cost:** ~20k gas per `record()` call (one `SSTORE` for the timestamp). +**Cost:** ~20k gas per `run()` call (one `SSTORE` for the timestamp). ## Next Steps diff --git a/contracts/hooks/combinator.mdx b/contracts/hooks/combinator.mdx index ce17955..f0ea711 100644 --- a/contracts/hooks/combinator.mdx +++ b/contracts/hooks/combinator.mdx @@ -24,7 +24,7 @@ config.authorizePostActionHook = comboAddress; - The combinator invokes hooks in the order provided - **If any hook reverts, all revert**: the entire recording is atomic -- Each hook receives the same `paymentInfo`, `amount`, and `caller` parameters +- Each hook receives the same `paymentInfo`, `amount`, `caller`, and `data` parameters ## Limits diff --git a/contracts/hooks/custom.mdx b/contracts/hooks/custom.mdx index b1f1048..7a0d4c2 100644 --- a/contracts/hooks/custom.mdx +++ b/contracts/hooks/custom.mdx @@ -12,59 +12,70 @@ You can build custom hooks for specialized tracking beyond what the built-in hoo ```solidity interface IHook { - function record( + function run( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, uint256 amount, - address caller + address caller, + bytes calldata data ) external; } ``` +`data` is forwarded verbatim from the action call (signatures, proofs, attestations). Subclasses ignore parameters they do not need. + ## Extending BaseHook -Extend `BaseHook` to ensure only authorized operators can call `record()`: +Extend `BaseHook` and call `_verifyAndHash(paymentInfo)` to enforce caller and payment-existence checks: ```solidity contract MyHook is BaseHook { - constructor(address _escrow) BaseHook(_escrow) {} + constructor(address escrow, bytes32 authorizedCodehash) + BaseHook(escrow, authorizedCodehash) + {} - function record( + function run( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, uint256 amount, - address caller - ) external onlyAuthorizedOperator { - // Your recording logic here + address /* caller */, + bytes calldata /* data */ + ) external override { + bytes32 hash = _verifyAndHash(paymentInfo); + // Your recording logic here, keyed by `hash` } } ``` -## Example: ReleaseCountHook +`authorizedCodehash` is the runtime codehash of an optional trusted caller (e.g. `HookCombinator`). Pass `bytes32(0)` to gate solely on `msg.sender == paymentInfo.operator`. + +## Example: CaptureCountHook -Tracks the number and total amount of releases per payment: +Tracks the number and total amount of captures per payment: ```solidity -contract ReleaseCountHook is BaseHook { - mapping(bytes32 => uint256) public releaseCount; - mapping(bytes32 => uint256) public totalReleased; +contract CaptureCountHook is BaseHook { + mapping(bytes32 => uint256) public captureCount; + mapping(bytes32 => uint256) public totalCaptured; - constructor(address _escrow) BaseHook(_escrow) {} + constructor(address escrow, bytes32 authorizedCodehash) + BaseHook(escrow, authorizedCodehash) + {} - function record( - PaymentInfo calldata payment, + function run( + AuthCaptureEscrow.PaymentInfo calldata paymentInfo, uint256 amount, - address caller - ) external onlyAuthorizedOperator { - bytes32 hash = escrow.getHash(payment); - releaseCount[hash]++; - totalReleased[hash] += amount; + address /* caller */, + bytes calldata /* data */ + ) external override { + bytes32 hash = _verifyAndHash(paymentInfo); + captureCount[hash]++; + totalCaptured[hash] += amount; } - // Enable tracking partial releases function getStats(bytes32 paymentHash) external view returns (uint256 count, uint256 total) { - return (releaseCount[paymentHash], totalReleased[paymentHash]); + return (captureCount[paymentHash], totalCaptured[paymentHash]); } } ``` @@ -74,24 +85,27 @@ contract ReleaseCountHook is BaseHook { Test custom hooks with Forge: ```solidity -contract ReleaseCountHookTest is Test { - ReleaseCountHook hook; +contract CaptureCountHookTest is Test { + CaptureCountHook hook; function setUp() public { - hook = new ReleaseCountHook(address(escrow)); + hook = new CaptureCountHook(address(escrow), bytes32(0)); } - function test_incrementsOnRecord() public { - hook.record(paymentInfo, 100e6, caller); + function test_incrementsOnRun() public { + vm.prank(paymentInfo.operator); + hook.run(paymentInfo, 100e6, address(0), ""); bytes32 hash = escrow.getHash(paymentInfo); (uint256 count, uint256 total) = hook.getStats(hash); assertEq(count, 1); assertEq(total, 100e6); } - function test_tracksMultipleReleases() public { - hook.record(paymentInfo, 50e6, caller); - hook.record(paymentInfo, 30e6, caller); + function test_tracksMultipleCaptures() public { + vm.startPrank(paymentInfo.operator); + hook.run(paymentInfo, 50e6, address(0), ""); + hook.run(paymentInfo, 30e6, address(0), ""); + vm.stopPrank(); bytes32 hash = escrow.getHash(paymentInfo); (uint256 count, uint256 total) = hook.getStats(hash); assertEq(count, 2); @@ -102,13 +116,13 @@ contract ReleaseCountHookTest is Test { ## Security Checklist -- [ ] Extends `BaseHook` for operator access control -- [ ] Handles edge cases (zero amounts, duplicate records) +- [ ] Extends `BaseHook` and uses `_verifyAndHash` for caller and payment-existence checks +- [ ] Returns early instead of reverting on business-logic edge cases (a reverting hook permanently bricks the surrounding action) - [ ] Gas-efficient storage layout - [ ] Full test coverage across the public surface -Unlike conditions, hooks **do mutate state**. Use `BaseHook` for access control to keep unauthorized writes out. +Unlike conditions, hooks **do mutate state**. `BaseHook._verifyAndHash` enforces `msg.sender == paymentInfo.operator` (or matches `AUTHORIZED_CODEHASH`) plus payment existence in escrow. Calling `_verifyAndHash` is mandatory. ## Next Steps diff --git a/contracts/hooks/overview.mdx b/contracts/hooks/overview.mdx index 3bc4666..d5055fe 100644 --- a/contracts/hooks/overview.mdx +++ b/contracts/hooks/overview.mdx @@ -20,10 +20,11 @@ Hooks are pluggable contracts that update state **after** an action successfully ```solidity interface IHook { - function record( + function run( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, uint256 amount, - address caller + address caller, + bytes calldata data ) external; } ``` @@ -32,6 +33,7 @@ interface IHook { - `paymentInfo`, The payment information struct - `amount`, The amount involved in the action - `caller`, The address that executed the action (msg.sender on operator) +- `data`, Arbitrary data forwarded from the caller (signatures, proofs, attestations) ## Default behavior diff --git a/contracts/hooks/payment-index.mdx b/contracts/hooks/payment-index.mdx index 58e42d7..a3d7076 100644 --- a/contracts/hooks/payment-index.mdx +++ b/contracts/hooks/payment-index.mdx @@ -6,59 +6,75 @@ icon: "list-ol" ## Overview -PaymentIndexRecorderHook records a sequential index for each payment action, so one payment can support more than one refund request. Every `record()` call increments the index by 1. +PaymentIndexRecorderHook indexes payments by payer and receiver and stores the full `PaymentInfo` struct keyed by `paymentInfoHash`. Wire it into `AUTHORIZE_POST_ACTION_HOOK` on a `HookCombinator` (or directly when authorize is the only hook slot you use) so each new authorization registers itself for on-chain lookups. ## When to use - You need on-chain payment lookups **without a subgraph** -- You need to support **more than one refund request** per payment (each keyed by nonce) -- Other contracts need to read the payment index on-chain +- Other contracts need to read the full `PaymentInfo` for a hash, or page through every payment by payer / receiver +- You want a single chain-singleton index that aggregates across every operator routing through `HookCombinator` **Skip when:** you're using a subgraph for payment queries, since the subgraph can derive indexes from events without the on-chain gas cost. ## State ```solidity -mapping(bytes32 paymentInfoHash => uint256 count) public paymentIndex; +mapping(bytes32 paymentInfoHash => AuthCaptureEscrow.PaymentInfo) private paymentInfoStore; +mapping(address payer => mapping(uint256 index => bytes32 hash)) private payerPayments; +mapping(address payer => uint256 count) public payerPaymentCount; +mapping(address receiver => mapping(uint256 index => bytes32 hash)) private receiverPayments; +mapping(address receiver => uint256 count) public receiverPaymentCount; ``` ## Methods ```solidity -function record( +function run( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, - uint256 amount, - address caller + uint256 /* amount */, + address /* caller */, + bytes calldata /* data */ ) external { bytes32 hash = escrow.getHash(paymentInfo); - paymentIndex[hash]++; + paymentInfoStore[hash] = paymentInfo; + payerPayments[paymentInfo.payer][payerPaymentCount[paymentInfo.payer]++] = hash; + receiverPayments[paymentInfo.receiver][receiverPaymentCount[paymentInfo.receiver]++] = hash; } -function getPaymentIndex( - AuthCaptureEscrow.PaymentInfo calldata paymentInfo -) external view returns (uint256) { - return paymentIndex[escrow.getHash(paymentInfo)]; -} +function getPaymentInfo( + bytes32 paymentInfoHash +) external view returns (AuthCaptureEscrow.PaymentInfo memory); + +function getPayerPayments( + address payer, + uint256 offset, + uint256 count +) external view returns (AuthCaptureEscrow.PaymentInfo[] memory, uint256 total); + +function getReceiverPayments( + address receiver, + uint256 offset, + uint256 count +) external view returns (AuthCaptureEscrow.PaymentInfo[] memory, uint256 total); ``` -## Integration with RefundRequest +`amount`, `caller`, and `data` are unused; they exist to satisfy `IHook.run`. -The `nonce` parameter in `RefundRequest.requestRefund()` corresponds to the payment index. This allows one refund request per charge/action: +## Querying ```typescript -// After two charges on the same payment: -// paymentIndex = 2 - -// Refund request for first charge -await refundRequest.requestRefund(paymentInfo, amount1, 0); // nonce 0 +const info = await paymentIndex.read.getPaymentInfo([paymentInfoHash]) -// Refund request for second charge -await refundRequest.requestRefund(paymentInfo, amount2, 1); // nonce 1 +const [payerInfos, total] = await paymentIndex.read.getPayerPayments([ + payer, + 0n, + 10n, +]) ``` ## Gas -**Cost:** ~20k gas per `record()` call (one `SSTORE` for the counter increment). +**Cost:** ~175k gas per authorization (payer index + receiver index + full `PaymentInfo` SSTORE). ## Next Steps diff --git a/contracts/overview.mdx b/contracts/overview.mdx index db337bf..c012015 100644 --- a/contracts/overview.mdx +++ b/contracts/overview.mdx @@ -22,17 +22,17 @@ Core escrow contract for holding ERC-20 tokens during payments. **Features:** - Authorization-based deposits (no direct transfers) -- Payment state machine: `NonExistent` → `InEscrow` → `Captured` → `Settled` +- Per-payment state queried via `paymentState(hash)` → `(hasCollected, capturableAmount, ...)` - Void/reclaim for failed authorizations - CaptureAuthorizer-based access control **Key Methods:** ```solidity -authorize(paymentId, payer, receiver, amount, token, operator) -charge(paymentId, amount) -capture(paymentInfo, amount) -void(paymentId) -reclaim(paymentId, receiver, amount) +authorize(paymentInfo, amount, tokenCollector, collectorData) +charge(paymentInfo, amount, tokenCollector, collectorData) +capture(paymentInfo, amount, data) +void(paymentInfo, data) +reclaim(paymentInfo, data) ``` ### ERC3009PaymentCollector diff --git a/contracts/payment-operator.mdx b/contracts/payment-operator.mdx index 4e772cd..f94121e 100644 --- a/contracts/payment-operator.mdx +++ b/contracts/payment-operator.mdx @@ -130,13 +130,15 @@ Releases funds from escrow to receiver (capture). ```solidity function capture( AuthCaptureEscrow.PaymentInfo calldata paymentInfo, - uint256 amount + uint256 amount, + bytes calldata data ) external nonReentrant ``` **Parameters:** - `paymentInfo` - Payment info struct - `amount` - Amount to capture +- `data` - Optional pass-through data for the pre/post action plugins **Flow:** 1. Check `CAPTURE_PRE_ACTION_CONDITION` (if set) diff --git a/contracts/periphery/auth-capture-escrow.mdx b/contracts/periphery/auth-capture-escrow.mdx index f1de345..9e32260 100644 --- a/contracts/periphery/auth-capture-escrow.mdx +++ b/contracts/periphery/auth-capture-escrow.mdx @@ -1,6 +1,6 @@ --- title: "Commerce Payments" -description: "AuthCaptureEscrow and the canonical token collectors that form the authCapture base layer" +description: "AuthCaptureEscrow and the canonical token collectors that form the auth-capture base layer" icon: "vault" --- @@ -102,7 +102,7 @@ function void(PaymentInfo calldata paymentInfo) external #### reclaim() -Permissionless: the payer can pull funds back out of escrow after `captureDeadline` if the captureAuthorizer never captured. +Payer-only: gated by `onlySender(paymentInfo.payer)`. The payer can pull funds back out of escrow after `captureDeadline` if the captureAuthorizer never captured. No third party (including the operator or arbiter) can call `reclaim` on the payer's behalf. ```solidity function reclaim(PaymentInfo calldata paymentInfo) external @@ -167,7 +167,7 @@ const authorization = { ``` -The authCapture scheme uses `receiveWithAuthorization` (not `transferWithAuthorization`). The token collector is the `to` address, which then routes tokens into the escrow. +The auth-capture scheme uses `receiveWithAuthorization` (not `transferWithAuthorization`). The token collector is the `to` address, which then routes tokens into the escrow. --- @@ -178,4 +178,4 @@ Collects ERC-20 tokens through Uniswap Permit2 `permitTransferFrom`. The operato The client signs a Permit2 `PermitTransferFrom`; the deterministic nonce binds the merchant address, removing the need for a separate witness struct. -See the [authCapture Scheme Specification](/x402-integration/auth-capture-scheme) for the full Permit2 wire format. +See the [auth-capture Scheme Specification](/x402-integration/auth-capture-scheme) for the full Permit2 wire format. diff --git a/docs.json b/docs.json index 4f340f4..36e8d95 100644 --- a/docs.json +++ b/docs.json @@ -21,16 +21,10 @@ ] }, { - "group": "authCapture Scheme", + "group": "auth-capture Scheme", "pages": [ "x402-integration/auth-capture-scheme" ] - }, - { - "group": "Roadmap", - "pages": [ - "roadmap" - ] } ] }, diff --git a/roadmap.mdx b/roadmap.mdx deleted file mode 100644 index dfa84b6..0000000 --- a/roadmap.mdx +++ /dev/null @@ -1,134 +0,0 @@ ---- -title: "Roadmap" -description: "What x402r has shipped, what's in flight, and what's planned next" -icon: "map" ---- - -## Protocol - -### Shipped - -- **authCapture scheme** built on the audited [Commerce Payments Protocol](https://github.com/base/commerce-payments) (`AuthCaptureEscrow` + token collectors), used directly at universal CREATE2 addresses (no fork) -- **Two settlement paths**: two-phase `authorize` then `capture` (default), and single-shot `autoCapture` for atomic `charge` -- **Client signing**: ERC-3009 (`receiveWithAuthorization`) or Uniswap Permit2 (`permitTransferFrom`), selectable per request via `assetTransferMethod` -- **PaymentOperator** with pluggable pre-action conditions and post-action hooks for `void`, `refund`, `capture`, `charge` -- **EscrowPeriod**, **Freeze**, **RefundRequest**, and **RefundRequestEvidence** plugins for dispute and arbitration flows -- **Factory pattern** with CREATE2 deterministic deployment -- **Reference implementation**: [x402r-scheme](https://github.com/BackTrackCo/x402r-scheme) (`@x402r/evm`) -- **Solana (`@x402r/svm`)**: 6-program Anchor pilot landed alongside the EVM mechanism package. See [SVM Pilot](#solana-pilot-alpha) below. - -### In flight - -- **authCapture scheme spec** moving toward upstream merge into the x402 protocol. See the [authCapture Scheme Specification](/x402-integration/auth-capture-scheme) for the wire format and verification logic. -- Documentation restructure and accuracy fixes (you are reading the result) -- Simple "API down" arbiter template for first merchants -- Tenjin: description-vs-content arbiter using an LLM in a TEE - -### Planned - -- Bond-based disputes -- Multi-level verification (L1 schema/auto, L2 AI review, L3 human arbitration) with per-tier `MaxAmountCondition` exposure caps -- Many arbiters per operator and an arbiter marketplace -- After-capture arbitration handling -- Reputation system for clients, merchants, and arbiters (via ERC-8004) -- Token wrapper for enforced refund protection - ---- - -## Solana Pilot (Alpha) - -Solana support is **alpha/experimental**. The Anchor programs and TypeScript mechanism package exist, but no audit has run, and the pilot has not yet shipped to mainnet-beta. Use at your own risk; flag any production use to the team first. - -### Shipped - -- Six Anchor programs at [x402r-contracts-svm](https://github.com/BackTrackCo/x402r-contracts-svm): `authCapture` escrow with `authorize`, `charge`, `capture`, `void`, `refund`, `reclaim` instructions, plus address-match condition programs (`static-address`, `receiver`, `payer`) -- `@x402r/svm` TypeScript package (Solana Kit + Codama-generated client) mirroring `@x402r/evm`'s client/server/facilitator/shared shape -- Plugin slot plumbing: 5 pre-action `condition_programs` and 5 post-action `hook_programs` per `PaymentInfo`, with canonical `ICondition` / `IHook` instruction signatures so third-party arbiter programs slot in without escrow changes -- Fee enforcement (protocol + operator splits) parity with EVM - -### Planned - -- Devnet + mainnet-beta deployment -- Devnet demo exercising all three lifecycle paths (`authorize, capture`, `authorize, void`, `charge, refund`) with a sample arbiter program -- `scheme_authCapture_svm.md` spec doc upstream - ---- - -## SDK - -The SDK lives at [x402r-sdk](https://github.com/BackTrackCo/x402r-sdk). All packages target the canonical `authCapture` contract surface and the `@x402r/evm` v0.2.x scheme adapter. - -### Shipped - -| Package | Purpose | -|---|---| -| `@x402r/core` | Stateless viem-style action functions (payment, operator, refund, evidence, escrow, freeze, watch) | -| `@x402r/sdk` | Stateful client with action groups and a `.extend()` plugin pattern | -| `@x402r/helpers` | `x402rDefaults()` and wire-format re-exports | -| `@x402r/cli` | Wallet-agnostic one-shot payment CLI (0.2.0+) | - -Other shipped capabilities: - -- **ERC-8004 plugin**: identity, reputation, and discovery actions -- **ERC-8004 extraction helpers** with `identity.check()` -- **Anvil fork testing** via prool, 80% coverage threshold in CI -- **Biome** + **@wagmi/cli** ABI codegen against the contracts -- Role-based presets: `createPayerClient()`, `createMerchantClient()`, `createArbiterClient()` - -### In flight - -- Pre-payment info extraction (`getOperatorInfo`: discover arbiter and escrow period from an operator address) -- Combined freeze + refund (`freezeAndRequestRefund` single call) -- Subgraph integration for historical payment listing and stubbed query methods - -### Planned - -- Evidence and metadata system with pluggable backends (IPFS, Arweave) -- Encrypted communication channels (XMTP) -- Session-based billing patterns -- Multi-arbiter support -- Dedicated Express and Hono middleware -- Garbage-detection / multi-tier verification SDK extensions (paired with the arbiter work above) - ---- - -## Contract Status - -All EVM contracts use **universal CREATE2 addresses**: when x402r supports a chain, the address matches every other supported chain. The canonical commerce-payments primitives come from [`base/commerce-payments@v1.0.0`](https://github.com/base/commerce-payments/releases/tag/v1.0.0). - -### Supported chains - -Today: **Base mainnet** and **Base Sepolia**. More EVMs land as canonical `commerce-payments@v1.0.0` coverage from Base extends. - -Solana support is alpha and not yet on mainnet-beta. See [Solana Pilot (Alpha)](#solana-pilot-alpha). - -### Contract inventory - -| Contract | Status | -|---|---| -| AuthCaptureEscrow (canonical) | Deployed | -| ERC3009PaymentCollector (canonical) | Deployed | -| Permit2PaymentCollector (canonical) | Deployed | -| PaymentOperatorFactory | Deployed | -| EscrowPeriodFactory | Deployed | -| FreezeFactory | Deployed | -| StaticFeeCalculatorFactory | Deployed | -| All condition / combinator factories | Deployed | -| ProtocolFeeConfig | Deployed | -| RefundRequestEvidence | Deployed | -| Condition singletons (Payer, Receiver, AlwaysTrue) | Deployed | - - -All contract addresses are available in `@x402r/core` via `getChainConfig(chainId)`. See the [SDK Overview](/sdk/overview) page for details. - - -## Get Involved - - - - Follow development and contribute. - - - Get in touch with the team. - - diff --git a/sdk/cli.mdx b/sdk/cli.mdx index ad9d522..92839fb 100644 --- a/sdk/cli.mdx +++ b/sdk/cli.mdx @@ -49,6 +49,7 @@ Environment variable names use no `X402R_` prefix to match Foundry, Hardhat, and | Flag | Description | |------|-------------| | `--chain ` | Select a specific `accepts[]` entry when the merchant offers more than one chain. Required when more than one option exists. | +| `--asset-transfer-method ` | Select the token-collection path when the chosen `accepts[]` entry supports both EIP-3009 and Permit2. Required when more than one option exists. | | `--rpc ` | Override the RPC endpoint for on-chain reads. Required for chain IDs not in `viem/chains`. | | `--max-amount ` | Refuse to pay more than `n` atomic token units. Exits with code 3 if the price exceeds this. | | `--json` | Emit a single JSON envelope to stdout instead of plain text. | diff --git a/sdk/delivery-arbiter.mdx b/sdk/delivery-arbiter.mdx index 58af2a4..bae01d0 100644 --- a/sdk/delivery-arbiter.mdx +++ b/sdk/delivery-arbiter.mdx @@ -54,25 +54,24 @@ The [AI garbage detector example](https://github.com/BackTrackCo/arbiter-example - The merchant's `forwardToArbiter()` hook POSTs `{ responseBody, transaction, paymentPayload }` to `/verify`. The payload does not contain `PaymentInfo` directly. Reconstruct it from `paymentPayload.accepted.extra` + `paymentPayload.payload.salt` + the recovered `payer` + the top-level requirements (`payTo`, `asset`, `amount`), or run the payload back through your own facilitator's verify step to recover it. + The merchant's `forwardToArbiter()` hook POSTs `{ responseBody, transaction, paymentInfoWire }` to `/verify`. The `paymentInfoWire` is the JSON-safe wire form of `PaymentInfo`; call `PaymentInfo.fromWire(...)` to recover the `bigint`-typed struct expected by SDK actions. ```typescript - import { isAuthCapturePayload } from '@x402r/helpers' + import { PaymentInfo } from '@x402r/sdk' import express from 'express' const app = express() app.use(express.json()) app.post('/verify', async (req, res) => { - const { responseBody, transaction, paymentPayload } = req.body + const { responseBody, transaction, paymentInfoWire } = req.body - if (!isAuthCapturePayload(paymentPayload?.payload)) { - res.status(400).json({ error: 'unsupported_payload' }) + if (!paymentInfoWire) { + res.status(400).json({ error: 'missing_payment_info' }) return } - // Reconstruct PaymentInfo from the wire payload (see forwardToArbiter docs). - const paymentInfo = reconstructPaymentInfo(paymentPayload) + const paymentInfo = PaymentInfo.fromWire(paymentInfoWire) const passed = await evaluate(responseBody) diff --git a/sdk/delivery-merchant.mdx b/sdk/delivery-merchant.mdx index 543d292..3ac4af0 100644 --- a/sdk/delivery-merchant.mdx +++ b/sdk/delivery-merchant.mdx @@ -25,20 +25,19 @@ icon: "store" - Add the `forwardToArbiter()` hook to your x402 resource server. After every successful `authCapture` settlement, it POSTs to your arbiter service fire-and-forget: + Add the `forwardToArbiter()` hook to your x402 resource server. After every successful `auth-capture` settlement, it POSTs to your arbiter service fire-and-forget: ```typescript import { forwardToArbiter } from '@x402r/helpers' - import { registerAuthCaptureEvmScheme } from '@x402r/evm/authCapture/server' + import { AuthCaptureEvmScheme } from '@x402r/evm/auth-capture/server' const resourceServer = new x402ResourceServer(facilitatorConfig) - registerAuthCaptureEvmScheme(resourceServer) - - resourceServer.onAfterSettle( - forwardToArbiter('http://your-arbiter:3001', { - onError: (err) => console.error('Arbiter unreachable:', err), - }), - ) + .register(networkId, new AuthCaptureEvmScheme()) + .onAfterSettle( + forwardToArbiter('http://your-arbiter:3001', { + onError: (err) => console.error('Arbiter unreachable:', err), + }), + ) ``` The hook POSTs to `{arbiterUrl}/verify` with: @@ -47,15 +46,24 @@ icon: "store" { "responseBody": "the HTTP response body as a string", "transaction": "0xsettlement_tx_hash", - "paymentPayload": { - "x402Version": 2, - "accepted": { "scheme": "authCapture", "network": "eip155:84532", "...": "..." }, - "payload": { "authorization": { "...": "..." }, "signature": "0x...", "salt": "0x..." } + "paymentInfoWire": { + "operator": "0x...", + "payer": "0x...", + "receiver": "0x...", + "token": "0x...", + "maxAmount": "10000", + "preApprovalExpiry": 1740758554, + "authorizationExpiry": 1740762154, + "refundExpiry": 1741276954, + "minFeeBps": 0, + "maxFeeBps": 500, + "feeReceiver": "0x...", + "salt": "0x..." } } ``` - The payload does not include a nested `paymentInfo`. The arbiter reconstructs `PaymentInfo` from `accepted.extra` + `payload.salt` + the recovered payer + the top-level requirements. See [forwardToArbiter() docs](/sdk/helpers/forward-to-arbiter) for the full payload shape. + The helper reconstructs `PaymentInfoWire` from the verified `SettleResultContext`. The arbiter consumes `req.body.paymentInfoWire` and runs it through `PaymentInfo.fromWire(...)` to recover the `bigint`-typed struct expected by SDK actions. See [forwardToArbiter() docs](/sdk/helpers/forward-to-arbiter) for the full payload shape. `forwardToArbiter()` is fire-and-forget. If the arbiter service is unreachable, funds stay in escrow until timeout. Add monitoring for arbiter availability. diff --git a/sdk/examples.mdx b/sdk/examples.mdx index 490030f..9625352 100644 --- a/sdk/examples.mdx +++ b/sdk/examples.mdx @@ -56,7 +56,7 @@ See each example directory's README on GitHub for the exact run command for that Walk through `deployMarketplaceOperator()` and `deployDeliveryProtectionOperator()`. - Forward `authCapture` settlements to an arbiter service. + Forward `auth-capture` settlements to an arbiter service. Browse every example. diff --git a/sdk/helpers/forward-to-arbiter.mdx b/sdk/helpers/forward-to-arbiter.mdx index b298754..28d7404 100644 --- a/sdk/helpers/forward-to-arbiter.mdx +++ b/sdk/helpers/forward-to-arbiter.mdx @@ -5,19 +5,20 @@ description: "Forward settlement data to an arbiter service for quality evaluati icon: "arrow-right" --- -The `forwardToArbiter()` function creates an `onAfterSettle` hook that forwards the response body and payment payload to an arbiter service. It runs fire-and-forget so it never blocks the response to the client. +The `forwardToArbiter()` function creates an `onAfterSettle` hook that forwards the response body and reconstructed `PaymentInfoWire` to an arbiter service. It runs fire-and-forget so it never blocks the response to the client. -- Only fires for successful **`authCapture`** scheme settlements -- POSTs `{ responseBody, transaction, paymentPayload }` to `{arbiterUrl}/verify` +- Only fires for successful **`auth-capture`** scheme settlements +- POSTs `{ responseBody, transaction, paymentInfoWire }` to `{arbiterUrl}/verify` - The hook catches errors internally so an unreachable arbiter cannot break the payment flow ## Usage ```typescript import { forwardToArbiter } from '@x402r/helpers' +import { AuthCaptureEvmScheme } from '@x402r/evm/auth-capture/server' const resourceServer = new x402ResourceServer(facilitatorClient) - .register(networkId, new AuthCaptureServerScheme()) + .register(networkId, new AuthCaptureEvmScheme()) .onAfterSettle( forwardToArbiter('http://arbiter:3001'), ) @@ -50,35 +51,30 @@ interface ForwardToArbiterOptions { ## Payload shape -When an `authCapture` settlement succeeds, the hook POSTs the following JSON to `{arbiterUrl}/verify`: +When an `auth-capture` settlement succeeds, the hook POSTs the following JSON to `{arbiterUrl}/verify`: ```typescript { - responseBody: string // UTF-8 encoded response body - transaction: string // Settlement transaction hash - paymentPayload: { - x402Version: number - accepted: { - scheme: 'authCapture' - network: string // e.g. "eip155:84532" - amount: string - asset: `0x${string}` - payTo: `0x${string}` - maxTimeoutSeconds: number - extra: AuthCaptureExtra // captureAuthorizer, captureDeadline, refundDeadline, ... - } - payload: - | { authorization: { from, to, value, validAfter, validBefore, nonce }; signature; salt } // EIP-3009 - | { permit2Authorization: { from, permitted, spender, nonce, deadline }; signature; salt } // Permit2 + responseBody: string // UTF-8 encoded response body + transaction: string // Settlement transaction hash + paymentInfoWire: { + operator: `0x${string}` // from extra.captureAuthorizer + payer: `0x${string}` // recovered at settlement + receiver: `0x${string}` // from requirements.payTo + token: `0x${string}` // from requirements.asset + maxAmount: string // from requirements.amount + preApprovalExpiry: number // authorization.validBefore (EIP-3009) or permit2Authorization.deadline (Permit2) + authorizationExpiry: number // from extra.captureDeadline + refundExpiry: number // from extra.refundDeadline + minFeeBps: number // from extra.minFeeBps + maxFeeBps: number // from extra.maxFeeBps + feeReceiver: `0x${string}` // from extra.feeRecipient + salt: string // from payload.salt } } ``` -The payload does not nest a `paymentInfo` object. The arbiter reconstructs `PaymentInfo` from `accepted.extra` + `payload.salt` + the recovered `payer` + the top-level requirements (`payTo`, `asset`, `amount`), the same way the facilitator does at settlement. - - -The arbiter can also resolve the `PaymentInfo` indirectly: derive `paymentInfoHash` from the wire data, then read the on-chain `PaymentInfo` from the escrow if it stores it, or simply forward the wire payload back into your own facilitator to verify. - +The helper reconstructs `PaymentInfoWire` from the verified `SettleResultContext` using the `reconstructPaymentInfoWire()` helper. The arbiter consumes `req.body.paymentInfoWire` and runs it through `PaymentInfo.fromWire(...)` (from `@x402r/sdk` or `@x402r/core`) to get the `bigint`-typed `PaymentInfo` struct expected by SDK actions. ## Error handling @@ -86,9 +82,10 @@ By default, the hook logs fetch errors with `console.warn`. Override this with a ```typescript import { forwardToArbiter } from '@x402r/helpers' +import { AuthCaptureEvmScheme } from '@x402r/evm/auth-capture/server' const resourceServer = new x402ResourceServer(facilitatorClient) - .register(networkId, new AuthCaptureServerScheme()) + .register(networkId, new AuthCaptureEvmScheme()) .onAfterSettle( forwardToArbiter('http://arbiter:3001', { onError: (err) => sentry.captureException(err), @@ -103,7 +100,7 @@ The hook wraps each error in an `X402rError` carrying the arbiter endpoint and r The hook returns without making a request when: - The settlement was not successful (`context.result.success === false`) -- The scheme is not `authCapture` +- The scheme is not `auth-capture` - No response body is available in the transport context ## Address re-exports diff --git a/sdk/merchant/getting-started.mdx b/sdk/merchant/getting-started.mdx index 4cae0b6..146beb7 100644 --- a/sdk/merchant/getting-started.mdx +++ b/sdk/merchant/getting-started.mdx @@ -44,7 +44,7 @@ This guide walks you through setting up an Express server that accepts x402r esc import "dotenv/config"; import express from "express"; import { paymentMiddleware, x402ResourceServer } from "@x402/express"; - import { AuthCaptureServerScheme } from "@x402r/evm/authCapture/server"; + import { AuthCaptureEvmScheme } from "@x402r/evm/auth-capture/server"; import { getChainConfig } from "@x402r/core"; import { HTTPFacilitatorClient } from "@x402/core/server"; @@ -74,7 +74,7 @@ This guide walks you through setting up an Express server that accepts x402r esc "GET /weather": { accepts: [ { - scheme: "authCapture", + scheme: "auth-capture", price: "$0.01", network: networkId, payTo: address, @@ -99,7 +99,7 @@ This guide walks you through setting up an Express server that accepts x402r esc }, new x402ResourceServer(facilitatorClient).register( networkId, - new AuthCaptureServerScheme() as never, + new AuthCaptureEvmScheme(), ), ), ); @@ -132,13 +132,13 @@ This guide walks you through setting up an Express server that accepts x402r esc curl http://localhost:4021/weather ``` - Without a valid payment header, the server responds with HTTP 402 and the authCapture payment requirements: + Without a valid payment header, the server responds with HTTP 402 and the auth-capture payment requirements: ```json { "x402Version": 2, "accepts": [{ - "scheme": "authCapture", + "scheme": "auth-capture", "price": "$0.01", "network": "eip155:84532", "payTo": "0x...", @@ -162,7 +162,7 @@ This guide walks you through setting up an Express server that accepts x402r esc ## How it works - **`extra` config** declares the captureAuthorizer, capture/refund deadlines, fee recipient, and fee bounds. The canonical `AuthCaptureEscrow` and token collector addresses are universal CREATE2 deploys, so routes do not need to repeat them. -- **`AuthCaptureServerScheme`** registers the authCapture payment scheme with the x402 resource server so it can verify authCapture-backed payments. +- **`AuthCaptureEvmScheme`** registers the auth-capture payment scheme with the x402 resource server so it can verify auth-capture-backed payments. - **`paymentMiddleware`** intercepts requests, checks for a valid payment header, and returns 402 when the caller has not provided one. - **`HTTPFacilitatorClient`** connects to the facilitator service that verifies and settles payments on-chain. diff --git a/sdk/overview.mdx b/sdk/overview.mdx index af3e984..a91e192 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -81,12 +81,15 @@ bunx @x402r/cli pay [options] ### Guides - - Deploy an operator, accept a payment, capture funds from escrow. - Capture funds from escrow, charge directly, void, and process refunds using `createMerchantClient`. + + Deploy a PaymentOperator on Base or Base Sepolia and configure plugin slots. + + + Wire merchant settlements into an arbiter that gates capture on response quality. + Wallet-agnostic one-shot payments from the command line or scripts. diff --git a/x402-integration/auth-capture-scheme.mdx b/x402-integration/auth-capture-scheme.mdx index aed1348..dd22a2d 100644 --- a/x402-integration/auth-capture-scheme.mdx +++ b/x402-integration/auth-capture-scheme.mdx @@ -1,14 +1,14 @@ --- -title: "authCapture Scheme Specification" -description: "Technical specification for the x402 authCapture payment scheme" +title: "auth-capture Scheme Specification" +description: "Technical specification for the x402 auth-capture payment scheme" icon: "file-contract" --- ## Overview -The **`authCapture` scheme** for x402 v2 uses the audited [Commerce Payments Protocol](https://github.com/base/commerce-payments) (`AuthCaptureEscrow` + token collectors) directly, no fork. The client signs a single signature (ERC-3009 or Permit2). The facilitator submits it, either locking funds in escrow for later capture (two-phase) or sending them directly to the receiver with refund capability (single-shot). +The **`auth-capture` scheme** for x402 v2 uses the audited [Commerce Payments Protocol](https://github.com/base/commerce-payments) (`AuthCaptureEscrow` + token collectors) directly, no fork. The client signs a single signature (ERC-3009 or Permit2). The facilitator submits it, either locking funds in escrow for later capture (two-phase) or sending them directly to the receiver with refund capability (single-shot). -Unlike `exact`, which has no mechanism for returning funds, `authCapture` supports returning funds to the client through void, refund, and reclaim. +Unlike `exact`, which has no mechanism for returning funds, `auth-capture` supports returning funds to the client through void, refund, and reclaim. ## Settlement Paths @@ -86,7 +86,7 @@ sequenceDiagram Note over Client,Receiver: No recourse after payment - Payment is final ``` -### authCapture (Two-phase) +### auth-capture (Two-phase) ```mermaid sequenceDiagram @@ -123,7 +123,7 @@ sequenceDiagram ### Key Differences -| Aspect | Exact | authCapture | +| Aspect | Exact | auth-capture | |---|---|---| | **Settlement** | Immediate on request | Via escrow (two-phase) or direct with refund (single-shot) | | **Payer Protection** | None (payment final) | Refundable in both paths | @@ -154,7 +154,7 @@ Server sends this to request payment: { "x402Version": 2, "accepts": [{ - "scheme": "authCapture", + "scheme": "auth-capture", "network": "eip155:8453", "amount": "1000000", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", @@ -190,7 +190,7 @@ Client sends this with a signed ERC-3009 authorization: "method": "GET" }, "accepted": { - "scheme": "authCapture", + "scheme": "auth-capture", "network": "eip155:8453", "amount": "1000000", "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", @@ -221,7 +221,7 @@ When `extra.assetTransferMethod === "permit2"`, the client signs a Permit2 `Perm { "x402Version": 2, "resource": { "url": "https://api.example.com/resource", "method": "GET" }, - "accepted": { "scheme": "authCapture", "...": "..." }, + "accepted": { "scheme": "auth-capture", "...": "..." }, "payload": { "permit2Authorization": { "from": "0xPayerAddress", @@ -283,7 +283,7 @@ The `salt` field enforces freshness: each signing call generates a fresh `bytes3 The facilitator performs these checks in order: 1. **Type guard**: Payload matches `Eip3009Payload` or `Permit2Payload` (includes `signature` and `salt`). -2. **Scheme match**: `requirements.scheme === "authCapture"` and `payload.accepted.scheme === "authCapture"`. +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**: All required `extra` fields present. 5. **Method routing**: `extra.assetTransferMethod` (default `"eip3009"`) matches the payload shape. @@ -374,17 +374,17 @@ The escrow contract enforces invariants on-chain: | Error Code | Description | |---|---| | `invalid_payload_format` | Payload doesn't match `Eip3009Payload` or `Permit2Payload`. | -| `unsupported_scheme` | Scheme is not `authCapture`. | +| `unsupported_scheme` | Scheme is not `auth-capture`. | | `network_mismatch` | Payload network doesn't match requirements. | | `invalid_network` | Network format is not `eip155:`. | -| `invalid_authCapture_extra` | Extra is missing required fields. | +| `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_authCapture_signature` | Signature verification failed. | +| `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`. | @@ -401,7 +401,7 @@ The escrow contract enforces invariants on-chain: ## vs Exact Scheme -The `authCapture` scheme adds an authorization step before settlement (or refundability for single-shot). For simple immediate payments where trust and refundability aren't concerns, the `exact` scheme remains more efficient. +The `auth-capture` scheme adds an authorization step before settlement (or refundability for single-shot). For simple immediate payments where trust and refundability aren't concerns, the `exact` scheme remains more efficient. ## Next Steps @@ -415,7 +415,7 @@ The `authCapture` scheme adds an authorization step before settlement (or refund - Build your first authCapture payment flow. + Build your first auth-capture payment flow. @@ -425,4 +425,4 @@ The `authCapture` scheme adds an authorization step before settlement (or refund - [AuthCaptureEscrow Contract](https://github.com/base/commerce-payments) - [EIP-3009: Transfer With Authorization](https://eips.ethereum.org/EIPS/eip-3009) - [Uniswap Permit2](https://docs.uniswap.org/contracts/permit2/overview) -- [x402r authCapture Scheme Reference Implementation](https://github.com/BackTrackCo/x402r-scheme) +- [x402r auth-capture Scheme Reference Implementation](https://github.com/BackTrackCo/x402r-scheme) diff --git a/x402-integration/overview.mdx b/x402-integration/overview.mdx index 2abe94a..0d87073 100644 --- a/x402-integration/overview.mdx +++ b/x402-integration/overview.mdx @@ -88,7 +88,7 @@ Client authorizes $10 once → Server tracks usage → Server batches captures o ## How x402r Extends X402 -x402r provides the **authCapture scheme implementation** for x402: +x402r provides the **auth-capture scheme implementation** for x402: 1. **Base Commerce Payments Integration** - Audited escrow contracts from Base @@ -205,8 +205,8 @@ Smart contract that controls capture/void logic. Different operators enable diff ## Next Steps - - Complete technical specification for the authCapture payment scheme. + + Complete technical specification for the auth-capture payment scheme. From c1dc43da321ef8efbea8ca128a4d5ea27976905a Mon Sep 17 00:00:00 2001 From: A1igator Date: Wed, 20 May 2026 23:28:48 -0700 Subject: [PATCH 33/37] docs: IA reorg, auth-capture page split, gas-costs regen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IA reorganization: - Merge `Smart Contracts` tab into `Protocol` — single tab now holds Getting Started, auth-capture Scheme, Contracts, Periphery, Conditions, Hooks. Old `Smart Contracts > Overview` group becomes `Protocol > Contracts` and drops license/audits/gas-costs (now in Resources tab). - SDK group renames: `Marketplace` → `Merchants`, `Reference` → `Tools`. - `sdk/helpers/forward-to-arbiter` moved from Merchants into Delivery Protection — it's a delivery-protection dependency, not a marketplace one. - New `Resources` tab (Aave-style) holds the due-diligence pages: gas-costs, audits, license. Page split for auth-capture-scheme.mdx (428 lines → 4 pages): - `x402-integration/auth-capture/index.mdx`: concept + flow diagrams + CaptureAuthorizer - `x402-integration/auth-capture/wire-format.mdx`: PaymentRequirements + EIP-3009/Permit2 payloads + field reference + nonce derivation - `x402-integration/auth-capture/verification-and-settlement.mdx`: 13-step verify flow, settlement, EIP-6492, error codes (folded in) - `x402-integration/auth-capture/payment-info.mdx`: on-chain struct, expiry ordering, safety guarantees - Redirect from `/x402-integration/auth-capture-scheme` to the new group index. Gas-costs regen (pinned to x402r-contracts @ bb188db, 2026-05-20): - Re-ran `forge test --match-path test/gas/GasBenchmark.t.sol -vv` and refreshed every number in the page from the test logs. - Authorize: 119,018 (bare) → 146,738 (+ fees) → 182,440 (+ EscrowPeriod hook). Capture: 78,074 → 117,033 → 121,529 → 126,888 → 147,298 across the same plugin progression. - Happy path total: 332,489 (was 331,806). Off-chain dispute total: 114,778 (was 110,575). Full on-chain dispute total: 992,995 (was 1,078,145). - Dropped the "Commerce Payments escrow (no operator)" baseline rows since the current benchmark test only measures through `PaymentOperator`. Added a "Reproducing these numbers" section that pins the commit hash and the exact forge command. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/gas-costs.mdx | 97 ++-- contracts/periphery/auth-capture-escrow.mdx | 2 +- docs.json | 46 +- x402-integration/auth-capture-scheme.mdx | 428 ------------------ x402-integration/auth-capture/index.mdx | 177 ++++++++ .../auth-capture/payment-info.mdx | 72 +++ .../verification-and-settlement.mdx | 83 ++++ x402-integration/auth-capture/wire-format.mdx | 149 ++++++ x402-integration/overview.mdx | 2 +- 9 files changed, 565 insertions(+), 491 deletions(-) delete mode 100644 x402-integration/auth-capture-scheme.mdx create mode 100644 x402-integration/auth-capture/index.mdx create mode 100644 x402-integration/auth-capture/payment-info.mdx create mode 100644 x402-integration/auth-capture/verification-and-settlement.mdx create mode 100644 x402-integration/auth-capture/wire-format.mdx diff --git a/contracts/gas-costs.mdx b/contracts/gas-costs.mdx index 348a198..105b701 100644 --- a/contracts/gas-costs.mdx +++ b/contracts/gas-costs.mdx @@ -1,6 +1,6 @@ --- title: "Gas Costs" -description: "Simulated gas measurements for every on-chain operation, with per-plugin overhead breakdown" +description: "Foundry-measured gas costs for every on-chain x402r operation, with per-plugin overhead breakdown" icon: "gas-pump" --- @@ -8,53 +8,53 @@ icon: "gas-pump" x402r adds escrow, refund windows, and dispute resolution on top of the [Commerce Payments Protocol](https://github.com/base/commerce-payments). Below you'll find the **measured gas cost** of every on-chain operation so you can weigh the overhead. -All numbers are from Foundry simulations (`forge test`) with optimizer enabled (200 runs, via IR). The benchmark test is at [`test/gas/GasBenchmark.t.sol`](https://github.com/BackTrackCo/x402r-contracts/blob/main/test/gas/GasBenchmark.t.sol). +All numbers come from Foundry simulations (`forge test --gas-report`) with optimizer enabled (200 runs, via IR), pinned to [x402r-contracts @ `bb188db`](https://github.com/BackTrackCo/x402r-contracts/commit/bb188dbc0251f9a3af7da57906d5c59e2b2a14d0) (snapshot: 2026-05-20). The benchmark lives at [`test/gas/GasBenchmark.t.sol`](https://github.com/BackTrackCo/x402r-contracts/blob/bb188db/test/gas/GasBenchmark.t.sol). Numbers are per-transaction and warm where the test measures warm (so they reflect typical second-and-beyond payments on the same operator); cold-vs-warm splits are reported alongside the warm number where relevant. -The buyer never pays gas. They only sign an off-chain ERC-3009 authorization. The facilitator, merchant, or another party submits every on-chain transaction. +The buyer never pays gas. They only sign an off-chain ERC-3009 or Permit2 authorization. The facilitator, merchant, or another party submits every on-chain transaction. ## What you'll pay on Base | Role | Operations | Gas | Cost on Base | |------|-----------|-----|-------------| -| **Facilitator** | `authorize()` | 181,544 | < $0.005 | -| **Merchant** | `capture()` | 150,262 | < $0.005 | -| **Happy path total** | authorize + capture | 331,806 | **< $0.01** | +| **Facilitator** | `authorize()` | 182,440 | < $0.005 | +| **Merchant** | `capture()` | 150,049 | < $0.005 | +| **Happy path total** | authorize + capture | 332,489 | **< $0.01** | Disputes are rare and add < $0.005 with off-chain resolution, see [Dispute Path](#dispute-path) below. ## Happy Path -The happy path has **2 on-chain transactions**: `authorize` (at checkout) and `capture` (after the escrow period expires). +The happy path has **2 on-chain transactions**: `authorize` (at checkout) and `capture` (after the escrow period expires). With operator fees enabled, the `distributeFees()` call adds a third settle-time write to claim accumulated protocol fees, batched across many payments. | Operation | Gas | vs transfer | Who Calls | When | |-----------|-----|------------|-----------|------| -| `authorize()` | 181,544 | 17.6x | Facilitator | At checkout (HTTP 402 settlement) | -| `capture()` | 150,262 | 14.6x | Anyone | After escrow period expires | +| `authorize()` | 182,440 | 17.8x | Facilitator | At checkout (HTTP 402 settlement) | +| `capture()` | 150,049 | 14.6x | Anyone | After escrow period expires | +| `distributeFees()` | 57,007 | 5.6x | Owner | Periodically, batched across payments | -The **vs transfer** column shows multiples of a cold ERC-20 `transfer()` (10,305 gas), the absolute floor for moving tokens on-chain. +The **vs transfer** column shows multiples of a cold ERC-20 `transfer()` (10,263 gas), the absolute floor for moving tokens on-chain. In production, the merchant typically calls `capture()`, but the function has no caller restriction beyond the configured capture condition (EscrowPeriod + Freeze). After the escrow period passes and the payment isn't frozen, anyone can trigger it. An escrow authorization is inherently more work than a raw ERC-20 transfer: it validates payment info, checks fee bounds, locks fees, transfers tokens into escrow, and records state. The per-plugin section below shows exactly where the gas goes. -**Facilitators: set a gas limit.** The facilitator pays gas for `authorize()`, but the operator chooses which conditions and hooks to run. Each plugin slot adds cost, and custom plugins can run arbitrary computation. Simulate the transaction with `eth_estimateGas` before submitting and reject operators whose `authorize()` exceeds a reasonable threshold (for example, 300,000 gas). The full x402r configuration uses around 181,000 gas; anything well above that warrants investigation. +**Facilitators: set a gas limit.** The facilitator pays gas for `authorize()`, but the operator chooses which conditions and hooks to run. Each plugin slot adds cost, and custom plugins can run arbitrary computation. Simulate the transaction with `eth_estimateGas` before submitting and reject operators whose `authorize()` exceeds a reasonable threshold (for example, 300,000 gas). The full x402r configuration uses around 182,000 gas; anything well above that warrants investigation. ## Per-Plugin Gas Costs -The PaymentOperator runs with pluggable conditions (checked before an action) and hooks (called after). You choose which plugins to use. Here's the marginal cost of each, measured by diffing adjacent configurations. +The PaymentOperator runs with pluggable conditions (checked before an action) and hooks (called after). You choose which plugins to use. Here's the marginal cost of each, measured by diffing adjacent configurations through the `PaymentOperator` entry point. ### authorize() | Configuration | Gas | Marginal Cost | Plugin | |---------------|-----|---------------|--------| -| Commerce Payments escrow (no operator) | 78,353 |: | Raw `AuthCaptureEscrow.authorize()`: validates payment, escrows tokens via `PreApprovalPaymentCollector` | -| + PaymentOperator layer | 117,250 | **+38,897** | Operator dispatch, plugin slot checks, access control: all conditions, hooks, and fee calculator set to `address(0)` | -| + Fee calculation | 135,961 | **+18,711** | `StaticFeeCalculator`: calculates protocol + operator fees, validates bounds, locks fees in `authorizedFees[hash]` | -| + EscrowPeriod hook | 162,744 | **+26,783** | `EscrowPeriod.run()`: stores `authorizationTime[hash] = block.timestamp` (cold SSTORE to cross-contract slot) | +| PaymentOperator, no plugins | 119,018 |: | `bareOperator.authorize()`: operator dispatch, plugin slot checks, escrow `authorize()` call | +| + Fee calculation | 146,738 | **+27,720** | `StaticFeeCalculator`: calculates protocol + operator fees, validates bounds, locks fees in `authorizedFees[hash]` | +| + EscrowPeriod hook | 182,440 | **+35,702** | `EscrowPeriod.run()`: stores `authorizationTime[hash] = block.timestamp` (cold SSTORE to cross-contract slot) | The EscrowPeriod hook is the single most expensive plugin on `authorize` because it writes to a new storage slot in the EscrowPeriod contract. @@ -62,15 +62,14 @@ The EscrowPeriod hook is the single most expensive plugin on `authorize` because | Configuration | Gas | Marginal Cost | Plugin | |---------------|-----|---------------|--------| -| Commerce Payments escrow (no operator) | 66,365 |: | Raw `AuthCaptureEscrow.capture()`: validates authorization, distributes tokens to receiver | -| + PaymentOperator layer | 77,926 | **+11,561** | Operator dispatch, plugin slot checks, access control: all conditions, hooks, and fee calculator set to `address(0)` | -| + Fee retrieval | 116,980 | **+39,054** | Reads locked fees from `authorizedFees[hash]`, calculates protocol share, accumulates in `accumulatedProtocolFees[token]` | -| + ReceiverCondition | 121,430 | **+4,450** | Pure calldata comparison: `caller == paymentInfo.receiver`: no storage reads | -| + EscrowPeriod condition | 122,520 | **+5,540** | Cross-contract SLOAD: reads `authorizationTime[hash]`, compares against `block.timestamp` | -| + Freeze + AndCondition | 142,961 | **+20,441** | AndCondition combinator loop + `Freeze.check()` reads `frozenUntil[hash]` + internal `isDuringEscrowPeriod()` | +| PaymentOperator, no plugins | 78,074 |: | `bareOperator.capture()`: operator dispatch + escrow `capture()` call | +| + Fee retrieval and distribution | 117,033 | **+38,959** | Reads locked fees from `authorizedFees[hash]`, calculates protocol share, accumulates in `accumulatedProtocolFees[token]` | +| + ReceiverCondition | 121,529 | **+4,496** | Pure calldata comparison: `caller == paymentInfo.receiver`, no storage reads | +| + EscrowPeriod condition | 126,888 | **+5,359** | Cross-contract SLOAD: reads `authorizationTime[hash]`, compares against `block.timestamp` | +| + Freeze + AndCondition | 147,298 | **+20,410** | AndCondition combinator loop + `Freeze.check()` reads `frozenUntil[hash]` + internal `isDuringEscrowPeriod()` | -**Simple conditions are close to free.** `ReceiverCondition` and `PayerCondition` cost around 4,500 gas; they only compare calldata fields. Cross-contract conditions like `EscrowPeriod` cost around 5,500 because of a cold SLOAD. The `Freeze` condition is the most expensive single condition (+20,441) because of the AndCondition combinator overhead, its own `frozenUntil` storage read, and an internal escrow period check. +**Simple conditions are close to free.** `ReceiverCondition` and `PayerCondition` cost around 4,500 gas; they only compare calldata fields. Cross-contract conditions like `EscrowPeriod` cost around 5,400 because of a cold SLOAD. The `Freeze` condition is the most expensive single condition (+20,410) because of the AndCondition combinator overhead, its own `frozenUntil` storage read, and an internal escrow period check. ## Dispute Path @@ -83,9 +82,9 @@ The refund request, evidence submission, and arbiter approval can all happen off | On-chain step | Gas | vs transfer | Who Calls | |--------------|-----|------------|-----------| -| `freeze()` | 44,651 | 4.3x | Buyer | -| `void()` | 65,924 | 6.4x | Anyone | -| **Total** | **110,575** | **10.7x** | | +| `freeze()` | 45,831 | 4.5x | Buyer | +| `void()` | 68,947 | 6.7x | Anyone | +| **Total** | **114,778** | **11.2x** | | Total dispute cost on Base with off-chain resolution: **< $0.005**. @@ -95,19 +94,19 @@ If the parties choose to handle the dispute fully on-chain instead: | Operation | Gas | vs transfer | Who Calls | Notes | |-----------|-----|------------|-----------|-------| -| `authorize()` | 181,544 | 17.6x | Facilitator | Already paid during happy path | -| `freeze()` | 44,651 | 4.3x | Buyer | Locks payment during escrow window | -| `capture()` | 150,262 | 14.6x | Anyone | Already paid during happy path | -| `requestRefund()` | 421,689 | 40.9x | Buyer | Creates refund request with multi-index storage | -| `submitEvidence()` | 135,597 | 13.2x | Any party | Stores IPFS CID on-chain | -| `approveWithSignature()` | 89,935 | 8.7x | Anyone | Relays arbiter's off-chain EIP-712 signature | -| `refund()` | 54,467 | 5.3x | Anyone | Pulls funds from merchant wallet via ReceiverRefundCollector | -| **Total** | **1,078,145** | **104.6x** | | | +| `authorize()` | 182,445 | 17.8x | Facilitator | Already paid during happy path | +| `freeze()` | 45,818 | 4.5x | Buyer | Locks payment during escrow window | +| `capture()` | 145,549 | 14.2x | Anyone | Already paid during happy path | +| `requestRefund()` | 418,174 | 40.7x | Buyer | Creates refund request with multi-index storage | +| `submitEvidence()` | 132,431 | 12.9x | Any party | Stores IPFS CID on-chain | +| `deny()` | 11,096 | 1.1x | Arbiter | Terminal status update on the request | +| `refund()` | 57,482 | 5.6x | Anyone | Pulls funds from merchant wallet via ReceiverRefundCollector | +| **Total** | **992,995** | **96.7x** | | | -This total includes the happy path steps (`authorize` + `capture`) since those already ran. The dispute-only overhead is 746,339 gas (< $0.02 on Base). +This total includes the happy path steps (`authorize` + `capture`) since those already ran. The dispute-only overhead is 665,001 gas (< $0.02 on Base). -`requestRefund()` at 421,689 gas is the most expensive operation because it writes to **five storage mappings** for indexing: +`requestRefund()` at 418,174 gas is the most expensive operation because it writes to **five storage mappings** for indexing: - Refund request data (status, amount, payment hash) - Payer index (`payerRefundRequests[payer][n]`) @@ -122,14 +121,13 @@ This indexing enables efficient off-chain queries but costs more gas upfront. On | Scenario | Gas | vs transfer | Cost on Base | |----------|-----|------------|-------------| -| ERC-20 transfer (baseline) | 10,305 | 1x | < $0.001 | -| Commerce Payments escrow, no operator (`authorize` + `capture`) | 144,718 | 14.0x | < $0.005 | -| + PaymentOperator layer, no plugins | 195,176 | 18.9x | < $0.005 | -| + fees | 252,941 | 24.5x | < $0.005 | -| + fees + simple condition | 257,391 | 25.0x | < $0.005 | -| **+ fees + EscrowPeriod + Freeze (x402r full)** | **331,806** | **32.2x** | **< $0.01** | -| x402r dispute (off-chain optimized) | 110,575 | 10.7x | < $0.005 | -| x402r dispute (fully on-chain, 7 txns) | 1,078,145 | 104.6x | < $0.05 | +| ERC-20 transfer (cold baseline) | 10,263 | 1x | < $0.001 | +| PaymentOperator, no plugins (`authorize` + `capture`) | 197,092 | 19.2x | < $0.005 | +| + fees | 263,771 | 25.7x | < $0.005 | +| + fees + simple condition | 268,267 | 26.1x | < $0.005 | +| **+ fees + EscrowPeriod + Freeze (x402r full)** | **332,489** | **32.4x** | **< $0.01** | +| x402r dispute (off-chain optimized) | 114,778 | 11.2x | < $0.005 | +| x402r dispute (fully on-chain, 7 txns) | 992,995 | 96.7x | < $0.05 | The full x402r happy path uses ~32x the gas of a single ERC-20 transfer, but on Base L2, the absolute cost stays under a penny. The overhead comes from escrow validation, fee locking, cross-contract storage writes, and condition checks, all detailed in the per-plugin breakdown above. @@ -137,6 +135,17 @@ The full x402r happy path uses ~32x the gas of a single ERC-20 transfer, but on All numbers above assume one payment per transaction. Batching operations in a single transaction (via a multicall contract) can reduce per-payment costs by 37 to 80 percent thanks to warm EVM access; contract addresses and shared storage only load once. The benchmark test includes warm measurements for reference. +## Reproducing these numbers + +```bash +git clone https://github.com/BackTrackCo/x402r-contracts +cd x402r-contracts +git checkout bb188dbc0251f9a3af7da57906d5c59e2b2a14d0 +forge test --match-path test/gas/GasBenchmark.t.sol -vv +``` + +Each test logs its measured gas via `console.log` and the `--gas-report` summary tables list aggregate per-function statistics. + How the operator calculates and distributes protocol and operator fees diff --git a/contracts/periphery/auth-capture-escrow.mdx b/contracts/periphery/auth-capture-escrow.mdx index 9e32260..3d18a00 100644 --- a/contracts/periphery/auth-capture-escrow.mdx +++ b/contracts/periphery/auth-capture-escrow.mdx @@ -178,4 +178,4 @@ Collects ERC-20 tokens through Uniswap Permit2 `permitTransferFrom`. The operato The client signs a Permit2 `PermitTransferFrom`; the deterministic nonce binds the merchant address, removing the need for a separate witness struct. -See the [auth-capture Scheme Specification](/x402-integration/auth-capture-scheme) for the full Permit2 wire format. +See the [auth-capture wire format](/x402-integration/auth-capture/wire-format) for the full Permit2 wire format. diff --git a/docs.json b/docs.json index 36e8d95..8e9758d 100644 --- a/docs.json +++ b/docs.json @@ -23,16 +23,14 @@ { "group": "auth-capture Scheme", "pages": [ - "x402-integration/auth-capture-scheme" + "x402-integration/auth-capture/index", + "x402-integration/auth-capture/wire-format", + "x402-integration/auth-capture/verification-and-settlement", + "x402-integration/auth-capture/payment-info" ] - } - ] - }, - { - "tab": "Smart Contracts", - "groups": [ + }, { - "group": "Overview", + "group": "Contracts", "pages": [ "contracts/overview", "contracts/architecture", @@ -40,10 +38,7 @@ "contracts/payment-operator", "contracts/factories", "contracts/fees", - "contracts/gas-costs", - "contracts/examples", - "contracts/audits", - "contracts/license" + "contracts/examples" ] }, { @@ -93,10 +88,9 @@ ] }, { - "group": "Marketplace", + "group": "Merchants", "pages": [ "sdk/merchant/getting-started", - "sdk/helpers/forward-to-arbiter", "sdk/merchant/quickstart", "sdk/merchant/refund-handling" ] @@ -105,18 +99,32 @@ "group": "Delivery Protection", "pages": [ "sdk/delivery-protection", + "sdk/helpers/forward-to-arbiter", "sdk/delivery-merchant", "sdk/delivery-arbiter" ] }, { - "group": "Reference", + "group": "Tools", "pages": [ "sdk/cli", "sdk/examples" ] } ] + }, + { + "tab": "Resources", + "groups": [ + { + "group": "Resources", + "pages": [ + "contracts/gas-costs", + "contracts/audits", + "contracts/license" + ] + } + ] } ] }, @@ -164,7 +172,11 @@ "redirects": [ { "source": "/x402-integration/escrow-scheme", - "destination": "/x402-integration/auth-capture-scheme" + "destination": "/x402-integration/auth-capture" + }, + { + "source": "/x402-integration/auth-capture-scheme", + "destination": "/x402-integration/auth-capture" }, { "source": "/contracts/periphery/refund-request", @@ -176,7 +188,7 @@ }, { "source": "/x402-integration/comparison", - "destination": "/x402-integration/auth-capture-scheme" + "destination": "/x402-integration/auth-capture" } ] } \ No newline at end of file diff --git a/x402-integration/auth-capture-scheme.mdx b/x402-integration/auth-capture-scheme.mdx deleted file mode 100644 index dd22a2d..0000000 --- a/x402-integration/auth-capture-scheme.mdx +++ /dev/null @@ -1,428 +0,0 @@ ---- -title: "auth-capture Scheme Specification" -description: "Technical specification for the x402 auth-capture payment scheme" -icon: "file-contract" ---- - -## Overview - -The **`auth-capture` scheme** for x402 v2 uses the audited [Commerce Payments Protocol](https://github.com/base/commerce-payments) (`AuthCaptureEscrow` + token collectors) directly, no fork. The client signs a single signature (ERC-3009 or Permit2). The facilitator submits it, either locking funds in escrow for later capture (two-phase) or sending them directly to the receiver with refund capability (single-shot). - -Unlike `exact`, which has no mechanism for returning funds, `auth-capture` supports returning funds to the client through void, refund, and reclaim. - -## 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, or refund. Client can reclaim if the capture deadline passes. | -| `true` | Single-shot. Funds sent directly to the receiver. CaptureAuthorizer can refund post-settlement. | - -### Two-phase (`autoCapture: false`, default) - -``` -AUTHORIZE -> RESOURCE DELIVERED -> CAPTURE / VOID -> (REFUND) -``` - - - - The facilitator submits the client's authorization, locking funds in escrow via `AuthCaptureEscrow.authorize()`. The token collector executes the client's signature (ERC-3009 `receiveWithAuthorization` or Permit2 `permitTransferFrom`) to pull tokens into escrow. - - - - Server returns the resource (HTTP 200). - - - - The captureAuthorizer can capture (capture funds to the receiver) or void (return escrowed funds to the client). Capture conditions are policy-defined per captureAuthorizer (time-locked, arbiter-approved, etc.). - - - - If `captureDeadline` passes without capture, the client can reclaim funds directly from the escrow without captureAuthorizer involvement. - - - - After capture, the captureAuthorizer can refund within the `refundDeadline` window. - - - -### Single-shot (`autoCapture: true`) - -``` -CHARGE -> RESOURCE DELIVERED -> (REFUND) -``` - - - - The facilitator submits the client's authorization, sending funds directly to the receiver via `AuthCaptureEscrow.charge()`. No escrow hold. - - - - Server returns the resource (HTTP 200). - - - - The captureAuthorizer can refund within the `refundDeadline` window. - - - -No capture, void, or reclaim, funds are never held in escrow. - -## Visual Flow - -### Exact Payment (Immediate Settlement) - -```mermaid -sequenceDiagram - participant Client - participant Server - participant Receiver - - Client->>Server: 1. Payment + Signature - Server->>Receiver: 2. Immediate Transfer - Server->>Client: 3. Deliver Resource - - Note over Client,Receiver: No recourse after payment - Payment is final -``` - -### auth-capture (Two-phase) - -```mermaid -sequenceDiagram - participant Client - participant Server - participant Facilitator - participant Escrow as AuthCaptureEscrow - participant Receiver - - Client->>Server: GET /resource - Server-->>Client: 402 PaymentRequired - Note over Client: Signs ERC-3009 or Permit2 - - Client->>Server: PaymentPayload with signature - Server->>Facilitator: verify + settle - Facilitator->>Escrow: authorize(paymentInfo, amount, tokenCollector, signature) - Escrow->>Escrow: Lock funds - Facilitator-->>Server: Settlement confirmed - Server-->>Client: 200 OK + resource - - Note over Facilitator,Escrow: Later: captureAuthorizer acts based on policy - - alt Successful completion - Facilitator->>Escrow: capture(paymentInfo, amount, feeBps, feeReceiver) - Escrow->>Receiver: Transfer funds (minus fees) - else Void (full return from escrow) - Facilitator->>Escrow: void(paymentInfo) - Escrow->>Client: Return to payer - else Capture deadline passed - Client->>Escrow: reclaim(paymentInfo) - Escrow->>Client: Return to payer (no captureAuthorizer needed) - end -``` - -### Key Differences - -| Aspect | Exact | auth-capture | -|---|---|---| -| **Settlement** | Immediate on request | Via escrow (two-phase) or direct with refund (single-shot) | -| **Payer Protection** | None (payment final) | Refundable in both paths | -| **Resource Delivery** | After payment clears | Immediately after authorization | -| **Recourse** | No recourse | Reclaim after capture deadline, refund via captureAuthorizer | -| **Fee System** | None | Configurable (min/max bounds, client-signed) | -| **Use Case** | Trusted, low-value, instant | High-value, variable cost, disputes | - -## CaptureAuthorizer - -The **captureAuthorizer** is the address that may call `authorize`, `capture`, `void`, `refund`, or `charge` on a payment. The escrow contract gates those operations on `msg.sender`. In x402's facilitator-submits flow that means either the facilitator's EOA, or any smart contract that ends up calling the escrow (for example, an arbiter contract with dispute logic or a multisig). - -| Use Case | CaptureAuthorizer | -|---|---| -| Session billing | EOA that tracks usage off-chain, captures periodically | -| Time-locked escrow | Contract that releases after a period expires | -| Dispute resolution | Arbiter contract that decides capture vs refund | -| Immediate (exact-like) | Facilitator with `autoCapture: true` for instant settlement | -| Streaming payments | Contract that performs time-proportional captures | - -## Message Format - -### PaymentRequirements (402 Response) - -Server sends this to request payment: - -```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" - } - }] -} -``` - -A server MAY list more than one `accepts[]` entry with different `assetTransferMethod` values so clients can pick the method matching their token approvals. - -### PaymentPayload: EIP-3009 (default) - -Client sends this with a signed ERC-3009 authorization: - -```json -{ - "x402Version": 2, - "resource": { - "url": "https://api.example.com/resource", - "method": "GET" - }, - "accepted": { - "scheme": "auth-capture", - "network": "eip155:8453", - "amount": "1000000", - "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", - "payTo": "0xReceiverAddress", - "maxTimeoutSeconds": 60, - "extra": { "..." } - }, - "payload": { - "authorization": { - "from": "0xPayerAddress", - "to": "0xEIP3009TokenCollectorAddress", - "value": "1000000", - "validAfter": "0", - "validBefore": "1740675754", - "nonce": "0xf374...3480" - }, - "signature": "0x2d6a...571c", - "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc" - } -} -``` - -### PaymentPayload: Permit2 - -When `extra.assetTransferMethod === "permit2"`, the client signs a Permit2 `PermitTransferFrom`: - -```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": "11021048692073456...", - "deadline": "1740675754" - }, - "signature": "0x2d6a...571c", - "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc" - } -} -``` - -The deterministic nonce binds the merchant address (no witness struct). - -## Field Reference - -### Required Extra Fields - -| Field | Type | Description | -|---|---|---| -| `name` | string | EIP-712 token-domain name (for example, `"USDC"`). Used for ERC-3009 signing only. | -| `version` | string | EIP-712 token-domain version (for example, `"2"`). | -| `captureAuthorizer` | address | Address that may call `authorize`, `capture`, `void`, `refund`, or `charge`. Committed on-chain as `PaymentInfo.operator`. | -| `captureDeadline` | uint48 | Absolute Unix seconds: capture must occur before this. Encoded as `authorizationExpiry`. | -| `refundDeadline` | uint48 | Absolute Unix seconds: refunds allowed until this. Encoded as `refundExpiry`. | -| `feeRecipient` | address | Fee recipient. Set to `address(0)` to let the captureAuthorizer specify any non-zero recipient at capture/charge time. | -| `minFeeBps` | uint16 | Lowest fee in basis points the captureAuthorizer must take. `0` = no floor. | -| `maxFeeBps` | uint16 | Highest fee in basis points the captureAuthorizer can take. | - -### Optional Extra Fields - -| Field | Type | Description | Default | -|---|---|---|---| -| `autoCapture` | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). | `false` | -| `assetTransferMethod` | `"eip3009"` \| `"permit2"` | Which token collector to use. | `"eip3009"` | - - -**Fee Configuration:** The escrow enforces fees on-chain via the `PaymentInfo` struct. The escrow rejects captures/charges that fall outside `[minFeeBps, maxFeeBps]`. If `feeRecipient` is non-zero, the actual fee recipient at capture/charge must match. - - -## Nonce Derivation - -The signature nonce is the payer-agnostic `PaymentInfo` hash. The encoding zeros out the payer; every other field carries the value that will appear on-chain. - -``` -paymentInfoHash = keccak256(abi.encode(PAYMENT_INFO_TYPEHASH, paymentInfoWithZeroPayer)) -nonce = keccak256(abi.encode(chainId, AUTH_CAPTURE_ESCROW_ADDRESS, paymentInfoHash)) -``` - -The `salt` field enforces freshness: 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**: Payload matches `Eip3009Payload` or `Permit2Payload` (includes `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**: All required `extra` fields present. -5. **Method routing**: `extra.assetTransferMethod` (default `"eip3009"`) matches the payload shape. -6. **Deadline ordering**: `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and the payload's `validBefore` (EIP-3009) or `deadline` (Permit2) `<= captureDeadline`. -7. **Time window**: `validBefore` / `deadline > now + 6s` (not expired) and `validAfter <= now` (active, EIP-3009 only). -8. **Spender / collector match**: `authorization.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `permit2Authorization.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). -9. **Token match**: `permit2Authorization.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 amount matches `requirements.amount`. -12. **Nonce match**: Reconstruct `PaymentInfo` from extra + salt + payer + requirements; recompute the 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). -13. **Simulate**: Call `AuthCaptureEscrow.authorize(...)` or `.charge(...)` via `eth_call` to verify 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 (catch 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**: `AuthCaptureEscrow.(paymentInfo, amount, tokenCollector, collectorData)`. -6. **Wait for receipt**: 60s timeout. -7. **Return result**: tx hash, network, payer. - -## PaymentInfo Struct - -This is the on-chain Solidity struct. The JSON payload omits the `payer` field; the facilitator recovers it from the signature at settlement time. Wire-format `extra` uses spec-level field names; the on-chain struct keeps canonical names so the EIP-712 typehash matches the AuthCaptureEscrow contract byte-for-byte. - -```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; allows `reclaim()` | -| `refundExpiry` | `refundDeadline` | `refund()` | Blocks refund requests | - -## Safety Guarantees - -The escrow contract enforces invariants on-chain: - - - - The client-signed `maxAmount` caps the settlement amount. Attempts to exceed the limit revert. - - - - Each payment has a unique nonce derived from `(chainId, escrowAddress, paymentInfoHash)`. The nonce is consumed on-chain at settlement. - - - - After `captureDeadline`, the payer can reclaim escrowed funds directly without captureAuthorizer approval. - - - - Min/max fee bounds in `PaymentInfo` are client-signed and enforced on-chain. The captureAuthorizer must respect these limits. - - - - -**CaptureAuthorizer Trust Required:** The captureAuthorizer controls when and how much to capture. Choose with intent and understand the capture policy. See [PaymentOperator](/contracts/payment-operator) for examples. - - -## Error 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. | - -### Settlement Errors - -| Error Code | Description | -|---|---| -| `verification_failed` | Re-verification before settlement failed. | -| `transaction_reverted` | On-chain transaction reverted after confirmation. | - -## vs Exact Scheme - -The `auth-capture` scheme adds an authorization step before settlement (or refundability for single-shot). For simple immediate payments where trust and refundability aren't concerns, the `exact` scheme remains more efficient. - -## Next Steps - - - - Understand why HTTP payments need escrow. - - - - Learn about the escrow and captureAuthorizer contracts. - - - - Build your first auth-capture payment flow. - - - -## References - -- [Commerce Payments Protocol](https://blog.base.dev/commerce-payments-protocol) -- [AuthCaptureEscrow Contract](https://github.com/base/commerce-payments) -- [EIP-3009: Transfer With Authorization](https://eips.ethereum.org/EIPS/eip-3009) -- [Uniswap Permit2](https://docs.uniswap.org/contracts/permit2/overview) -- [x402r auth-capture Scheme Reference Implementation](https://github.com/BackTrackCo/x402r-scheme) diff --git a/x402-integration/auth-capture/index.mdx b/x402-integration/auth-capture/index.mdx new file mode 100644 index 0000000..4f6a611 --- /dev/null +++ b/x402-integration/auth-capture/index.mdx @@ -0,0 +1,177 @@ +--- +title: "auth-capture Scheme" +description: "Concept, flow diagrams, and captureAuthorizer model for the x402 auth-capture payment scheme" +icon: "file-contract" +--- + +## Overview + +The **`auth-capture` scheme** for x402 v2 uses the audited [Commerce Payments Protocol](https://github.com/base/commerce-payments) (`AuthCaptureEscrow` + token collectors) directly, no fork. The client signs a single signature (ERC-3009 or Permit2). The facilitator submits it, either locking funds in escrow for later capture (two-phase) or sending them directly to the receiver with refund capability (single-shot). + +Unlike `exact`, which has no mechanism for returning funds, `auth-capture` supports returning funds to the client through void, refund, and reclaim. + +## 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, or refund. Client can reclaim if the capture deadline passes. | +| `true` | Single-shot. Funds sent directly to the receiver. CaptureAuthorizer can refund post-settlement. | + +### Two-phase (`autoCapture: false`, default) + +``` +AUTHORIZE -> RESOURCE DELIVERED -> CAPTURE / VOID -> (REFUND) +``` + + + + The facilitator submits the client's authorization, locking funds in escrow via `AuthCaptureEscrow.authorize()`. The token collector executes the client's signature (ERC-3009 `receiveWithAuthorization` or Permit2 `permitTransferFrom`) to pull tokens into escrow. + + + + Server returns the resource (HTTP 200). + + + + The captureAuthorizer can capture (capture funds to the receiver) or void (return escrowed funds to the client). Capture conditions are policy-defined per captureAuthorizer (time-locked, arbiter-approved, etc.). + + + + If `captureDeadline` passes without capture, the client can reclaim funds directly from the escrow without captureAuthorizer involvement. + + + + After capture, the captureAuthorizer can refund within the `refundDeadline` window. + + + +### Single-shot (`autoCapture: true`) + +``` +CHARGE -> RESOURCE DELIVERED -> (REFUND) +``` + + + + The facilitator submits the client's authorization, sending funds directly to the receiver via `AuthCaptureEscrow.charge()`. No escrow hold. + + + + Server returns the resource (HTTP 200). + + + + The captureAuthorizer can refund within the `refundDeadline` window. + + + +No capture, void, or reclaim, funds are never held in escrow. + +## Visual Flow + +### Exact Payment (Immediate Settlement) + +```mermaid +sequenceDiagram + participant Client + participant Server + participant Receiver + + Client->>Server: 1. Payment + Signature + Server->>Receiver: 2. Immediate Transfer + Server->>Client: 3. Deliver Resource + + Note over Client,Receiver: No recourse after payment - Payment is final +``` + +### auth-capture (Two-phase) + +```mermaid +sequenceDiagram + participant Client + participant Server + participant Facilitator + participant Escrow as AuthCaptureEscrow + participant Receiver + + Client->>Server: GET /resource + Server-->>Client: 402 PaymentRequired + Note over Client: Signs ERC-3009 or Permit2 + + Client->>Server: PaymentPayload with signature + Server->>Facilitator: verify + settle + Facilitator->>Escrow: authorize(paymentInfo, amount, tokenCollector, signature) + Escrow->>Escrow: Lock funds + Facilitator-->>Server: Settlement confirmed + Server-->>Client: 200 OK + resource + + Note over Facilitator,Escrow: Later: captureAuthorizer acts based on policy + + alt Successful completion + Facilitator->>Escrow: capture(paymentInfo, amount, feeBps, feeReceiver) + Escrow->>Receiver: Transfer funds (minus fees) + else Void (full return from escrow) + Facilitator->>Escrow: void(paymentInfo) + Escrow->>Client: Return to payer + else Capture deadline passed + Client->>Escrow: reclaim(paymentInfo) + Escrow->>Client: Return to payer (no captureAuthorizer needed) + end +``` + +### Key Differences + +| Aspect | Exact | auth-capture | +|---|---|---| +| **Settlement** | Immediate on request | Via escrow (two-phase) or direct with refund (single-shot) | +| **Payer Protection** | None (payment final) | Refundable in both paths | +| **Resource Delivery** | After payment clears | Immediately after authorization | +| **Recourse** | No recourse | Reclaim after capture deadline, refund via captureAuthorizer | +| **Fee System** | None | Configurable (min/max bounds, client-signed) | +| **Use Case** | Trusted, low-value, instant | High-value, variable cost, disputes | + +## CaptureAuthorizer + +The **captureAuthorizer** is the address that may call `authorize`, `capture`, `void`, `refund`, or `charge` on a payment. The escrow contract gates those operations on `msg.sender`. In x402's facilitator-submits flow that means either the facilitator's EOA, or any smart contract that ends up calling the escrow (for example, an arbiter contract with dispute logic or a multisig). + +| Use Case | CaptureAuthorizer | +|---|---| +| Session billing | EOA that tracks usage off-chain, captures periodically | +| Time-locked escrow | Contract that releases after a period expires | +| Dispute resolution | Arbiter contract that decides capture vs refund | +| Immediate (exact-like) | Facilitator with `autoCapture: true` for instant settlement | +| Streaming payments | Contract that performs time-proportional captures | + +## vs Exact Scheme + +The `auth-capture` scheme adds an authorization step before settlement (or refundability for single-shot). For simple immediate payments where trust and refundability aren't concerns, the `exact` scheme remains more efficient. + +## Next Steps + + + + PaymentRequirements and PaymentPayload shapes for EIP-3009 and Permit2. + + + + The 13-step verification flow, settlement logic, and error codes. + + + + On-chain struct, expiry ordering, and safety guarantees. + + + + Build your first auth-capture payment flow. + + + +## References + +- [Commerce Payments Protocol](https://blog.base.dev/commerce-payments-protocol) +- [AuthCaptureEscrow Contract](https://github.com/base/commerce-payments) +- [EIP-3009: Transfer With Authorization](https://eips.ethereum.org/EIPS/eip-3009) +- [Uniswap Permit2](https://docs.uniswap.org/contracts/permit2/overview) +- [x402r auth-capture Scheme Reference Implementation](https://github.com/BackTrackCo/x402r-scheme) diff --git a/x402-integration/auth-capture/payment-info.mdx b/x402-integration/auth-capture/payment-info.mdx new file mode 100644 index 0000000..57352ad --- /dev/null +++ b/x402-integration/auth-capture/payment-info.mdx @@ -0,0 +1,72 @@ +--- +title: "PaymentInfo Struct" +description: "On-chain PaymentInfo struct, expiry ordering, and safety guarantees" +icon: "cube" +--- + +This is the on-chain Solidity struct. The JSON payload omits the `payer` field; the facilitator recovers it from the signature at settlement time. Wire-format `extra` uses spec-level field names; the on-chain struct keeps canonical names so the EIP-712 typehash matches the AuthCaptureEscrow contract byte-for-byte. + +```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; allows `reclaim()` | +| `refundExpiry` | `refundDeadline` | `refund()` | Blocks refund requests | + +## Safety Guarantees + +The escrow contract enforces invariants on-chain: + + + + The client-signed `maxAmount` caps the settlement amount. Attempts to exceed the limit revert. + + + + Each payment has a unique nonce derived from `(chainId, escrowAddress, paymentInfoHash)`. The nonce is consumed on-chain at settlement. + + + + After `captureDeadline`, the payer can reclaim escrowed funds directly without captureAuthorizer approval. + + + + Min/max fee bounds in `PaymentInfo` are client-signed and enforced on-chain. The captureAuthorizer must respect these limits. + + + + +**CaptureAuthorizer Trust Required:** The captureAuthorizer controls when and how much to capture. Choose with intent and understand the capture policy. See [PaymentOperator](/contracts/payment-operator) for examples. + + +## Next Steps + + + + Where each PaymentInfo field comes from on the wire. + + + + The 13-step verification flow that enforces these invariants. + + diff --git a/x402-integration/auth-capture/verification-and-settlement.mdx b/x402-integration/auth-capture/verification-and-settlement.mdx new file mode 100644 index 0000000..d42940d --- /dev/null +++ b/x402-integration/auth-capture/verification-and-settlement.mdx @@ -0,0 +1,83 @@ +--- +title: "Verification and Settlement" +description: "Facilitator verification flow, settlement logic, EIP-6492 wallets, and error codes" +icon: "shield-check" +--- + +## Verification Logic + +The facilitator performs these checks in order: + +1. **Type guard**: Payload matches `Eip3009Payload` or `Permit2Payload` (includes `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**: All required `extra` fields present. +5. **Method routing**: `extra.assetTransferMethod` (default `"eip3009"`) matches the payload shape. +6. **Deadline ordering**: `refundDeadline >= captureDeadline`, `captureDeadline > now + 6s`, and the payload's `validBefore` (EIP-3009) or `deadline` (Permit2) `<= captureDeadline`. +7. **Time window**: `validBefore` / `deadline > now + 6s` (not expired) and `validAfter <= now` (active, EIP-3009 only). +8. **Spender / collector match**: `authorization.to === EIP3009_TOKEN_COLLECTOR_ADDRESS` (EIP-3009) or `permit2Authorization.spender === PERMIT2_TOKEN_COLLECTOR_ADDRESS` (Permit2). +9. **Token match**: `permit2Authorization.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 amount matches `requirements.amount`. +12. **Nonce match**: Reconstruct `PaymentInfo` from extra + salt + payer + requirements; recompute the 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). +13. **Simulate**: Call `AuthCaptureEscrow.authorize(...)` or `.charge(...)` via `eth_call` to verify success. + +The `SAFETY_MARGIN_SECONDS` constant is `6`, which is why deadline comparisons use `now + 6s`. + +### 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 (catch 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**: `AuthCaptureEscrow.(paymentInfo, amount, tokenCollector, collectorData)`. +6. **Wait for receipt**: 60s timeout. +7. **Return result**: tx hash, network, payer. + +## Error 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. | + +### Settlement Errors + +| Error Code | Description | +|---|---| +| `verification_failed` | Re-verification before settlement failed. | +| `transaction_reverted` | On-chain transaction reverted after confirmation. | + +## Next Steps + + + + PaymentRequirements and PaymentPayload shapes. + + + + On-chain struct, expiry ordering, and safety guarantees. + + diff --git a/x402-integration/auth-capture/wire-format.mdx b/x402-integration/auth-capture/wire-format.mdx new file mode 100644 index 0000000..eaf24a9 --- /dev/null +++ b/x402-integration/auth-capture/wire-format.mdx @@ -0,0 +1,149 @@ +--- +title: "Wire Format" +description: "PaymentRequirements, PaymentPayload, and Extra-field reference for the auth-capture scheme" +icon: "file-code" +--- + +## PaymentRequirements (402 Response) + +Server sends this to request payment: + +```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" + } + }] +} +``` + +A server MAY list more than one `accepts[]` entry with different `assetTransferMethod` values so clients can pick the method matching their token approvals. + +## PaymentPayload: EIP-3009 (default) + +Client sends this with a signed ERC-3009 authorization: + +```json +{ + "x402Version": 2, + "resource": { + "url": "https://api.example.com/resource", + "method": "GET" + }, + "accepted": { + "scheme": "auth-capture", + "network": "eip155:8453", + "amount": "1000000", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "payTo": "0xReceiverAddress", + "maxTimeoutSeconds": 60, + "extra": { "..." } + }, + "payload": { + "authorization": { + "from": "0xPayerAddress", + "to": "0xEIP3009TokenCollectorAddress", + "value": "1000000", + "validAfter": "0", + "validBefore": "1740675754", + "nonce": "0xf374...3480" + }, + "signature": "0x2d6a...571c", + "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc" + } +} +``` + +## PaymentPayload: Permit2 + +When `extra.assetTransferMethod === "permit2"`, the client signs a Permit2 `PermitTransferFrom`: + +```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": "11021048692073456...", + "deadline": "1740675754" + }, + "signature": "0x2d6a...571c", + "salt": "0x0000000000000000000000000000000000000000000000000000000000000abc" + } +} +``` + +The deterministic nonce binds the merchant address (no witness struct). + +## Field Reference + +### Required Extra Fields + +| Field | Type | Description | +|---|---|---| +| `name` | string | EIP-712 token-domain name (for example, `"USDC"`). Used for ERC-3009 signing only. | +| `version` | string | EIP-712 token-domain version (for example, `"2"`). | +| `captureAuthorizer` | address | Address that may call `authorize`, `capture`, `void`, `refund`, or `charge`. Committed on-chain as `PaymentInfo.operator`. | +| `captureDeadline` | uint48 | Absolute Unix seconds: capture must occur before this. Encoded as `authorizationExpiry`. | +| `refundDeadline` | uint48 | Absolute Unix seconds: refunds allowed until this. Encoded as `refundExpiry`. | +| `feeRecipient` | address | Fee recipient. Set to `address(0)` to let the captureAuthorizer specify any non-zero recipient at capture/charge time. | +| `minFeeBps` | uint16 | Lowest fee in basis points the captureAuthorizer must take. `0` = no floor. | +| `maxFeeBps` | uint16 | Highest fee in basis points the captureAuthorizer can take. | + +### Optional Extra Fields + +| Field | Type | Description | Default | +|---|---|---|---| +| `autoCapture` | `bool` | `true` → facilitator calls `charge()` (atomic). `false` → `authorize()` (two-phase). | `false` | +| `assetTransferMethod` | `"eip3009"` \| `"permit2"` | Which token collector to use. | `"eip3009"` | + + +**Fee Configuration:** The escrow enforces fees on-chain via the `PaymentInfo` struct. The escrow rejects captures/charges that fall outside `[minFeeBps, maxFeeBps]`. If `feeRecipient` is non-zero, the actual fee recipient at capture/charge must match. + + +## Nonce Derivation + +The signature nonce is the payer-agnostic `PaymentInfo` hash. The encoding zeros out the payer; every other field carries the value that will appear on-chain. + +``` +paymentInfoHash = keccak256(abi.encode(PAYMENT_INFO_TYPEHASH, paymentInfoWithZeroPayer)) +nonce = keccak256(abi.encode(chainId, AUTH_CAPTURE_ESCROW_ADDRESS, paymentInfoHash)) +``` + +The `salt` field enforces freshness: each signing call generates a fresh `bytes32` salt, so two payers signing concurrently produce distinct nonces with no collision risk. + +## Next Steps + + + + The 13-step verification flow and error codes. + + + + How wire fields map to the on-chain struct. + + diff --git a/x402-integration/overview.mdx b/x402-integration/overview.mdx index 0d87073..bbe8877 100644 --- a/x402-integration/overview.mdx +++ b/x402-integration/overview.mdx @@ -205,7 +205,7 @@ Smart contract that controls capture/void logic. Different operators enable diff ## Next Steps - + Complete technical specification for the auth-capture payment scheme. From e419b0bbea74d7e4b218ab6386766cb258b7b7de Mon Sep 17 00:00:00 2001 From: A1igator Date: Sun, 31 May 2026 18:16:56 -0400 Subject: [PATCH 34/37] auth-capture docs: point client signing at upstreamed @x402/evm package The client half of the auth-capture scheme now ships in the x402 monorepo as AuthCaptureEvmScheme on the @x402/evm/auth-capture/client subpath. Add a "Signing the payload (client)" section to wire-format showing the canonical x402Client.register + wrapFetchWithPayment flow, and link the upstream client implementation from the scheme References. Co-Authored-By: Claude Opus 4.8 (1M context) --- x402-integration/auth-capture/index.mdx | 1 + x402-integration/auth-capture/wire-format.mdx | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/x402-integration/auth-capture/index.mdx b/x402-integration/auth-capture/index.mdx index 4f6a611..49fe492 100644 --- a/x402-integration/auth-capture/index.mdx +++ b/x402-integration/auth-capture/index.mdx @@ -174,4 +174,5 @@ The `auth-capture` scheme adds an authorization step before settlement (or refun - [AuthCaptureEscrow Contract](https://github.com/base/commerce-payments) - [EIP-3009: Transfer With Authorization](https://eips.ethereum.org/EIPS/eip-3009) - [Uniswap Permit2](https://docs.uniswap.org/contracts/permit2/overview) +- [auth-capture client scheme (`@x402/evm/auth-capture/client`)](https://github.com/x402-foundation/x402/tree/main/typescript/packages/mechanisms/evm/src/auth-capture) - [x402r auth-capture Scheme Reference Implementation](https://github.com/BackTrackCo/x402r-scheme) diff --git a/x402-integration/auth-capture/wire-format.mdx b/x402-integration/auth-capture/wire-format.mdx index eaf24a9..e286033 100644 --- a/x402-integration/auth-capture/wire-format.mdx +++ b/x402-integration/auth-capture/wire-format.mdx @@ -36,6 +36,26 @@ Server sends this to request payment: A server MAY list more than one `accepts[]` entry with different `assetTransferMethod` values so clients can pick the method matching their token approvals. +## Signing the payload (client) + +Clients do not hand-build these payloads. The client half of the scheme ships in the x402 monorepo as `AuthCaptureEvmScheme` on the `@x402/evm/auth-capture/client` subpath. Register it on an `x402Client` and it reads the `extra` fields, reconstructs the PaymentInfo struct, derives the payer-agnostic nonce, and emits the ERC-3009 (default) or Permit2 payload shown below. + +```typescript +import { AuthCaptureEvmScheme } from '@x402/evm/auth-capture/client' +import { x402Client, wrapFetchWithPayment } from '@x402/fetch' +import { privateKeyToAccount } from 'viem/accounts' + +const account = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`) + +const client = new x402Client() +client.register('eip155:*', new AuthCaptureEvmScheme(account)) + +// fetchWithPayment auto-signs any auth-capture 402 it receives +const fetchWithPayment = wrapFetchWithPayment(fetch, client) +``` + +The signer only needs `address` and `signTypedData`, so a bare viem `LocalAccount` works with no `PublicClient`. The scheme selects ERC-3009 or Permit2 from `extra.assetTransferMethod`. + ## PaymentPayload: EIP-3009 (default) Client sends this with a signed ERC-3009 authorization: From 252d6a61fb821bffe378a95d3c749a615e3cb5f8 Mon Sep 17 00:00:00 2001 From: A1igator Date: Sun, 31 May 2026 18:19:36 -0400 Subject: [PATCH 35/37] contracts/examples: fix misleading "No refunds" comments refundPreActionCondition: address(0) is default-allow (anyone can call refund()), not "no refunds". Mirror the void() default-allow wording so the three refund-condition examples match the corrected line above them. Co-Authored-By: Claude Opus 4.8 (1M context) --- contracts/examples.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/examples.mdx b/contracts/examples.mdx index 74f84ea..195850b 100644 --- a/contracts/examples.mdx +++ b/contracts/examples.mdx @@ -529,7 +529,7 @@ const config = { capturePostActionHook: '0x0000000000000000000000000000000000000000', voidPreActionCondition: daoCondition.address, // DAO can refund if needed voidPostActionHook: '0x0000000000000000000000000000000000000000', - refundPreActionCondition: '0x0000000000000000000000000000000000000000', // No refunds + refundPreActionCondition: '0x0000000000000000000000000000000000000000', // Open-access: anyone can call refund() (`address(0)` = default-allow) refundPostActionHook: '0x0000000000000000000000000000000000000000' }; @@ -601,7 +601,7 @@ const config = { capturePostActionHook: '0x0000000000000000000000000000000000000000', voidPreActionCondition: PAYER_CONDITION, // Payer can cancel stream voidPostActionHook: '0x0000000000000000000000000000000000000000', - refundPreActionCondition: '0x0000000000000000000000000000000000000000', // No refunds after charged + refundPreActionCondition: '0x0000000000000000000000000000000000000000', // Open-access: anyone can call refund() (`address(0)` = default-allow) refundPostActionHook: '0x0000000000000000000000000000000000000000' }; @@ -670,7 +670,7 @@ const config = { capturePostActionHook: '0x0000000000000000000000000000000000000000', voidPreActionCondition: RECEIVER_CONDITION,// Receiver can refund (invoice error) voidPostActionHook: '0x0000000000000000000000000000000000000000', - refundPreActionCondition: '0x0000000000000000000000000000000000000000', // No post-delivery refunds + refundPreActionCondition: '0x0000000000000000000000000000000000000000', // Open-access: anyone can call refund() (`address(0)` = default-allow) refundPostActionHook: '0x0000000000000000000000000000000000000000' }; From 1782321d772de8fb5c322fac49a651973a652ab9 Mon Sep 17 00:00:00 2001 From: A1igator Date: Sun, 31 May 2026 18:31:54 -0400 Subject: [PATCH 36/37] docs: defer audits to upstream commerce-payments; reference upstream gas + CI audits.mdx: replace the unsourced audit counts with a link-out to Base's canonical commerce-payments audits/ directory and README Security Audits section, which host the dated report PDFs. Keeps the x402r-specific unaudited disclosure intact. gas-costs.mdx: note the contracts-repo CI job that re-runs GasBenchmark.t.sol and flags drift, and add a reference to the upstream commerce-payments escrow baseline the x402r figures build on. Co-Authored-By: Claude Opus 4.8 (1M context) --- contracts/audits.mdx | 8 ++++---- contracts/gas-costs.mdx | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/contracts/audits.mdx b/contracts/audits.mdx index 54c5505..67581de 100644 --- a/contracts/audits.mdx +++ b/contracts/audits.mdx @@ -10,12 +10,12 @@ x402r extends the canonical [commerce-payments](https://github.com/base/commerce ### What's Audited (Upstream) -Auditors covered the commerce-payments `AuthCaptureEscrow` contract and its supporting infrastructure (TokenCollectors, TokenStore, Permit2 integration): +x402r runs the commerce-payments primitives at their canonical addresses, so their audit coverage applies directly with no fork to re-audit. Base maintains the authoritative, dated report list, defer to it rather than this page: -- **Spearbit** (2 audits) -- **Coinbase Protocol Security** (3 audits) +- [commerce-payments `audits/` directory](https://github.com/base/commerce-payments/tree/main/audits) hosts the report PDFs. +- [Security Audits section](https://github.com/base/commerce-payments#security-audits) of the upstream README lists each audit with its date and report link. -These audits cover the core escrow lifecycle: `authorize`, `capture`, `void`, `reclaim`, and `refund`. The [commerce-payments repository](https://github.com/base/commerce-payments) hosts the audit reports. +As of the latest published list, the `AuthCaptureEscrow` contract and its supporting infrastructure (TokenCollectors, TokenStore, Permit2 integration) were covered by five reports, three from Coinbase Protocol Security and two from Spearbit. These cover the core escrow lifecycle: `authorize`, `capture`, `void`, `reclaim`, and `refund`. ### What's Not Audited diff --git a/contracts/gas-costs.mdx b/contracts/gas-costs.mdx index 105b701..beace94 100644 --- a/contracts/gas-costs.mdx +++ b/contracts/gas-costs.mdx @@ -146,6 +146,14 @@ forge test --match-path test/gas/GasBenchmark.t.sol -vv Each test logs its measured gas via `console.log` and the `--gas-report` summary tables list aggregate per-function statistics. + +A scheduled CI job in [x402r-contracts](https://github.com/BackTrackCo/x402r-contracts) re-runs `GasBenchmark.t.sol` and flags drift past threshold, so the numbers on this page are checked against the live benchmark rather than left to manual regen. When the benchmark moves, the pinned commit and figures above are refreshed. + + +## Reference: upstream escrow costs + +The escrow layer these numbers build on is Base's audited [Commerce Payments Protocol](https://github.com/base/commerce-payments). For the baseline cost of the underlying `AuthCaptureEscrow` lifecycle (`authorize`, `capture`, `void`, `refund`) independent of x402r's condition and hook plugins, see the upstream contracts and their own benchmarks in the [commerce-payments repository](https://github.com/base/commerce-payments). The x402r figures above are the upstream escrow cost plus the per-plugin overhead detailed in the breakdown. + How the operator calculates and distributes protocol and operator fees From c1f8182b97503f897bc505d497d62497682f08c5 Mon Sep 17 00:00:00 2001 From: A1igator Date: Sun, 31 May 2026 23:16:41 -0400 Subject: [PATCH 37/37] docs: dedupe cross-tab content to canonical owners Resolves the cross-tab redundancy map from the fourth-pass review. Each duplicated topic now lives on one canonical page; other pages carry a short pointer instead of a repeated block: - Fee-math worked example -> contracts/fees (trimmed from architecture + payment-operator; also drops a stale 3+2bps variant that disagreed with the canonical 50+250bps example). - Protocol-fee 7-day timelock walkthrough -> contracts/fees (trimmed from architecture + payment-operator). - Condition-singleton address table -> periphery/overview (trimmed from factories). - EscrowPeriod+Freeze composition pattern -> conditions/freeze (trimmed from conditions/escrow-period). - Supported-networks table -> sdk/overview (trimmed from index + deploy-operator). - auth-capture two-phase flow -> auth-capture/index + architecture (trimmed the duplicate sequence diagram from x402-integration/overview). - "What is x402r" -> index; operator slot tables -> payment-operator; deploy preset comparison -> deploy-operator: added pointers, kept role/scoped halves. Kept the homepage hero diagram and the scoped condition/hook slot tables intentionally (landing + on-topic reference), pointing each to its canonical source. broken-links passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- contracts/architecture.mdx | 32 ++------------------------ contracts/conditions/escrow-period.mdx | 8 +------ contracts/conditions/overview.mdx | 2 ++ contracts/factories.mdx | 8 +------ contracts/hooks/overview.mdx | 2 ++ contracts/overview.mdx | 2 +- contracts/payment-operator.mdx | 20 ++-------------- index.mdx | 7 +----- sdk/deploy-operator.mdx | 9 +------- sdk/overview.mdx | 2 ++ x402-integration/overview.mdx | 23 +----------------- 11 files changed, 16 insertions(+), 99 deletions(-) diff --git a/contracts/architecture.mdx b/contracts/architecture.mdx index acab0b8..97bbca2 100644 --- a/contracts/architecture.mdx +++ b/contracts/architecture.mdx @@ -202,13 +202,7 @@ mapping(bytes32 paymentInfoHash => uint256 frozenUntil) public frozenUntil; ### Fee Distribution (Additive Model) -Fees are **additive**: `totalFee = protocolFee + operatorFee` - -For a 1000 USDC payment with 3 bps protocol fee + 2 bps operator fee: -- **Protocol Fee:** 0.30 USDC (3 bps) → `protocolFeeRecipient` on ProtocolFeeConfig -- **Operator Fee:** 0.20 USDC (2 bps) → `FEE_RECEIVER` on operator -- **Total Fee:** 0.50 USDC (5 bps) -- **Receiver Gets:** 999.50 USDC +Fees are **additive**: `totalFee = protocolFee + operatorFee`. They are split between the protocol fee recipient (on ProtocolFeeConfig) and the operator's `FEE_RECEIVER`. For a worked example with concrete amounts, see the [Fee System](/contracts/fees#example-calculation). Fees accumulate in the operator. Anyone can call `distributeFees(token)` to disburse them. @@ -247,29 +241,7 @@ All state-changing functions use `ReentrancyGuardTransient` (EIP-1153): ### Timelock Protection -Protocol fee calculator changes require 7-day delay on `ProtocolFeeConfig`: - -```typescript -// Step 1: Queue new calculator -await protocolFeeConfig.queueCalculator(newCalculatorAddress); - -// Step 2: Wait 7 days - -// Step 3: Execute -await protocolFeeConfig.executeCalculator(); -``` - -Protocol fee recipient changes also require 7-day timelock: - -```typescript -// Step 1: Queue new recipient -await protocolFeeConfig.queueRecipient(newRecipientAddress); - -// Step 2: Wait 7 days - -// Step 3: Execute -await protocolFeeConfig.executeRecipient(); -``` +Protocol fee calculator and recipient changes require a 7-day delay on `ProtocolFeeConfig` (queue, wait, execute). See [Fee System: 7-day timelock](/contracts/fees#calculator-changes-7-day-timelock) for the full workflow with events and the cancel path. Operator fees are **immutable**: set at deploy time via `IFeeCalculator`. Only protocol fees can change, and only after a 7-day timelock. diff --git a/contracts/conditions/escrow-period.mdx b/contracts/conditions/escrow-period.mdx index f5fa15e..2b8bf61 100644 --- a/contracts/conditions/escrow-period.mdx +++ b/contracts/conditions/escrow-period.mdx @@ -77,13 +77,7 @@ const config = { ## Composition with Freeze -For freeze functionality, deploy a separate [Freeze](/contracts/conditions/freeze) condition and compose via [AndCondition](/contracts/conditions/combinators): - -```solidity -// Escrow period only: capturePreActionCondition = escrowPeriod -// Freeze only: capturePreActionCondition = freeze -// Both: capturePreActionCondition = AndCondition([escrowPeriod, freeze]) -``` +Compose this condition with a separate [Freeze](/contracts/conditions/freeze) condition via [AndCondition](/contracts/conditions/combinators) to gate capture on both escrow elapsed **and** not frozen. See [Composition Patterns](/contracts/conditions/freeze#composition-patterns) for the wiring. ## Use Cases diff --git a/contracts/conditions/overview.mdx b/contracts/conditions/overview.mdx index 3d302d6..a7acdf9 100644 --- a/contracts/conditions/overview.mdx +++ b/contracts/conditions/overview.mdx @@ -16,6 +16,8 @@ Conditions are swappable contracts that control who can perform actions on a Pay | `VOID_PRE_ACTION_CONDITION` | Who can refund during escrow | | `REFUND_PRE_ACTION_CONDITION` | Who can refund after capture | +These are the condition half of the operator's 10 slots. For the full slot layout alongside the post-action hooks, see [PaymentOperator: 10-slot configuration](/contracts/payment-operator#10-slot-configuration). + ## ICondition Interface ```solidity diff --git a/contracts/factories.mdx b/contracts/factories.mdx index 28fd3ce..bc5ea8b 100644 --- a/contracts/factories.mdx +++ b/contracts/factories.mdx @@ -350,13 +350,7 @@ const config = { ### Condition Singletons -Use these pre-deployed condition contracts: - -| Condition | Address (all chains) | Description | -|-----------|---------------------|-------------| -| PayerCondition | `0x586486394C38A2a7d36B16a3FDaF366cd202d823` | Only payer can call | -| ReceiverCondition | `0x321651df4593DA57C413579c5b611D1A90168a3A` | Only receiver can call | -| AlwaysTrueCondition | `0x2ef2A6162aEF9Df1022ff51c011af94D99AB4904` | Anyone can call | +Reference the pre-deployed condition singletons (PayerCondition, ReceiverCondition, AlwaysTrueCondition) by their canonical addresses. The full address registry lives on [Periphery Overview: Condition Singletons](/contracts/periphery/overview#condition-singletons), identical across every supported chain. ### Example Deployments diff --git a/contracts/hooks/overview.mdx b/contracts/hooks/overview.mdx index d5055fe..ddb0e3a 100644 --- a/contracts/hooks/overview.mdx +++ b/contracts/hooks/overview.mdx @@ -16,6 +16,8 @@ Hooks are pluggable contracts that update state **after** an action successfully | `VOID_POST_ACTION_HOOK` | Void | | `REFUND_POST_ACTION_HOOK` | Refund (after capture) | +These are the hook half of the operator's 10 slots. For the full slot layout alongside the pre-action conditions, see [PaymentOperator: 10-slot configuration](/contracts/payment-operator#10-slot-configuration). + ## IHook Interface ```solidity diff --git a/contracts/overview.mdx b/contracts/overview.mdx index c012015..c0df1c2 100644 --- a/contracts/overview.mdx +++ b/contracts/overview.mdx @@ -6,7 +6,7 @@ icon: "circle-info" ## What is x402r -x402r is a smart contract extension for HTTP-native refundable payments. It builds on the canonical [Commerce Payments Protocol](https://github.com/base/commerce-payments) to add dispute resolution, escrow periods, and refund capabilities. +x402r builds on the canonical [Commerce Payments Protocol](https://github.com/base/commerce-payments) to add dispute resolution, escrow periods, and refund capabilities. For the protocol-level introduction and the payer/merchant/arbiter model, see [What is x402r](/). This page covers the contract layer. ## Architecture Layers diff --git a/contracts/payment-operator.mdx b/contracts/payment-operator.mdx index f94121e..2357aee 100644 --- a/contracts/payment-operator.mdx +++ b/contracts/payment-operator.mdx @@ -244,13 +244,7 @@ address public immutable FEE_RECEIVER; // Operator fee recipient mapping(address token => uint256) public accumulatedProtocolFees; ``` -**Example Fee Calculation (Additive):** - -For a 1000 USDC payment: -- **Protocol Fee:** 3 bps (0.03%) = 0.30 USDC -> goes to `protocolFeeRecipient` -- **Operator Fee:** 2 bps (0.02%) = 0.20 USDC -> goes to `FEE_RECEIVER` -- **Total Fee:** 5 bps (0.05%) = 0.50 USDC -- **Receiver Gets:** 999.50 USDC +Fees are additive: `totalFee = protocolFee + operatorFee`, split between `protocolFeeRecipient` and the operator's `FEE_RECEIVER`. For a worked example with concrete amounts, see the [Fee System](/contracts/fees#example-calculation). **Fee Locking:** The operator calculates fees at `authorize()` time and stores them in `authorizedFees[hash]`. This stops later protocol fee changes from breaking capture of already-authorized payments. @@ -282,17 +276,7 @@ operator.distributeFees(usdcAddress); ### Protocol Fee Changes (7-day Timelock) -Protocol fee calculator changes require a 7-day timelock on `ProtocolFeeConfig`: - -```solidity -// Step 1: Queue new calculator -protocolFeeConfig.queueCalculator(newCalculatorAddress); - -// Step 2: Wait 7 days - -// Step 3: Execute -protocolFeeConfig.executeCalculator(); -``` +Protocol fee calculator and recipient changes require a 7-day timelock on `ProtocolFeeConfig`. See [Fee System: 7-day timelock](/contracts/fees#calculator-changes-7-day-timelock) for the full queue, wait, execute workflow. Protocol fee changes require 7-day timelock. Operator fees are immutable (set at deploy time). diff --git a/index.mdx b/index.mdx index 72f9d97..6bfe861 100644 --- a/index.mdx +++ b/index.mdx @@ -79,12 +79,7 @@ All protocol contracts use universal CREATE2 addresses, same address on every su ## Supported networks -Today, the supported chains in `@x402r/core` are **Base** and **Base Sepolia**. More EVMs land as canonical `base/commerce-payments@v1.0.0` coverage extends. - -| Network | Chain ID | Status | -|---|---|---| -| Base | 8453 | Supported | -| Base Sepolia | 84532 | Testnet | +Today, the supported chains in `@x402r/core` are **Base** and **Base Sepolia**. More EVMs land as canonical `base/commerce-payments@v1.0.0` coverage extends. See [Network support](/sdk/overview#network-support) for chain IDs and USDC token addresses. ## Resources diff --git a/sdk/deploy-operator.mdx b/sdk/deploy-operator.mdx index d568759..285efc9 100644 --- a/sdk/deploy-operator.mdx +++ b/sdk/deploy-operator.mdx @@ -147,14 +147,7 @@ The deployed marketplace operator has the following slot configuration: ## Network support -Today, the deploy presets target the chains in `@x402r/core`'s `x402rChains`: - -| Network | Chain ID | EIP-155 ID | -|---|---|---| -| Base | 8453 | `eip155:8453` | -| Base Sepolia | 84532 | `eip155:84532` | - -More EVM chains land as canonical `base/commerce-payments@v1.0.0` coverage extends. +The deploy presets target the chains in `@x402r/core`'s `x402rChains` (Base and Base Sepolia today). See [Network support](/sdk/overview#network-support) for chain IDs, EIP-155 IDs, and token addresses. Deployment requires gas fees. Ensure your wallet has ETH on the target network. On Base Sepolia, you can fund a wallet from [Base network faucets](https://docs.base.org/base-chain/tools/network-faucets). diff --git a/sdk/overview.mdx b/sdk/overview.mdx index a91e192..4d704ac 100644 --- a/sdk/overview.mdx +++ b/sdk/overview.mdx @@ -20,6 +20,8 @@ Three roles interact with the protocol: **Delivery Protection** (`deployDeliveryProtectionOperator`): The arbiter evaluates every transaction. The arbiter or a satisfied payer can capture funds. On a FAIL verdict, the arbiter can trigger an immediate refund. If nobody acts, funds return to the payer once escrow expires. Use this for AI content verification, schema validation, or quality checks. +See [Deploy an operator](/sdk/deploy-operator) for the full preset feature comparison, slot configurations, and deployment code. + ### Packages diff --git a/x402-integration/overview.mdx b/x402-integration/overview.mdx index bbe8877..941997f 100644 --- a/x402-integration/overview.mdx +++ b/x402-integration/overview.mdx @@ -113,28 +113,7 @@ x402r provides the **auth-capture scheme implementation** for x402: ## Payment Flow -```mermaid -sequenceDiagram - participant Client - participant Server - participant Facilitator - participant Operator - participant Escrow - - Client->>Server: GET /resource - Server-->>Client: 402 Payment Required - Note over Client: Signs ERC-3009 authorization - Client->>Server: Payment payload - Server->>Facilitator: Verify & settle - Facilitator->>Operator: authorize(paymentInfo, amount, tokenCollector, signature) - Operator->>Escrow: Lock funds in escrow - Facilitator-->>Server: Settlement confirmed - Server-->>Client: 200 OK + resource - - Note over Operator,Escrow: Later: capture or void - Operator->>Escrow: capture(paymentInfo, amount) - Note over Escrow: Funds released to receiver (minus fees) -``` +The auth-capture scheme is two-phase: the facilitator first authorizes (locks the client's signed funds in escrow at the 402 settlement), then later captures to the receiver or voids back to the payer per policy. For the full HTTP and on-chain sequence diagrams, see the [auth-capture flow](/x402-integration/auth-capture) and the [on-chain payment sequence](/contracts/architecture#payment-flow-sequence). ## Use Cases