Spend caps
Every Obscura agent has a monthly spend cap — a hard ceiling on how much the agent can spend in a calendar month, enforced at signature time. Caps are the single most important safety feature on the agent side: a leaked API key can never drain more than the remaining cap.
How the cap is enforced
When /api/x402/sign receives a sign request, it runs this SQL before
producing a signature:
UPDATE budgets
SET spent_usdg = spent_usdg + $amount
WHERE agent_id = $agent_id
AND spent_usdg + $amount <= cap_usdg
RETURNING id;Postgres returns zero rows if the would-be post-update value exceeds
the cap. We interpret that as "over cap," refuse to sign, and return a
402 over_cap response.
This is atomic at the DB level — no read-then-write race. Two
concurrent sign requests that would collectively exceed the cap are
handled correctly: one succeeds, one returns over_cap.
INR vs USDC
Caps are authored in rupees in the dashboard because that's what users think in. Internally we store two values:
cap_inr— the rupee amount the user set (source of truth).cap_usdg— a snapshot of the INR→USDC exchange rate at the time the cap was created, used for the atomic check above.
We DON'T re-compute the rate on every spend. That's deliberate: a merchant asking for $0.01 shouldn't see a different "allowed" outcome depending on the hour the rupee rate was pulled. If you change the cap via the dashboard, a fresh snapshot is taken.
Reset cadence
Caps reset at the start of every calendar month, UTC. There's no prorating or rolling window — the cap is simply "this month's budget."
The reset is lazy, not scheduled: every sign request first runs an
idempotent UPDATE … SET spent_usdg=0, period_start=now() WHERE period_start <= now() - interval '1 month'. The first spend after the
month boundary triggers the reset for that agent; subsequent spends
the same period are no-ops on the reset clause. No cron job to fail.
Compensation on signing failure
The cap is incremented BEFORE the Umbra mixer call (it has to be — a
read-then-mixer-then-write would have a TOCTOU window). If the mixer
call fails AFTER the increment, /api/x402/sign runs safeRevertCap
to subtract the amount back. Two cases:
- Mixer fails cleanly (RPC error, prove failure). Revert succeeds → cap is back to its pre-spend value, transaction row marked failed.
- Mixer's MPC callback didn't finalize within timeout. The on-chain
debit may have already landed, so we do NOT revert the cap (that
would let the agent double-spend). The transaction row stays
pending, and/api/cron/reconcile(every 5 minutes) inspects the queue tx on-chain to decide whether to confirm or fail-and-revert.
Practical patterns
Fleet-of-agents
Give each agent a small per-month cap (say ₹500). Spin up many agents in parallel — your total exposure is still bounded per-agent, even if one key leaks.
Burst workloads
If an agent will do a burst of 1000 paid calls in a few minutes, set
the cap to cover the burst plus a safety margin. over_cap responses
are non-retryable; once you hit the ceiling you can't get through
without raising it.
Audit trail
Every spend — successful or failed — is recorded in transactions.
Failed spends stay at status='failed' with the reason in memo,
which is useful when debugging "why is my agent over cap" issues.
What caps don't protect against
- Application-level errors. If your agent is buggy and makes 10000 redundant API calls within the cap, the cap limits the damage but doesn't prevent it. Build in idempotency on the agent side.
- Merchant pricing changes. The merchant's
amountis what gets charged. If a merchant raises their price, your agent spends more per-call — the cap is the bound. - Insufficient encrypted balance. The cap is a policy ceiling; the
encrypted balance is the actual fund pool. Spending below the cap
but above the balance returns
insufficient_funds(402), notover_cap.