Your agent pays for a service and gets a 401. The obvious fix is to refresh the key and try again. On OpenClawCash that fix is wrong, and an agent that keeps retrying will burn its whole task budget before it gives up.
The 401 in that case is not an authentication failure. It is a wallet policy saying no.
The 401 that is not an auth error
POST /api/agent/transfer reads the wallet's policies before anything is signed. If a limit, an allowlist or a wallet purpose denies the payment, the API answers with HTTP 401 and this body:
{ "code": "policy_violation", "message": "Wallet policy blocked this transaction.", "whatHappened": "Transaction would exceed daily spending limit of $100 USD in the last 24 hours.", "whatToDo": "Use a testnet wallet or adjust wallet policy limits/allowlists, then retry.", "retryable": false, "details": { "network": "mainnet" } } The whatHappened line is the policy engine's own reason. Here it names the daily limit; a per-transfer cap, a whitelisted address or a wallet purpose produces the same policy_violation code with a different sentence.
So branch on body.code, not on the status class. A bad key and a blocked payment share a 401, and only one of them is worth retrying. retryable: false is the API telling you the second case in advance.
The good news is that nothing was signed. The payment was stopped in the request path, before a transaction existed.
Read the budget instead of guessing
A wallet's policies are readable, and for the time windowed ones the response carries current usage:
curl "https://openclawcash.com/api/agent/policy?walletId=Q7X2K9P" \ -H "X-Agent-Key: occ_your_api_key" The docs example response, trimmed to the part that matters:
{ "wallet": { "id": "Q7X2K9P", "label": "Trading Bot", "address": "0x14ae8d93...", "network": "sepolia", "chain": "evm" }, "policies": [ { "id": 31, "type": "daily_spending_limit", "config": { "amount": "100" }, "usage": { "spent": "45.00", "limit": "100.00", "symbol": "USD", "decimals": 2, "window": "24h" } } ] } spent against limit is the headroom left, in USD. The window field says how to read it: 24h for the daily policy, week and month for the calendar ones.
The per-transfer cap carries no usage block, because it has nothing to accumulate. It is checked against the single payment in front of it.
GET /api/agent/policies returns the same hydrated shape for every wallet the key can reach, which is the cheaper call when your agent runs more than one wallet.
A preflight your agent runs once per task
Ask before you spend:
async function budgetFor(walletId, apiKey) { const res = await fetch( `https://openclawcash.com/api/agent/policy?walletId=${walletId}`, { headers: { "X-Agent-Key": apiKey } } ); if (!res.ok) throw new Error(`policy read failed: ${res.status}`); const { policies = [] } = await res.json(); const windows = policies .filter((p) => p.usage) .map((p) => ({ type: p.type, left: Number(p.usage.limit) - Number(p.usage.spent), window: p.usage.window, })); return { windows, left: windows.length ? Math.min(...windows.map((w) => w.left)) : Infinity, }; } Then treat left as a planning number, not a permission slip:
const { left } = await budgetFor(walletId, apiKey); if (plannedUsd > left) { return { ok: false, reason: `over budget by ${(plannedUsd - left).toFixed(2)} USD` }; } The agent reports the shortfall to whoever asked for the work, and stops. That is a far better outcome than six retries and a stalled task.
What the windowed figures actually count
The daily figure is the USD value of transfers, swaps and venue orders in the last 24 hours. A swap counts. A venue order counts. It is USD value, not token amount, so the same limit holds whether the agent pays in USDC or in ETH.
Two behaviours worth knowing before you plan around it:
- If the price lookup behind that conversion fails, the payment is blocked, not allowed. The engine's answer is
Unable to validate spending limit - price lookup failed.Failing closed is the right default, but it means a flaky price feed looks like a limit problem from your agent's side. - The limit is evaluated in USD at signing time, so a token that moves between your check and your payment can land differently than you planned.
Where this design breaks
The usage block is computed when you read it. It is a preflight, not a reservation. Two agents spending from the same wallet at the same moment both see the same headroom, and the second one still gets the 401.
If the limit has to hold, keep one spending agent per wallet, or serialize the payments through your own queue. The policy engine enforces the ceiling either way; what it cannot do is stop your two agents from planning against the same dollars.
Two calls that replace the retry loop
- Read
GET /api/agent/policy?walletId=...once per task, take the smallestlimit - spent, and plan against it. - When a payment comes back
401withcode: "policy_violation", stop. Report the reason fromwhatHappenedand let a human raise the limit or fund a wallet that has room.
Everything above is on openclawcash.com/docs: the transfer endpoint, the policy endpoints and their response shapes.

