From aca5789dc9db04946c5d0263656343c468b3a29f Mon Sep 17 00:00:00 2001 From: Anuj Bansal Date: Wed, 7 Oct 2026 21:26:15 +0530 Subject: [PATCH 1/3] Add x402 paid quote and withdrawal guide --- docs/cow-protocol/integrate/README.mdx | 3 +- docs/cow-protocol/integrate/api.mdx | 4 +- docs/cow-protocol/integrate/x402.mdx | 176 +++++++++++++++++++++++++ package.json | 1 + static/code-examples/x402/inspect.mjs | 26 ++++ static/code-examples/x402/quote.mts | 127 ++++++++++++++++++ static/code-examples/x402/withdraw.mts | 61 +++++++++ tsconfig.json | 1 + yarn.lock | 8 ++ 9 files changed, 405 insertions(+), 2 deletions(-) create mode 100644 docs/cow-protocol/integrate/x402.mdx create mode 100644 static/code-examples/x402/inspect.mjs create mode 100644 static/code-examples/x402/quote.mts create mode 100644 static/code-examples/x402/withdraw.mts diff --git a/docs/cow-protocol/integrate/README.mdx b/docs/cow-protocol/integrate/README.mdx index 58342a334..06a54453f 100644 --- a/docs/cow-protocol/integrate/README.mdx +++ b/docs/cow-protocol/integrate/README.mdx @@ -9,6 +9,7 @@ CoW Protocol offers multiple integration approaches to suit different developmen | **Widget** | Quick integration, minimal setup | Low | Basic styling and configuration | | **SDK** | Full control, custom UI | Medium | Complete control over UI/UX | | **API** | Backend integration, custom logic | High | Maximum flexibility | +| **[x402](./x402.mdx)** | Paid quote requests from scripts and agents | Low | Prepaid payments without an API key | ## Choose Your Integration Method @@ -35,4 +36,4 @@ Best for backend services, advanced integrations, and when you need maximum cont --- -Choose the integration method that best fits your project's requirements and development timeline. Each approach is designed to provide progressively more control and customization options. \ No newline at end of file +Choose the integration method that best fits your project's requirements and development timeline. Each approach is designed to provide progressively more control and customization options. diff --git a/docs/cow-protocol/integrate/api.mdx b/docs/cow-protocol/integrate/api.mdx index b0c5c141a..f763c7850 100644 --- a/docs/cow-protocol/integrate/api.mdx +++ b/docs/cow-protocol/integrate/api.mdx @@ -1,5 +1,8 @@ # API Integration +For scripts and agents that want to pay per quote without an API key, see +[Paid quotes with x402](./x402.mdx). The guide covers prepaid funding, quote requests and withdrawals. + The CoW Protocol API provides direct access to the protocol's functionality through RESTful endpoints. This approach offers maximum flexibility and control for backend integrations and custom implementations. ## Overview @@ -217,4 +220,3 @@ For complete API documentation including all endpoints, parameters, and response - **[API Explorer](https://api.cow.fi/docs/)** - Interactive documentation - **[GitHub Examples](https://github.com/cowprotocol/cow-sdk/tree/main/examples)** - Code examples - **[Order Signing Guide](../reference/core/signing_schemes.mdx)** - Cryptographic signing details - diff --git a/docs/cow-protocol/integrate/x402.mdx b/docs/cow-protocol/integrate/x402.mdx new file mode 100644 index 000000000..338ed13fb --- /dev/null +++ b/docs/cow-protocol/integrate/x402.mdx @@ -0,0 +1,176 @@ +--- +title: Paid quotes with x402 +description: Get CoW Protocol quotes using prepaid x402 payments and withdraw unused funds. +sidebar_position: 4 +--- + +import CodeBlock from '@theme/CodeBlock' +import inspectSource from '!!raw-loader!@site/static/code-examples/x402/inspect.mjs' +import quoteSource from '!!raw-loader!@site/static/code-examples/x402/quote.mts' +import withdrawSource from '!!raw-loader!@site/static/code-examples/x402/withdraw.mts' + +# Paid quotes with x402 + +Get CoW Protocol quotes from a script or agent without an API key. Deposit tokens into a prepaid channel, then authorise each quote with an off-chain signature. The deposit funds future requests; it is not an extra quote fee. CoW collects the charges in batches. + +## Get a quote + +This example uses **Node.js 22** and **USDC on Base** to buy an Ethereum quote. +The payment and quote chains are independent. It returns a quote; it does not execute a swap. +Browser paid retries are not supported. + +### 1. Check the terms + +Save inspect.mjs and run `node inspect.mjs`. +It shows the current price, minimum deposit and withdrawal delay without a key, signature or payment. +Fund your wallet with enough Base USDC if you accept the terms. + +
+Read-only terms preview + + + {inspectSource} + + +
+ +For illustration, a 1 USDC deposit at 0.001 USDC per quote funds 1,000 quotes while that price is unchanged. +Always read current terms from the service. + +### 2. Set up the example + +```bash +mkdir x402-quickstart +cd x402-quickstart +npm init -y +npm install @x402/fetch@2.26.0 @x402/evm@2.26.0 @x402/core@2.26.0 viem +npm install --save-dev tsx +umask 077 +touch .env +chmod 600 .env +``` + +Edit `.env` locally with your payer key and a Base RPC URL: + +```text +PRIVATE_KEY=YOUR_PAYER_PRIVATE_KEY +RPC_URL=YOUR_BASE_RPC_URL +``` + +Keep `.env`, `x402-state/`, `x402-channels/` and `x402-attempts/` out of source control. +Saved signed attempts are sensitive too. + +The example defaults to **0.001 USDC per quote**, **1 USDC per deposit or top-up**, and a **24-hour maximum accepted withdrawal delay**. These are client limits, not guaranteed server terms. If an offer exceeds them, the script stops before signing. + +
+Optional limits + +```text +# Optional overrides; these are the example's defaults. +MAX_QUOTE_USDC=0.001 +MAX_DEPOSIT_USDC=1 +MAX_WITHDRAW_DELAY_HOURS=24 +``` + +`MAX_WITHDRAW_DELAY_HOURS` only limits which offers your client accepts. It does not choose the waiting period: a new channel takes the delay advertised by the server, and keeps that delay for its lifetime. You can omit this setting; the example accepts delays up to 24 hours. + +Enter price and deposit limits in human USDC units. +A per-deposit limit applies to each top-up; it is not a lifetime spending cap. + +
+ +### 3. Request a quote + +Save quote.mts in the project directory. +It checks payment terms, uses SDK receipt validation and saves channel state and recovery files. +The example uses `@x402/*` **2.26.0**. + +
+Complete quote example + + + {quoteSource} + + +
+ +```bash +node --env-file=.env --import tsx quote.mts +``` + +A successful run prints `status: 200`, `quoteReceived: true`, the charge, a saved channel-file path and the quote JSON. +The first request funds the channel; later requests reuse it and top up when needed, within your limits. +CoW pays deposit and collection gas. + +Run one request at a time per channel and keep the saved files across restarts. + +:::info Uncertain payment outcomes + +If a request times out or returns a settlement error, keep the state, signed attempt and transaction hash. A charge or deposit may have succeeded even without a quote response. Do not repeatedly rerun it or create another deposit; check the original transaction and use the SDK’s validated recovery. Each new successful request can incur another charge. +The [API reference](https://x402.cow.fi/docs/) describes the payment response and recovery errors. +Never reset your charged total to on-chain claims alone: they omit charges awaiting collection. + +::: + +## Withdraw unused funds + +You initiate a withdrawal, wait for the saved channel's deadline, then finalize it. +These are **two transactions**, paid by your wallet in Base ETH for this example. +Finalization returns tokens to the payer; it is not automatic. + +Save withdraw.mts in the same project directory. +In `.env`, set `CHANNEL` to the channel-file path printed by the quote example. +Keep `RPC_URL` on that channel's payment chain. + +```text +CHANNEL=./x402-channels/YOUR_CHANNEL_ID.json +``` + +
+Complete withdrawal example + + + {withdrawSource} + + +
+ +**1. Check status** without spending gas: + +```bash +node --env-file=.env --import tsx withdraw.mts status +``` + +**2. Stop using the channel and initiate** the withdrawal: + +```bash +node --env-file=.env --import tsx withdraw.mts initiate +node --env-file=.env --import tsx withdraw.mts status +``` + +**3. Wait until `finalizeAt`**, calculated from the on-chain initiation time and that channel's original delay. +A new offer cannot change it. + +**4. Finalize**, then check the receipt and your token balance: + +```bash +node --env-file=.env --import tsx withdraw.mts finalize +node --env-file=.env --import tsx withdraw.mts status +``` + +CoW may collect outstanding charges during the wait, reducing the returned amount. +Collection happens only when economic; uncollected fees do not extend the withdrawal deadline. +A pending withdrawal cannot be cancelled. +If a transaction times out, check its hash and status before sending another. +Resolve uncertain payments before withdrawing. + +## Other payment options and help + +The current options are Base USDC/COW, Ethereum USDC and BNB USDC. +COW and BNB USDC require an explicit SDK asset entry and Permit2 approval. +Approval and withdrawal gas is paid in the payment chain's native token. +COW amounts have no guaranteed dollar equivalent. + +Use the [live API reference](https://x402.cow.fi/docs/) for payment fields, networks and errors. +Contact [CoW Protocol Discord](https://discord.com/invite/cowprotocol) with your network, channel ID and transaction hash; +never share keys or signed payments. diff --git a/package.json b/package.json index 1ac9fb925..75da204ce 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "docusaurus-plugin-typedoc": "^0.21.0", "dotenv": "^16.3.1", "prettier": "^3.0.3", + "raw-loader": "4.0.2", "typedoc": "^0.25.3", "typedoc-plugin-markdown": "^3.17.0", "typescript": "~5.2.2" diff --git a/static/code-examples/x402/inspect.mjs b/static/code-examples/x402/inspect.mjs new file mode 100644 index 000000000..1cddc0d10 --- /dev/null +++ b/static/code-examples/x402/inspect.mjs @@ -0,0 +1,26 @@ +// inspect.mjs — read-only; no private key, SDK, signature or payment. +const response = await fetch("https://x402.cow.fi/mainnet/api/v1/quote", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", +}); +const header = response.headers.get("PAYMENT-REQUIRED"); +if (response.status !== 402 || !header) throw new Error("No payment challenge received"); +const challenge = JSON.parse(Buffer.from(header, "base64").toString("utf8")); +const option = challenge.accepts.find(r => + r.scheme === "batch-settlement" && r.network === "eip155:8453" && + r.asset.toLowerCase() === "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913" +); +if (!option) throw new Error("Base USDC is not currently offered"); +function usdc(atomic) { + if (typeof atomic !== "string" || !/^\d+$/.test(atomic)) throw new Error("Invalid amount"); + const digits = BigInt(atomic).toString().padStart(7, "0"); + return (digits.slice(0, -6) + "." + digits.slice(-6)).replace(/\.?0+$/, "") + " USDC"; +} +console.table({ + payment: "USDC on Base", + quotePrice: usdc(option.amount), + minimumDepositOrTopUp: usdc(option.extra.minDeposit), + newChannelWithdrawalDelay: option.extra.withdrawDelay / 3600 + " hours", + depositAuthorizationValidity: option.maxTimeoutSeconds + " seconds", +}); diff --git a/static/code-examples/x402/quote.mts b/static/code-examples/x402/quote.mts new file mode 100644 index 000000000..9c77fe13c --- /dev/null +++ b/static/code-examples/x402/quote.mts @@ -0,0 +1,127 @@ +import { randomUUID } from "node:crypto"; +import { decodePaymentRequiredHeader, decodePaymentResponseHeader } from "@x402/core/http"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; +import { BatchSettlementEvmScheme, computeChannelId } from "@x402/evm/batch-settlement/client"; +import { FileClientChannelStorage } from "@x402/evm/batch-settlement/client/file-storage"; +import { privateKeyToAccount } from "viem/accounts"; +import { createPublicClient, formatUnits, http, parseUnits } from "viem"; +import { base } from "viem/chains"; +import { toClientEvmSigner } from "@x402/evm"; + +const NETWORK = "eip155:8453"; +const ASSET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; +const PAY_TO = "0xaa0a2b5E68351C46474f4CE142A1357e25EedBA9"; +const RECEIVER_AUTHORIZER = "0x0DA62b1f9904AB97be179579A32533210d7ebBA5"; +function usdcLimit(name: string, fallback: string) { + const value = process.env[name] ?? fallback; + if (!/^\d+(\.\d{1,6})?$/.test(value)) throw new Error(`Set ${name} in USDC (up to 6 decimals)`); + return parseUnits(value, 6); +} +const maxQuote = usdcLimit("MAX_QUOTE_USDC", "0.001"); +const maxDeposit = usdcLimit("MAX_DEPOSIT_USDC", "1"); +const maxDelay = Number(process.env.MAX_WITHDRAW_DELAY_HOURS ?? "24") * 3600; +if (maxQuote <= 0n || maxDeposit < maxQuote || !Number.isSafeInteger(maxDelay) || maxDelay <= 0) { + throw new Error("Set your quote, deposit and withdrawal-delay limits"); +} + +if (!process.env.RPC_URL) throw new Error("Set RPC_URL for Base"); +if (!/^0x[0-9a-fA-F]{64}$/.test(process.env.PRIVATE_KEY ?? "")) throw new Error("Set PRIVATE_KEY securely in .env"); +const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); +const publicClient = createPublicClient({ chain: base, transport: http(process.env.RPC_URL) }); +if (await publicClient.getChainId() !== base.id) throw new Error("RPC_URL points to the wrong chain"); +const signer = toClientEvmSigner(account, publicClient); +const client = new x402Client().register( + NETWORK, + new BatchSettlementEvmScheme(signer, { + storage: new FileClientChannelStorage({ directory: "./x402-state" }), + depositStrategy: ({ depositAmount }) => { + if (BigInt(depositAmount) > maxDeposit) throw new Error("Deposit exceeds your limit"); + return depositAmount; + }, + }), +); +// Sign only the terms you expect. +client.registerPolicy((_version, requirements) => + requirements.filter( + (r) => + r.scheme === "batch-settlement" && + r.network === NETWORK && + r.asset.toLowerCase() === ASSET.toLowerCase() && + r.payTo.toLowerCase() === PAY_TO.toLowerCase() && + typeof r.extra?.receiverAuthorizer === "string" && + r.extra.receiverAuthorizer.toLowerCase() === RECEIVER_AUTHORIZER.toLowerCase() && + BigInt(r.amount) > 0n && BigInt(r.amount) <= maxQuote && + typeof r.extra?.minDeposit === "string" && + /^\d+$/.test(r.extra.minDeposit) && + BigInt(r.extra.minDeposit) > 0n && BigInt(r.extra.minDeposit) <= maxDeposit && + typeof r.extra?.withdrawDelay === "number" && + Number.isSafeInteger(r.extra.withdrawDelay) && + r.extra.withdrawDelay > 0 && r.extra.withdrawDelay <= maxDelay, + ), +); +// Allow the selected token; the policy caps quote prices and withdrawal delay. +// depositStrategy checks the actual deposit amount before signing it. +client.setSpendControls({ + allowedAssets: [{ network: NETWORK, asset: ASSET, maxAmountPerPayment: maxDeposit.toString() }], +}); +// Save the original configuration and signed attempt before sending. +let latestAttemptFile: string | undefined; +let latestChannelFile: string | undefined; +client.onAfterPaymentCreation(async ({ paymentPayload }) => { + const config = paymentPayload.payload.channelConfig as Parameters[0]; + const network = paymentPayload.accepted.network; + const channelId = computeChannelId(config, network); + mkdirSync("./x402-channels", { recursive: true, mode: 0o700 }); + mkdirSync("./x402-attempts", { recursive: true, mode: 0o700 }); + latestAttemptFile = `./x402-attempts/${randomUUID()}.json`; + writeFileSync(latestAttemptFile, JSON.stringify(paymentPayload), { mode: 0o600, flag: "wx" }); + latestChannelFile = `./x402-channels/${channelId}.json`; + writeFileSync(latestChannelFile, JSON.stringify({ network, channelId, channelConfig: config }, null, 2), { mode: 0o600 }); +}); +const paidFetch = wrapFetchWithPayment(fetch, client); + +try { + const res = await paidFetch("https://x402.cow.fi/mainnet/api/v1/quote", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + sellToken: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH + buyToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC + from: "0x0000000000000000000000000000000000000000", + receiver: "0x0000000000000000000000000000000000000000", + kind: "sell", + sellAmountBeforeFee: "1000000000000000000", + }), + }); + const requiredHeader = res.headers.get("PAYMENT-REQUIRED"); + const responseHeader = res.headers.get("PAYMENT-RESPONSE"); + const challenge = requiredHeader ? decodePaymentRequiredHeader(requiredHeader) : undefined; + const receipt = responseHeader ? decodePaymentResponseHeader(responseHeader) : undefined; + const body = await res.text(); + if (res.status !== 200 || receipt?.success !== true) { + console.error({ + status: res.status, + error: receipt?.errorReason ?? challenge?.error, + transaction: receipt?.transaction, + savedAttempt: latestAttemptFile, + body, + }); + throw new Error("Payment outcome needs checking; follow Troubleshooting before retrying"); + } + const result = JSON.parse(body); + if (!result.quote) throw new Error("Expected a quote in the successful response"); + const charged = receipt.extra?.chargedAmount ?? "0"; + if (typeof charged !== "string" || !/^\d+$/.test(charged)) throw new Error("Invalid receipt charge"); + console.log({ + status: res.status, + quoteReceived: true, + paymentNetwork: receipt.network, + chargedUSDC: formatUnits(BigInt(charged), 6), + channelFile: latestChannelFile, + }); + console.log(result); +} catch (error) { + console.error({ savedAttempt: latestAttemptFile, message: "Keep state and check Troubleshooting before rerunning an unresolved attempt." }); + throw error; +} diff --git a/static/code-examples/x402/withdraw.mts b/static/code-examples/x402/withdraw.mts new file mode 100644 index 000000000..8312e7a23 --- /dev/null +++ b/static/code-examples/x402/withdraw.mts @@ -0,0 +1,61 @@ +// withdraw.mts — status is read-only; initiate/finalize each send one transaction. +import { readFileSync } from "node:fs"; +import { createPublicClient, createWalletClient, http, parseAbi } from "viem"; +import { base, mainnet, bsc } from "viem/chains"; +import { privateKeyToAccount } from "viem/accounts"; +import { computeChannelId } from "@x402/evm/batch-settlement/client"; + +const action = process.argv[2] ?? "status"; +if (!["status", "initiate", "finalize"].includes(action)) throw new Error("Use status, initiate or finalize"); +if (!process.env.CHANNEL || !process.env.RPC_URL) throw new Error("Set CHANNEL and RPC_URL"); +const saved = JSON.parse(readFileSync(process.env.CHANNEL, "utf8")); +const { network, channelConfig: config } = saved; +const chain = [base, mainnet, bsc].find(c => network === `eip155:${c.id}`); +if (!chain) throw new Error("Unsupported payment chain"); +const channelId = computeChannelId(config, network); +if (saved.channelId && saved.channelId.toLowerCase() !== channelId.toLowerCase()) throw new Error("Channel configuration mismatch"); +const ESCROW: `0x${string}` = "0x4020074e9dF2ce1deE5A9C1b5c3f541D02a10003"; +const abi = parseAbi([ + "struct ChannelConfig { address payer; address payerAuthorizer; address receiver; address receiverAuthorizer; address token; uint40 withdrawDelay; bytes32 salt; }", + "function channels(bytes32 channelId) view returns (uint128 balance, uint128 totalClaimed)", + "function pendingWithdrawals(bytes32 channelId) view returns (uint128 amount, uint40 initiatedAt)", + "function initiateWithdraw(ChannelConfig config, uint128 amount)", + "function finalizeWithdraw(ChannelConfig config)", +]); +const publicClient = createPublicClient({ chain, transport: http(process.env.RPC_URL) }); +if (await publicClient.getChainId() !== chain.id) throw new Error("RPC_URL points to the wrong chain"); +const block = await publicClient.getBlock(); +const [balance, totalClaimed] = await publicClient.readContract({ address: ESCROW, abi, functionName: "channels", args: [channelId], blockNumber: block.number }); +const [requested, initiatedAt] = await publicClient.readContract({ address: ESCROW, abi, functionName: "pendingWithdrawals", args: [channelId], blockNumber: block.number }); +const finalizeAt = initiatedAt ? BigInt(initiatedAt) + BigInt(config.withdrawDelay) : null; +console.log({ channelId, balanceAtomic: balance.toString(), claimedAtomic: totalClaimed.toString(), requestedAtomic: requested.toString(), finalizeAt: finalizeAt ? new Date(Number(finalizeAt) * 1000).toISOString() : null }); + +if (action !== "status") { + const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); + if (![config.payer, config.payerAuthorizer].some((a: string) => a.toLowerCase() === account.address.toLowerCase())) throw new Error("Wrong payer key"); + const wallet = createWalletClient({ account, chain, transport: http(process.env.RPC_URL) }); + if (action === "initiate") { + if (initiatedAt) throw new Error("Withdrawal already pending"); + if (balance <= totalClaimed) throw new Error("Nothing remains to withdraw"); + } else { + if (!finalizeAt || block.timestamp < finalizeAt) throw new Error("No pending withdrawal, or its on-chain delay has not elapsed"); + } + const fees = await publicClient.estimateFeesPerGas({ type: "eip1559" }); + const floor = chain.id === 8453 ? 1_000_000n : chain.id === 56 ? 50_000_000n : 100_000_000n; + const extraTip = fees.maxPriorityFeePerGas < floor ? floor - fees.maxPriorityFeePerGas : 0n; + const pricing = { maxPriorityFeePerGas: fees.maxPriorityFeePerGas + extraTip, maxFeePerGas: fees.maxFeePerGas + extraTip }; + let hash: `0x${string}`; + if (action === "initiate") { + const call = { address: ESCROW, abi, functionName: "initiateWithdraw", args: [config, balance - totalClaimed], account } as const; + await publicClient.simulateContract(call); + hash = await wallet.writeContract({ ...call, ...pricing }); + } else { + const call = { address: ESCROW, abi, functionName: "finalizeWithdraw", args: [config], account } as const; + await publicClient.simulateContract(call); + hash = await wallet.writeContract({ ...call, ...pricing }); + } + console.log({ action, transaction: hash }); // Keep this hash if receipt waiting times out. + const receipt = await publicClient.waitForTransactionReceipt({ hash, timeout: 60_000 }); + if (receipt.status !== "success") throw new Error(`Transaction reverted: ${hash}`); + console.log({ status: receipt.status, transaction: hash }); +} diff --git a/tsconfig.json b/tsconfig.json index 24f5b963d..173d88cf8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { // This file is not used in compilation. It is here just for a nice editor experience. "extends": "@docusaurus/tsconfig", + "exclude": ["static/code-examples/x402"], "compilerOptions": { "baseUrl": ".", "resolveJsonModule": true, diff --git a/yarn.lock b/yarn.lock index e072a3928..e83db01e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9656,6 +9656,14 @@ raw-body@~2.5.3: iconv-lite "~0.4.24" unpipe "~1.0.0" +raw-loader@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" + integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + rc@1.2.8, rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" From 0fd042b4944aceca9cd52e0230cef479b3ada272 Mon Sep 17 00:00:00 2001 From: Anuj Bansal Date: Tue, 13 Oct 2026 06:50:30 +0100 Subject: [PATCH 2/3] Simplify x402 quickstart and separate recovery guide --- docs/cow-protocol/integrate/x402-recovery.mdx | 159 ++++++++++++++++++ docs/cow-protocol/integrate/x402.mdx | 122 ++++---------- static/code-examples/x402/quote.mts | 4 +- tsconfig.json | 2 +- 4 files changed, 192 insertions(+), 95 deletions(-) create mode 100644 docs/cow-protocol/integrate/x402-recovery.mdx diff --git a/docs/cow-protocol/integrate/x402-recovery.mdx b/docs/cow-protocol/integrate/x402-recovery.mdx new file mode 100644 index 000000000..d35ae0e32 --- /dev/null +++ b/docs/cow-protocol/integrate/x402-recovery.mdx @@ -0,0 +1,159 @@ +--- +title: x402 recovery and withdrawals +description: Complete x402 client example, payment recovery and withdrawal instructions. +sidebar_position: 5 +--- + +import CodeBlock from '@theme/CodeBlock' +import inspectSource from '!!raw-loader!@site/static/code-examples/x402/inspect.mjs' +import quoteSource from '!!raw-loader!@site/static/code-examples/x402/quote.mts' +import withdrawSource from '!!raw-loader!@site/static/code-examples/x402/withdraw.mts' + +# x402 recovery and withdrawals + +Start with the [x402 quickstart](./x402.mdx) to install the SDK and set up your payer. +This page contains the full runnable example, optional limits, recovery guidance and withdrawal steps. +Use the same project directory and preserve its saved files when moving between the examples. + +## Complete quote example + +The SDK handles the payment protocol. The extra code here configures your limits, +checks the RPC and receiver, saves signed attempts and original channel terms, and reports errors. +These are application safeguards, not additional protocol steps you must implement yourself. + +### Inspect current terms + +Save inspect.mjs and run `node inspect.mjs`. +It shows the current price, minimum deposit and withdrawal delay without a key, signature or payment. +Fund your wallet with enough Base USDC if you accept the terms. + +
+Read-only terms preview + + + {inspectSource} + + +
+ +For illustration, a 1 USDC deposit at 0.001 USDC per quote funds 1,000 quotes while that price is unchanged. +Always read current terms from the service. + +### Client limits + +The example defaults to **0.001 USDC per quote**, **1 USDC per deposit or top-up**, and a **24-hour maximum accepted withdrawal delay**. These are client limits, not guaranteed server terms. If an offer exceeds them, the script stops before signing. + +
+Optional limits + +```text +# Optional overrides; these are the example's defaults. +MAX_QUOTE_USDC=0.001 +MAX_DEPOSIT_USDC=1 +MAX_WITHDRAW_DELAY_HOURS=24 +``` + +`MAX_WITHDRAW_DELAY_HOURS` only limits which offers your client accepts. It does not choose the waiting period: a new channel takes the delay advertised by the server, and keeps that delay for its lifetime. You can omit this setting; the example accepts delays up to 24 hours. + +Enter price and deposit limits in human USDC units. +A per-deposit limit applies to each top-up; it is not a lifetime spending cap. + +
+ +### Run the complete example + +Save quote.mts in the project directory. +It checks payment terms, uses SDK receipt validation and saves channel state and recovery files. +The example uses `@x402/*` **2.26.0**. + +
+Complete quote example + + + {quoteSource} + + +
+ +```bash +node --env-file=.env --import tsx quote.mts +``` + +A successful run prints `status: 200`, `quoteReceived: true`, the charge, a saved channel-file path and the quote JSON. +The first request funds the channel; later requests reuse it and top up when needed, within your limits. +CoW pays deposit and collection gas. + +Run one request at a time per channel and keep the saved files across restarts. + +## Recover an uncertain payment + +:::info Keep the original attempt + +If a request times out or returns a settlement error, keep the state, signed attempt and transaction hash. A charge or deposit may have succeeded even without a quote response. Do not repeatedly rerun it or create another deposit; check the original transaction and use the SDK’s validated recovery. Each new successful request can incur another charge. +The [API reference](https://x402.cow.fi/docs/) describes the payment response and recovery errors. +Never reset your charged total to on-chain claims alone: they omit charges awaiting collection. + +::: + +## Withdraw unused funds + +You initiate a withdrawal, wait for the saved channel's deadline, then finalize it. +These are **two transactions**, paid by your wallet in Base ETH for this example. +Finalization returns tokens to the payer; it is not automatic. + +Save withdraw.mts in the same project directory. +In `.env`, set `CHANNEL` to the channel-file path printed by the quote example. +Keep `RPC_URL` on that channel's payment chain. + +```text +CHANNEL=./x402-channels/YOUR_CHANNEL_ID.json +``` + +
+Complete withdrawal example + + + {withdrawSource} + + +
+ +**1. Check status** without spending gas: + +```bash +node --env-file=.env --import tsx withdraw.mts status +``` + +**2. Stop using the channel and initiate** the withdrawal: + +```bash +node --env-file=.env --import tsx withdraw.mts initiate +node --env-file=.env --import tsx withdraw.mts status +``` + +**3. Wait until `finalizeAt`**, calculated from the on-chain initiation time and that channel's original delay. +A new offer cannot change it. + +**4. Finalize**, then check the receipt and your token balance: + +```bash +node --env-file=.env --import tsx withdraw.mts finalize +node --env-file=.env --import tsx withdraw.mts status +``` + +CoW may collect outstanding charges during the wait, reducing the returned amount. +Collection happens only when economic; uncollected fees do not extend the withdrawal deadline. +A pending withdrawal cannot be cancelled. +If a transaction times out, check its hash and status before sending another. +Resolve uncertain payments before withdrawing. + +## Other payment options and help + +The current options are Base USDC/COW, Ethereum USDC and BNB USDC. +COW and BNB USDC require an explicit SDK asset entry and Permit2 approval. +Approval and withdrawal gas is paid in the payment chain's native token. +COW amounts have no guaranteed dollar equivalent. + +Use the [live API reference](https://x402.cow.fi/docs/) for payment fields, networks and errors. +Contact [CoW Protocol Discord](https://discord.com/invite/cowprotocol) with your network, channel ID and transaction hash; +never share keys or signed payments. diff --git a/docs/cow-protocol/integrate/x402.mdx b/docs/cow-protocol/integrate/x402.mdx index 338ed13fb..f5586741a 100644 --- a/docs/cow-protocol/integrate/x402.mdx +++ b/docs/cow-protocol/integrate/x402.mdx @@ -5,14 +5,15 @@ sidebar_position: 4 --- import CodeBlock from '@theme/CodeBlock' -import inspectSource from '!!raw-loader!@site/static/code-examples/x402/inspect.mjs' import quoteSource from '!!raw-loader!@site/static/code-examples/x402/quote.mts' -import withdrawSource from '!!raw-loader!@site/static/code-examples/x402/withdraw.mts' # Paid quotes with x402 Get CoW Protocol quotes from a script or agent without an API key. Deposit tokens into a prepaid channel, then authorise each quote with an off-chain signature. The deposit funds future requests; it is not an extra quote fee. CoW collects the charges in batches. +The x402 SDK handles the payment challenge, signing, paid retry and receipt validation. +Your application configures a payer and spending limits, then uses a payment-aware `fetch`. + ## Get a quote This example uses **Node.js 22** and **USDC on Base** to buy an Ethereum quote. @@ -25,17 +26,7 @@ Save inspect.mj It shows the current price, minimum deposit and withdrawal delay without a key, signature or payment. Fund your wallet with enough Base USDC if you accept the terms. -
-Read-only terms preview - - - {inspectSource} - - -
- -For illustration, a 1 USDC deposit at 0.001 USDC per quote funds 1,000 quotes while that price is unchanged. -Always read current terms from the service. +Deposits remain available for later quotes until spent or withdrawn. ### 2. Set up the example @@ -60,39 +51,32 @@ RPC_URL=YOUR_BASE_RPC_URL Keep `.env`, `x402-state/`, `x402-channels/` and `x402-attempts/` out of source control. Saved signed attempts are sensitive too. -The example defaults to **0.001 USDC per quote**, **1 USDC per deposit or top-up**, and a **24-hour maximum accepted withdrawal delay**. These are client limits, not guaranteed server terms. If an offer exceeds them, the script stops before signing. - -
-Optional limits - -```text -# Optional overrides; these are the example's defaults. -MAX_QUOTE_USDC=0.001 -MAX_DEPOSIT_USDC=1 -MAX_WITHDRAW_DELAY_HOURS=24 -``` - -`MAX_WITHDRAW_DELAY_HOURS` only limits which offers your client accepts. It does not choose the waiting period: a new channel takes the delay advertised by the server, and keeps that delay for its lifetime. You can omit this setting; the example accepts delays up to 24 hours. - -Enter price and deposit limits in human USDC units. -A per-deposit limit applies to each top-up; it is not a lifetime spending cap. - -
+The runnable example defaults to **0.001 USDC per quote**, **1 USDC per deposit or top-up**, and a **24-hour maximum accepted withdrawal delay**. These are client limits, not guaranteed server terms. If an offer exceeds them, the script stops before signing. +See [optional limits](./x402-recovery.mdx#client-limits) to change them. A per-deposit limit is not a lifetime spending cap. ### 3. Request a quote -Save
quote.mts in the project directory. -It checks payment terms, uses SDK receipt validation and saves channel state and recovery files. -The example uses `@x402/*` **2.26.0**. - -
-Complete quote example +The core SDK setup is below. This is an **excerpt**, not a standalone script: +the [complete example](./x402-recovery.mdx#complete-quote-example) creates the RPC-backed +`signer`, selects Base USDC, checks the expected receiver and your limits, and saves recovery files. - - {quoteSource} + + {quoteSource.slice(quoteSource.indexOf('const client ='), quoteSource.indexOf('// Sign only')).trim()} -
+Once that client is configured, request a quote with `paidFetch`: + +```typescript +const paidFetch = wrapFetchWithPayment(fetch, client); +const response = await paidFetch("https://x402.cow.fi/mainnet/api/v1/quote", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(quoteRequest), // Standard orderbook quote parameters. +}); +``` + +To run it, save quote.mts +in the project directory. It includes the imports, payment limits and a complete quote request. ```bash node --env-file=.env --import tsx quote.mts @@ -104,13 +88,9 @@ CoW pays deposit and collection gas. Run one request at a time per channel and keep the saved files across restarts. -:::info Uncertain payment outcomes - -If a request times out or returns a settlement error, keep the state, signed attempt and transaction hash. A charge or deposit may have succeeded even without a quote response. Do not repeatedly rerun it or create another deposit; check the original transaction and use the SDK’s validated recovery. Each new successful request can incur another charge. -The [API reference](https://x402.cow.fi/docs/) describes the payment response and recovery errors. -Never reset your charged total to on-chain claims alone: they omit charges awaiting collection. - -::: +If a request times out or returns a settlement error, keep the saved files and transaction hash. +A charge or deposit may have succeeded even without a quote response. +Follow the [recovery guidance](./x402-recovery.mdx#recover-an-uncertain-payment) before retrying. ## Withdraw unused funds @@ -118,51 +98,9 @@ You initiate a withdrawal, wait for the saved channel's deadline, then finalize These are **two transactions**, paid by your wallet in Base ETH for this example. Finalization returns tokens to the payer; it is not automatic. -Save withdraw.mts in the same project directory. -In `.env`, set `CHANNEL` to the channel-file path printed by the quote example. -Keep `RPC_URL` on that channel's payment chain. - -```text -CHANNEL=./x402-channels/YOUR_CHANNEL_ID.json -``` - -
-Complete withdrawal example - - - {withdrawSource} - - -
- -**1. Check status** without spending gas: - -```bash -node --env-file=.env --import tsx withdraw.mts status -``` - -**2. Stop using the channel and initiate** the withdrawal: - -```bash -node --env-file=.env --import tsx withdraw.mts initiate -node --env-file=.env --import tsx withdraw.mts status -``` - -**3. Wait until `finalizeAt`**, calculated from the on-chain initiation time and that channel's original delay. -A new offer cannot change it. - -**4. Finalize**, then check the receipt and your token balance: - -```bash -node --env-file=.env --import tsx withdraw.mts finalize -node --env-file=.env --import tsx withdraw.mts status -``` - -CoW may collect outstanding charges during the wait, reducing the returned amount. -Collection happens only when economic; uncollected fees do not extend the withdrawal deadline. -A pending withdrawal cannot be cancelled. -If a transaction times out, check its hash and status before sending another. -Resolve uncertain payments before withdrawing. +Follow the [withdrawal walkthrough](./x402-recovery.mdx#withdraw-unused-funds) using +the original channel-file path printed by the quote example. +Claims during the waiting period can reduce the amount returned. ## Other payment options and help diff --git a/static/code-examples/x402/quote.mts b/static/code-examples/x402/quote.mts index 9c77fe13c..9823523ca 100644 --- a/static/code-examples/x402/quote.mts +++ b/static/code-examples/x402/quote.mts @@ -107,7 +107,7 @@ try { savedAttempt: latestAttemptFile, body, }); - throw new Error("Payment outcome needs checking; follow Troubleshooting before retrying"); + throw new Error("Payment outcome needs checking; follow the recovery guide before retrying"); } const result = JSON.parse(body); if (!result.quote) throw new Error("Expected a quote in the successful response"); @@ -122,6 +122,6 @@ try { }); console.log(result); } catch (error) { - console.error({ savedAttempt: latestAttemptFile, message: "Keep state and check Troubleshooting before rerunning an unresolved attempt." }); + console.error({ savedAttempt: latestAttemptFile, message: "Keep state and check the recovery guide before rerunning an unresolved attempt." }); throw error; } diff --git a/tsconfig.json b/tsconfig.json index 173d88cf8..a8ae6a1d7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { // This file is not used in compilation. It is here just for a nice editor experience. "extends": "@docusaurus/tsconfig", - "exclude": ["static/code-examples/x402"], + "exclude": ["node_modules", ".docusaurus", "build", "static/code-examples/x402"], "compilerOptions": { "baseUrl": ".", "resolveJsonModule": true, From c78a5e0207ae11ccbcbaa5adad95a6570ee8dfe5 Mon Sep 17 00:00:00 2001 From: Anuj Bansal Date: Mon, 19 Oct 2026 15:11:22 +0530 Subject: [PATCH 3/3] Clarify x402 payment recovery and service terminology --- docs/cow-protocol/integrate/x402-recovery.mdx | 59 ++++++++++++++++--- docs/cow-protocol/integrate/x402.mdx | 10 ++-- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/docs/cow-protocol/integrate/x402-recovery.mdx b/docs/cow-protocol/integrate/x402-recovery.mdx index d35ae0e32..51a60122a 100644 --- a/docs/cow-protocol/integrate/x402-recovery.mdx +++ b/docs/cow-protocol/integrate/x402-recovery.mdx @@ -81,17 +81,58 @@ node --env-file=.env --import tsx quote.mts A successful run prints `status: 200`, `quoteReceived: true`, the charge, a saved channel-file path and the quote JSON. The first request funds the channel; later requests reuse it and top up when needed, within your limits. -CoW pays deposit and collection gas. +The CoW Protocol x402 payment service pays deposit and collection gas. Run one request at a time per channel and keep the saved files across restarts. ## Recover an uncertain payment -:::info Keep the original attempt +The SDK handles a corrective `402` inside `wrapFetchWithPayment`: it validates the returned channel/voucher state, updates local storage and retries once with a fresh payment. This can produce a new charged quote; it does not retrieve a lost quote response. +If the script still fails or the connection drops, follow these steps before rerunning it or withdrawing. -If a request times out or returns a settlement error, keep the state, signed attempt and transaction hash. A charge or deposit may have succeeded even without a quote response. Do not repeatedly rerun it or create another deposit; check the original transaction and use the SDK’s validated recovery. Each new successful request can incur another charge. -The [API reference](https://x402.cow.fi/docs/) describes the payment response and recovery errors. -Never reset your charged total to on-chain claims alone: they omit charges awaiting collection. +**1. Stop requests on this channel and keep the evidence.** Preserve `x402-state/`, `x402-channels/`, all `x402-attempts/` files from the run, the error output and any transaction hash. Use the `savedAttempt` path printed by the error in step 2. Do not delete state, manually lower its charged total or replay the signed attempt to check its status. + +**2. Inspect the saved attempt and local state.** Run this from the same project directory, replacing the attempt filename. It reads local files without signing or sending a request: + +```bash +ATTEMPT=./x402-attempts/YOUR_ATTEMPT.json node --input-type=module <<'JS' +import { readFileSync } from "node:fs"; +import { getChannel } from "@x402/evm/batch-settlement/client"; +import { FileClientChannelStorage } from "@x402/evm/batch-settlement/client/file-storage"; + +const attempt = JSON.parse(readFileSync(process.env.ATTEMPT, "utf8")); +const channelId = attempt.payload.voucher.channelId; +const storage = new FileClientChannelStorage({ directory: "./x402-state" }); +const local = await getChannel(storage, channelId); +console.log({ + channelId, + paymentNetwork: attempt.accepted.network, + paymentType: attempt.payload.type, + signedLimitAtomic: attempt.payload.voucher.maxClaimableAmount, + recordedChargesAtomic: local?.chargedCumulativeAmount ?? null, +}); +JS +``` + +The signed limit is a cumulative authorization cap, not proof of a charge. `recordedChargesAtomic` is the client's last recorded total; it may be stale after a lost response. A missing record does not mean the payment failed. Keep the signed attempt private. + +**3. Check the chain without changing it.** Save withdraw.mts. Set `CHANNEL=./x402-channels/YOUR_CHANNEL_ID.json` in `.env` using the channel ID above, and keep `RPC_URL` on the payment chain. Then run: + +```bash +node --env-file=.env --import tsx withdraw.mts status +``` + +This reads the channel balance, claimed total and withdrawal status; it sends no transaction and needs no private key. If the original error included a transaction hash, also check its receipt in the payment chain's explorer. A pending or missing receipt remains unresolved. A successful deposit proves that funds reached the channel, not that the quote was charged. The on-chain claimed total omits charges awaiting collection. + +**4. Establish the payment outcome.** A completed example run prints `status: 200`, `quoteReceived: true` and `chargedUSDC` from the payment receipt. If you have that output, that request succeeded and another run is a new purchase. If the receipt was lost, failed validation, or the script reported an accounting/settlement error, the checks above cannot establish whether that request was charged. + +Contact [CoW Protocol Discord](https://discord.com/invite/cowprotocol) with the payment network, channel ID, approximate request time, error code and any transaction hash. Ask the team to confirm the request's charge and reconcile the channel state before another payment or withdrawal. Do not post your private key, signed attempt or full state files. There is no public read-only payment-status endpoint for this check. + +**5. Resume only after the uncertainty is resolved.** Keep using the reconciled state and original channel configuration. Each successful new quote can incur another charge. If you are finished with the channel, follow the withdrawal steps below. + +:::warning Do not reset state from on-chain claims + +SDK 2.26.0's `recoverChannel()` rebuilds the charged total from on-chain claims. It is not a general repair for a lost response while uncollected charges may exist. The normal corrective-`402` path uses `processCorrectivePaymentRequired()` through the SDK hooks; do not bypass those checks or manually edit the charged total. ::: @@ -141,7 +182,7 @@ node --env-file=.env --import tsx withdraw.mts finalize node --env-file=.env --import tsx withdraw.mts status ``` -CoW may collect outstanding charges during the wait, reducing the returned amount. +The x402 payment service may collect outstanding charges during the wait, reducing the returned amount. Collection happens only when economic; uncollected fees do not extend the withdrawal deadline. A pending withdrawal cannot be cancelled. If a transaction times out, check its hash and status before sending another. @@ -149,10 +190,10 @@ Resolve uncertain payments before withdrawing. ## Other payment options and help -The current options are Base USDC/COW, Ethereum USDC and BNB USDC. -COW and BNB USDC require an explicit SDK asset entry and Permit2 approval. +The current options are USDC or \$COW on Base, USDC on Ethereum and USDC on BNB. +\$COW and BNB USDC require an explicit SDK asset entry and Permit2 approval. Approval and withdrawal gas is paid in the payment chain's native token. -COW amounts have no guaranteed dollar equivalent. +\$COW amounts have no guaranteed dollar equivalent. Use the [live API reference](https://x402.cow.fi/docs/) for payment fields, networks and errors. Contact [CoW Protocol Discord](https://discord.com/invite/cowprotocol) with your network, channel ID and transaction hash; diff --git a/docs/cow-protocol/integrate/x402.mdx b/docs/cow-protocol/integrate/x402.mdx index f5586741a..275948872 100644 --- a/docs/cow-protocol/integrate/x402.mdx +++ b/docs/cow-protocol/integrate/x402.mdx @@ -9,7 +9,7 @@ import quoteSource from '!!raw-loader!@site/static/code-examples/x402/quote.mts' # Paid quotes with x402 -Get CoW Protocol quotes from a script or agent without an API key. Deposit tokens into a prepaid channel, then authorise each quote with an off-chain signature. The deposit funds future requests; it is not an extra quote fee. CoW collects the charges in batches. +Get CoW Protocol quotes from a script or agent without an API key. Deposit tokens into a prepaid channel, then authorise each quote with an off-chain signature. The deposit funds future requests; it is not an extra quote fee. The CoW Protocol x402 payment service collects the charges in batches. The x402 SDK handles the payment challenge, signing, paid retry and receipt validation. Your application configures a payer and spending limits, then uses a payment-aware `fetch`. @@ -84,7 +84,7 @@ node --env-file=.env --import tsx quote.mts A successful run prints `status: 200`, `quoteReceived: true`, the charge, a saved channel-file path and the quote JSON. The first request funds the channel; later requests reuse it and top up when needed, within your limits. -CoW pays deposit and collection gas. +The CoW Protocol x402 payment service pays deposit and collection gas. Run one request at a time per channel and keep the saved files across restarts. @@ -104,10 +104,10 @@ Claims during the waiting period can reduce the amount returned. ## Other payment options and help -The current options are Base USDC/COW, Ethereum USDC and BNB USDC. -COW and BNB USDC require an explicit SDK asset entry and Permit2 approval. +The current options are USDC or \$COW on Base, USDC on Ethereum and USDC on BNB. +\$COW and BNB USDC require an explicit SDK asset entry and Permit2 approval. Approval and withdrawal gas is paid in the payment chain's native token. -COW amounts have no guaranteed dollar equivalent. +\$COW amounts have no guaranteed dollar equivalent. Use the [live API reference](https://x402.cow.fi/docs/) for payment fields, networks and errors. Contact [CoW Protocol Discord](https://discord.com/invite/cowprotocol) with your network, channel ID and transaction hash;