You cap what an agent pays over x402 with applySpendControls from @coinbase/cdp-sdk/x402,
and the agent restarts or runs as several replicas. CDP keeps its running total in a
SpendStore, and the default store is in memory, in one process. budget.cdpStore() is
walletcap’s SQLite ledger behind CDP’s SpendStore interface, so CDP’s total lives in one
budget file that every replica and every restart opens.
store line in applySpendControls$ npm install walletcap @coinbase/cdp-sdk @x402/core @x402/evm viem
$ npx walletcap set --wallet 0xYourAgentWallet --per-payment 50 --per-day 150
import { openBudget } from 'walletcap';
import { applySpendControls } from '@coinbase/cdp-sdk/x402';
import { x402Client } from '@x402/core/client';
import { ExactEvmScheme } from '@x402/evm/exact/client';
import { privateKeyToAccount } from 'viem/accounts';
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // USDC on Base
const account = privateKeyToAccount(process.env.AGENT_KEY);
const budget = openBudget({ wallet: account.address }); // the budget `walletcap set` created
const client = new x402Client().register('eip155:8453', new ExactEvmScheme(account));
// As CdpX402Client does: CDP's caps below replace x402's own $1-per-payment default.
client.setSpendControls(false);
applySpendControls(client, {
maxAmountPerPayment: { atomic: 50_000_000n, asset: USDC },
maxCumulativeSpend: { atomic: 150_000_000n, asset: USDC },
maxCumulativeSpendWindow: '24h',
allowedAssets: [USDC],
store: budget.cdpStore({ asset: USDC }),
});
maxCumulativeSpend: CDP only records through its store when a cumulative cap is set.allowedAssets: CDP never consults its store for a token outside the cap.budget.guard(). Each
records the payment, so with both the budget would count it twice. budget.guard() refuses a
client that already has CDP controls, and a client guarded first refuses CDP controls before
signing.npx walletcap release <id> gives it back sooner, and so does
npx walletcap release --all-unknown --older-than 24h once that hour and a minute has passed.maxTimeoutSeconds over 3600, which
the store’s hour relies on. The test suite drives the published @coinbase/cdp-sdk 1.60.0
through the store, and the crash test runs 1.61.0.cdpStorenpx walletcap-crash-test runs CDP’s controls set as Coinbase’s README shows, then runs them
again set as above, with store: budget.cdpStore(). Every agent is a separate Node process,
held to $50 a payment and $150 in any 24 hours, in USDC on Base. Results from 6 November 2026:
| Event | CDP’s controls | CDP’s controls on walletcap cdpStore |
|---|---|---|
| restart, then pay again | $300 signed · $150 over | $150 signed · $0 over |
| 10 replicas | $1,500 signed · $1,350 over | $150 signed · $0 over |
| 10 replicas, shared store 1 ms away | $350 signed · $200 over | $150 signed · $0 over |
| payee asks for EURC, not USDC | 1,000 EURC signed, counted as $0 | refused before signing |
The shared-store row varies from run to run: in eight runs it was $100 over five times and $200 over three times. The cdpStore column was $0 over in every run. In the shared-store row, walletcap’s store also answers each call after 1 ms.