Docs/Concepts

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:

sql
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 amount is 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), not over_cap.