Skip to main content
Yanked

This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 0.3.0 instead.

walletcap

CircleCI

Your agent's spending limit forgets. This one doesn't. By ShadowPath.

Agent payment kits count "spent so far" per process, per key, per permission or per period. So a restart, a second replica, one more key, or the next period hands the agent a fresh budget. walletcap counts per wallet: one budget that every process, replica, key, permission and payment rail spends from.

import { openBudget } from 'walletcap';

const budget = openBudget({ wallet: account.address, perPayment: '$50', perDay: '$150' });

await budget.pay('$12.50', () => sendUsdc(payee, '12.50')); // refused past $150 in any 24h, restarts included
from walletcap import open_budget

budget = open_budget(agent_address, per_payment="$50", per_day="$150")

with budget.pay("$12.50"):
    send_usdc(payee, "12.50")

npm install walletcap (Node 22.13+) · pip install walletcap (Python 3.11+) · no dependencies · Apache-2.0

Why a per-wallet count

Most agent spend controls keep "spent so far" next to something temporary or repeatable: a process, a browser window, an API key, a delegate, one permission, or a fixed calendar period. The money, though, sits in one wallet. When the count and the wallet don't match, ordinary events (a restart, a second replica, another key or permission, the next period) can hand the agent a fresh budget against the same money.

ShadowPath measures spend controls against that pattern, on owned test chains with the vendors' own code. Findings go to the vendors first; this package is the fix, not the findings.

The fix: four rules

  1. Count per wallet. The budget belongs to the account the money leaves from, not to a process, key, permission or delegate. Every payment from that wallet reserves against one number. Checksummed, lowercase and CAIP-10 forms of an address are one wallet, so they can't split it.
  2. Count over a rolling window. "$150 a day" means any 24 hours: there is no period boundary to straddle.
  3. Changing a limit never resets the count. Raising $150 to $200 after spending $150 leaves $50, not $200. A restart can lower a limit but never raise one: if the code comes back asking for looser limits (a kit's defaults, say), the stored limits stay and you get a warning with the command that raises them on purpose.
  4. Fail closed. If the count can't be read, nothing pays. A payment whose outcome is unknown keeps counting, because a payment that failed halfway may still have moved money.

The count lives in a SQLite file that every process on the machine shares, and each payment is reserved inside one write transaction, so replicas racing for the last dollar can't both win. JavaScript and Python read and write the same file.

Use it

Any payment, any rail

pay() reserves the amount, runs your payment, and records it as settled.

const tx = await budget.pay('$5', async reservation => {
  return wallet.sendTransaction(request); // reservation.id makes a good idempotency key
}, { to: payee, note: 'weather API' });

If the payment throws, it still counts, because it may have left the wallet. If you know it didn't, say so and the amount comes back:

import { notSent } from 'walletcap';

throw notSent(error); // inside the payment function

In Python, raise NotSent("card declined") inside the with block. Set payment.tx in the block to keep the transaction hash in the history, and use async with in async code.

budget.check('$5') answers { ok, reason, remaining } without spending. budget.reserve() gives you a reservation to commit() or release() yourself.

x402

guard() puts the budget in front of any x402 v2 client from @x402/core, so it works under @x402/fetch and alongside Coinbase's CDP spend controls.

import { x402Client } from '@x402/core/client';
import { ExactEvmScheme } from '@x402/evm/exact/client';
import { wrapFetchWithPayment } from '@x402/fetch';

const client = budget.guard(new x402Client().register('eip155:8453', new ExactEvmScheme(account)));
const fetchPaid = wrapFetchWithPayment(fetch, client);
  • Each payment is checked before it is signed, and reserved once its payload exists and every other control has passed.
  • It counts as spent when the server confirms settlement. A payee that answers "payment failed" still holds your signed authorization and could settle it until it expires, so the payment keeps counting until then.
  • USDC on Base, Ethereum, Polygon, Arbitrum, Optimism, Avalanche and Solana is priced out of the box, plus Base Sepolia, Arbitrum Sepolia and Solana devnet. Add another USD stablecoin with budget.guard(client, { assets: [{ network, asset, decimals }] }). Anything else is refused, not guessed.
  • @x402/fetch rewraps errors from payment creation, so a refusal reaches you as an Error whose message starts Failed to create payment payload: and carries the budget's reason.

Coinbase CDP spend controls

applySpendControls builds on an x402 client, so budget.guard(client) works alongside it, and it's the one to use:

  • It counts every token. guard() prices the USD stablecoins it knows and refuses anything else, whatever assets the client would otherwise accept.
  • It reserves in one transaction. Each payment is checked and reserved inside one write transaction on the budget file, so processes and replicas sharing the file can't both take the last dollar.
  • It keeps counting what the payee calls failed until the authorization expires, then frees it on its own.

To keep CDP's own count durable and shared as well, swap its in-memory store for walletcap's, set maxCumulativeSpend (CDP only records through its store when a cumulative cap is set), and set allowedAssets: CDP never consults its store for a token outside the cap.

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 }),
});
budget.guard(client);

walletcap's store re-checks the budget when CDP records a payment, so it holds across replicas on its own. When CDP removes a payment from its store, the store can't tell why, so the payment stays counted as in-flight until you check the chain and run npx walletcap release <id>. Use CDP 1.56.0 or later.

Coinbase Agentic Wallet (awal CLI and MCP server)

Run the Agentic Wallet's CLI and MCP server through walletcap, and every payment counts in the wallet's budget, whichever process, session or machine makes it:

$ npx walletcap set --wallet 0xYourAgentWallet --per-payment 0.05 --per-day 0.15
$ npx walletcap awal x402 pay https://api.example.com/data   # instead of: npx awal x402 pay …
$ npx walletcap awal send 0.05 0xRecipient --chain base       # instead of: npx awal send …

For an MCP client such as Claude Desktop, start Coinbase's server behind walletcap. It runs npx -y @coinbase/payments-mcp; put another command after --.

{ "mcpServers": { "payments": { "command": "npx", "args": ["walletcap", "awal-mcp", "--wallet", "0xYourAgentWallet"] } } }
  • send and x402 pay (over MCP, send and make_http_request_with_x402) are checked against the wallet's budget before the wallet signs anything, and counted. Every other command and tool is the wallet's own.
  • An x402 payment is priced from the endpoint's own 402 answer, and the wallet is capped at exactly that amount, so it refuses to sign for more.
  • A payment the payee confirms as settled counts for one window. One it doesn't confirm keeps counting until you release it: the wallet signs whatever validity the payee asks for, and walletcap can't see it. Check the chain, then npx walletcap release <id>.
  • The wallet's own refusal, before it signs, gives the amount back. Any other failure keeps counting.
  • Only USDC is counted; a send in any other asset is refused.
  • Machines share a budget when they share its file (a shared volume). Separate hosts without one wait for the Postgres backend.
  • It binds the payments that go through it. The wallet app takes requests from any process of the same user, so keep the real awal out of an agent's reach.

Coinbase AgentKit

AgentKit's x402 actions build their own x402 client on every call. Put the budget on that client with a three-line subclass, and every payment is checked against the wallet's budget before it is signed, whatever terms the merchant sends:

import { X402ActionProvider } from '@coinbase/agentkit';

class BudgetedX402ActionProvider extends X402ActionProvider {
  async createX402Client(walletProvider) {
    return budget.guard(await super.createX402Client(walletProvider));
  }
}
// AgentKit.from({ walletProvider, actionProviders: [new BudgetedX402ActionProvider()] })

Coinbase Spend Permissions, on chain

For a limit the agent's own code can't go around, solidity/src/AccountCapSpender.sol is the spender for Spend Permissions. Deploy it with the SpendPermissionManager, the agent's address, the cap, the window, the per-payment max and the payees allowed. Grant Spend Permissions to the contract instead of the agent; the agent calls pay(permission, amount, payee). Every permission the account grants it then shares one rolling cap.

The owner's controls

$ npx walletcap status
Budget file /home/you/.local/state/walletcap/budget.sqlite

0xd797…ca02: $0.15 spent of $0.15 per 24h, $0.00 left · at most $0.05 a payment

$ npx walletcap set --per-day 0.20     # what was spent still counts
$ npx walletcap freeze                 # every process, every rail, now
$ npx walletcap unfreeze
$ npx walletcap history
$ npx walletcap release <id>           # an in-flight payment you know never left

Python ships the same commands as walletcap. Amounts are dollars; leave out the $ sign, which your shell would expand. Add --wallet when the file holds more than one budget, and --json for scripts.

Where the budget lives

The default file is per user: ~/.local/state/walletcap/budget.sqlite on Linux, ~/Library/Application Support/walletcap/budget.sqlite on macOS, and %LOCALAPPDATA%\walletcap\budget.sqlite on Windows. Set WALLETCAP_PATH, or pass path, to put it elsewhere. Both languages compute the same default.

The budget is as durable as that file. In a container, put it on a volume that outlives the container, or a redeploy starts a new file. For agents that can touch the filesystem, open the budget with create: false and create it once, as the owner, with npx walletcap set: then a missing file refuses every payment instead of starting a new budget.

What it doesn't do

  • It binds the payments that go through it. An agent with a shell on the same machine can go around it: call the wallet directly, or delete the file. To bound an agent like that, keep the budget out of its reach (another OS user, another service) or enforce on chain, as AccountCapSpender does for Spend Permissions.
  • One file is one host, or a shared volume. A Postgres backend for replicas on separate hosts is next.
  • It can't see a revoke that lands later in the same block, and for x402 schemes that settle later (batch vouchers), a payment counts from when the server confirms it.
  • Some x402 payments can't be confirmed. @x402/fetch doesn't report v1 settlements back, and only the exact EVM scheme carries an expiry the guard can read. An unconfirmed payment keeps counting until its authorization expires plus one window, or, with no expiry, until you release it.
  • Amounts are micro-dollars and must fit in 64 bits.

This repository

Path What it is
js/ the npm package walletcap: the budget, the x402 guard, the CDP store, the CLI, and the Agentic Wallet CLI wrapper and MCP proxy
src/walletcap/ the PyPI package walletcap, on the same file format
solidity/ AccountCapSpender, the on-chain cap for Coinbase Spend Permissions
tests/ the Python suite, including the tests that drive both languages against one budget file
$ cd js && npm install && node --test            # 50 tests
$ uv run --with pytest pytest -q                 # 53 tests, from the repository root
$ cd solidity/evm && npm install && node check.mjs ..   # the contract on an EVM, no Foundry needed
$ cd solidity && forge test                      # the contract's canonical tests, if you have Foundry

The JavaScript suite runs the x402 guard against x402's own client and fetch wrapper, and runs restarts and racing replicas as separate processes, and drives the Agentic Wallet wrapper and MCP proxy against stand-ins for Coinbase's wallet and MCP server. The Python suite covers the ledger, the API, the CLI and one budget shared between both languages. solidity/evm/check.mjs compiles the contract with solc-js and drives it on an in-process EVM, covering what the Foundry tests cover.

One test is not here: the integration test that drives Coinbase's CDP spend controls through the shared store. It runs against a vendored build of Coinbase's SDK and lives in ShadowPath's own repository. Porting it to the published @coinbase/cdp-sdk is open work.

Prove it holds in your deployment

This package closes the gaps ShadowPath measured. Whether your configuration is closed is a measurement: ShadowPath runs your agent with every control switched on and certifies, in dollars, what can still get past your budget, signed and re-checkable offline. Get a certificate for your deployment.

Apache-2.0. Copyright 2026 Coriolis Labs Inc.

Metadata

Release files for walletcap 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for walletcap 0.2.0
File Size Uploaded Provenance Actions
walletcap-0.2.0.tar.gz 89.4 kB Without provenance Details for walletcap-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for walletcap 0.2.0
File Interpreter ABI Platform Actions
walletcap-0.2.0-py3-none-any.whl Python 3 none any Details for walletcap-0.2.0-py3-none-any.whl

Total release size: 121.7 kB

Release files / walletcap-0.2.0.tar.gz

Download URL walletcap-0.2.0.tar.gz
Size 89.4 kB
Tags Source
SHA-256 checksum
How to use checksums
a8fbba11fbb815c45887ae88f0d1584e66b856d1e66b539d3b9e346da4161e5f
BLAKE2b-256 checksum
How to use checksums
71cec9910a6d60ba8691f6fbbd060ca9afcdd7841b987c21e6a7494aa68af372
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / walletcap-0.2.0-py3-none-any.whl

Download URL walletcap-0.2.0-py3-none-any.whl
Size 32.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
927aee7a376a17bfe2edd2db9e0faeaa25c4b381f307b44daea4bca8b65da181
BLAKE2b-256 checksum
How to use checksums
e2d73c03ac8bc73ab7e15e897c03a3dbcf8b1ff2211cd376d79e39208d84d248
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for walletcap 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page