From aca5789dc9db04946c5d0263656343c468b3a29f Mon Sep 17 00:00:00 2001 From: Anuj Bansal Date: Wed, 7 Oct 2026 21:26:15 +0530 Subject: [PATCH] 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"