A spending cap your agent cannot read is a wall it walks into. The agent sends a payment, the policy refuses it, the agent retries the same call, and its step budget goes on an error it could have predicted. The fix is not a bigger cap. It is one GET before the send.
OpenClawCash exposes that read. Two endpoints return the policies on a wallet together with how much of each one is already used.
What a policy looks like when you read it
The public docs show this shape for both policy endpoints:
[ { "wallet": { "id": "Q7X2K9P", "label": "Trading Bot", "address": "0x14ae8d93...", "network": "sepolia", "chain": "evm" }, "policies": [ { "id": 31, "type": "daily_spending_limit", "config": { "amount": "100" }, "createdAt": "2026-01-15T10:00:00.000Z", "usage": { "spent": "45.00", "limit": "100.00", "symbol": "USD", "decimals": 2, "window": "24h" } } ] } ] That usage object is the part worth noticing. The response is hydrated, so spent and limit arrive with the policy. Your agent does not need a second call to work out how much of today's budget is gone. In the example above the wallet has 100 units of daily room and has spent 45 of them.
Step 1: list policies across every wallet the key can see
curl https://openclawcash.com/api/agent/policies \ -H "X-Agent-Key: occ_your_api_key" The answer is an array of { wallet, policies }, one entry per wallet reachable by that API key. This is the call to make at the start of a batch, so the agent knows the budget for every wallet it is about to spend from, in one request.
Step 2: read one wallet
curl "https://openclawcash.com/api/agent/policy?walletId=Q7X2K9P" \ -H "X-Agent-Key: occ_your_api_key" Use walletId from GET /api/agent/wallets when you have it. The docs also accept walletLabel or walletAddress, and an optional chain of evm or solana:
curl "https://openclawcash.com/api/agent/policy?walletLabel=Trading%20Bot&chain=evm" \ -H "X-Agent-Key: occ_your_api_key" Step 3: gate the transfer in your own code
The point of the read is to keep the agent out of a refusal it can see coming. This is the whole loop, with the exact fields the docs list:
const base = "https://openclawcash.com"; const headers = { "X-Agent-Key": process.env.OCC_AGENT_KEY }; async function sendIfRoom(walletId, to, amountDisplay) { const res = await fetch(`${base}/api/agent/policy?walletId=${walletId}`, { headers }); const { policies } = await res.json(); const daily = policies.find((p) => p.type === "daily_spending_limit"); if (!daily) throw new Error("no daily policy on this wallet"); const spent = Number(daily.usage.spent); const limit = Number(daily.usage.limit); const amount = Number(amountDisplay); if (spent + amount > limit) { return { sent: false, reason: "over daily limit", spent, limit }; } const send = await fetch(`${base}/api/agent/transfer`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ chain: "evm", walletId, to, token: "USDC", amountDisplay }), }); return send.json(); } Two commands and one comparison. The agent now reasons about its budget instead of discovering it through a 401.
What this does not do
Be honest about the seam here.
- The check above is advisory. Enforcement is server side, before anything is signed, so the policy still has the final word if the numbers move between the read and the send. The read reduces wasted steps. It does not replace the gate.
-
spentandlimitcome back as strings, because they carry decimals. Compare them as numbers, never as text. - A wallet can carry more than one policy, and the windows differ. Filter by
typeand readusage.windowrather than taking the first entry in the array. - If the header is missing you get a different problem entirely. A call without the key answers
missing_api_key, and the body names the header it wanted:X-Agent-Key.
The docs for both endpoints are at openclawcash.com/docs, next to the wallet and transfer calls this loop uses.