This walkthrough shows how to purchase a product through Rye’s x402 endpoint using the AgentCash client. AgentCash handles the 402 → sign → retry loop transparently, so your code reads like a normal HTTP call.
Prerequisites
- An AgentCash wallet with a small USDC balance on Base (≥ $1 is plenty for testing — covers the access fees and a low-priced test purchase)
- A product URL from a supported merchant (Shopify storefronts, etc.)
- The AgentCash CLI or SDK — follow the AgentCash setup guide to install and initialize a wallet
The first time you initialize AgentCash it generates a wallet and stores keys at ~/.agentcash/wallet.json (EVM) and ~/.agentcash/solana-wallet.json (Solana). Top up the wallet with USDC on the network you plan to use before making any paid call.
End-to-end purchase
The example below uses @agentcash/fetch — the programmatic AgentCash SDK. Its executeFetch function handles the 402 → sign → retry loop transparently, so your code reads like a normal HTTP call.
npm install @agentcash/fetch @agentcash/networks @solana/kit viem
import { privateKeyToAccount } from "viem/accounts";
import { generateKeyPairSigner } from "@solana/kit";
import { executeFetch, PaymentProtocol } from "@agentcash/fetch";
import { Network } from "@agentcash/networks";
const baseUrl = "https://x402.rye.com";
// AgentCash requires both wallets in the type even for an EVM-only flow.
const evm = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY as `0x${string}`);
const svm = await generateKeyPairSigner();
const params = {
paymentProtocol: PaymentProtocol.X402,
paymentNetwork: Network.BASE,
};
// 1. Create a checkout intent — pays $0.02 over x402 automatically.
const create = await executeFetch(
{
url: `${baseUrl}/v1/checkout-intents`,
method: "POST",
headers: {},
body: JSON.stringify({
productUrl: "https://shop.example.com/running-shoes",
quantity: 1,
buyer: {
firstName: "Jane",
lastName: "Doe",
email: "jane@example.com",
phone: "+14155551234",
address1: "123 Market St",
city: "San Francisco",
province: "CA",
country: "US",
postalCode: "94103",
},
}),
},
{ wallets: { evm, svm }, params, pickProtocol: async () => PaymentProtocol.X402 },
);
const created = await create.response.json();
const intentId = created.id;
// create.paymentInfo.payment.transactionHash is the on-chain settle tx
// 2. Poll for the offer — free, no signature required.
async function pollUntil(targetState: string) {
while (true) {
const r = await fetch(`${baseUrl}/v1/checkout-intents?id=${intentId}`, {
headers: { "X-Wallet-Address": evm.address },
});
const intent = await r.json();
if (intent.state === targetState || intent.state === "failed") return intent;
await new Promise((res) => setTimeout(res, 3000));
}
}
const ready = await pollUntil("awaiting_confirmation");
if (ready.state === "failed") throw new Error(ready.failureReason?.message);
// 3. Inspect the offer and decide whether to proceed.
const totalSubunits = ready.offer.cost.total.amountSubunits;
const budgetSubunits = 15000; // $150.00
if (totalSubunits > budgetSubunits) throw new Error("over budget");
// 4. Confirm — pays purchase total + $0.03 over x402 in a single signed authorization.
await executeFetch(
{
url: `${baseUrl}/v1/checkout-intents/confirm`,
method: "POST",
headers: {},
body: JSON.stringify({
id: intentId,
paymentMethod: { type: "x402", network: "base" },
}),
},
{ wallets: { evm, svm }, params, pickProtocol: async () => PaymentProtocol.X402 },
);
// 5. Poll for completion.
const final = await pollUntil("completed");
console.log("order placed:", final.orderId);
Prefer the CLI or an MCP-installed agent? AgentCash also ships npx agentcash fetch <url> and an MCP server (claude mcp add agentcash --scope user -- npx -y agentcash@latest). Both speak the same canonical x402 v2 against this endpoint.
What happens behind the scenes
executeFetch receives a 402 Payment Required from the proxy with a PAYMENT-REQUIRED header. It signs an EIP-3009 TransferWithAuthorization (off-chain only — the buyer never broadcasts) and retries the same request with a PAYMENT-SIGNATURE header.
- The proxy verifies the signature, broadcasts
transferWithAuthorization on Base, and pays the gas. On the success response it returns an X-PAYMENT-RESPONSE header carrying the on-chain transaction hash, which executeFetch exposes as paymentInfo.payment.transactionHash.
- For step 1 the authorization is for 0.02.Forstep4itcoverstheoffer′spurchasetotalplusthe0.03 API fee — bundled into a single signed authorization.
GET calls in steps 2 and 5 are free and use a normal fetch. The X-Wallet-Address header scopes the read to the wallet that paid for the intent.
- After step 4 returns, Rye places the order asynchronously. The intent moves to
placing_order, then to completed or failed. See Checkout Intent Lifecycle for the full state machine.
Timing
| Step | Typical latency |
|---|
| Create intent (incl. on-chain settle) | 1–3 s |
| Offer retrieval | 5–15 s |
| Confirm (incl. on-chain settle) | 1–3 s |
| Order placement | 30–60 s typical |
Order placement runs asynchronously — your code does not block while Rye places the order at the merchant. Keep polling GET /v1/checkout-intents?id=… until you see state: "completed" or state: "failed".
Failure modes
- Insufficient wallet balance — the on-chain transfer fails; the intent moves to
failed and no order is placed.
- Offer retrieval fails (out of stock, unsupported product) —
state: "failed" after step 2; the $0.02 access fee is not refunded because the offer retrieval work was performed.
- Order placement fails at the merchant —
state: "failed" after step 4; the purchase amount is automatically refunded to the signing wallet on-chain.
- Signature expired between calls — the retry returns
400 with a fresh PAYMENT-REQUIRED; executeFetch re-signs and retries automatically.
See the Endpoint Reference for full request and response shapes, including how to call the API without the AgentCash SDK.