Why polling is the wrong model for agent spending

Most developers start with polling: call GET /api/wallets/:id/balance every 60 seconds, check if anything changed, react if it did. This works for simple monitoring dashboards. It fails for autonomous agents.

The problem is latency. If your agent makes 50 purchases between your 60-second polling intervals, you have no idea when each dollar left. Budget exhaustion is discovered an hour later, not when it happened. A runaway loop spending $0.01 per call doesn't trigger any alert until you've paid $300 for a task that should have cost $3.

Webhooks solve this. When a purchase succeeds, Trove fires an HTTP POST to your endpoint with the transaction details — vendor, amount, timestamp, wallet ID. Your handler reacts immediately. No polling, no gaps.

The webhook flow for AI agent spending

AI Agent Trove API Your Webhook Slack / DB / Dashboard

The event types your handler needs

Trove fires webhooks for every significant wallet event. The three you need first:

  • purchase.succeeded — A purchase was recorded. This is your bread-and-butter event. Every API call your agent makes triggers this. Log it, dashboard it, build your cost attribution model on it.
    Fields: wallet_id, transaction_id, amount_usd, vendor, description, timestamp
  • budget.exhausted — The wallet hit its limit. The agent tried to purchase and got a 402. This is your most critical alert. Slack it, email it, pause your agent's queue. This means your agent is now operating without spending capability.
    Fields: wallet_id, budget_limit_usd, last_transaction_id
  • purchase.failed — A purchase attempt returned an error (not over-budget — that would be 402). If your agent is hitting network errors or vendor failures, this is how you see it.
    Fields: wallet_id, error_code, error_message, vendor

Building the webhook handler

The handler has three jobs: verify the signature, parse the event, respond fast. Everything else happens asynchronously.

Step 1 of 3

Verify the signature

Every webhook from Trove includes an HMAC-SHA256 signature in X-Trove-Signature. If you skip this step, anyone can POST fake events to your handler and pollute your spending data.

Express handler
const crypto = require('crypto'); function verifyWebhook(rawBody, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(rawBody, 'utf8') .digest('hex'); // timing-safe comparison prevents timing attacks return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } app.post('/webhooks/trove', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-trove-signature']; const secret = process.env.TROVE_WEBHOOK_SECRET; if (!verifyWebhook(req.body, signature, secret)) { return res.status(401).json({ ok: false, error: 'Invalid signature' }); } // Acknowledge immediately — handle async res.status(200).json({ ok: true }); processWebhookAsync(req.body); });
Step 2 of 3

Handle events by type

Parse the event and route it to the right handler. Separate the acknowledgment from the processing so slow downstream calls don't cause Trove to retry.

Event routing
async function processWebhookAsync(rawBody) { const event; try { event = JSON.parse(rawBody.toString()); } catch { console.error('Invalid webhook JSON'); return; } switch (event.type) { case 'purchase.succeeded': logPurchase(event.data); updateDashboard(event.data); break; case 'budget.exhausted': sendAlert('Wallet ' + event.data.wallet_id + ' exhausted its budget'); pauseAgent(event.data.wallet_id); break; case 'purchase.failed': logError(event.data); notifyOnRepeatedFailure(event.data); break; default: console.warn('Unknown webhook type: ' + event.type); } }
Step 3 of 3

Handle the budget exhaustion alert

This is the event that makes webhooks worth the setup. When your agent hits its budget ceiling, you want to know immediately — and your infrastructure should respond automatically.

Budget alert handler
async function sendAlert(message) { // Post to Slack webhook await fetch(process.env.SLACK_WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: ':warning: ' + message, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: message } }, { type: 'actions', elements: [ { type: 'button', text: { type: 'plain_text', text: 'Increase Budget', action_id: 'increase_budget' } ]} ] }) }); } async function pauseAgent(walletId) { // Push to a job queue — don't block the webhook response await redisClient.lpush('agent:pause:queue', JSON.stringify({ walletId, reason: 'budget_exhausted', timestamp: Date.now() })); }

What to log and why

Every purchase.succeeded event is a data point for your cost attribution model. If you store them in a simple table, you can answer questions that matter:

Which vendors cost the most? SELECT vendor, SUM(amount_usd) FROM spending_events GROUP BY vendor ORDER BY SUM DESC LIMIT 10;

Which agent is burning budget fastest? SELECT wallet_id, SUM(amount_usd) FROM spending_events WHERE created_at > NOW() - INTERVAL '1 hour' GROUP BY wallet_id;

Are there spikes that indicate a runaway loop? SELECT DATE_TRUNC('minute', created_at) AS minute, SUM(amount_usd) FROM spending_events WHERE wallet_id = $1 AND created_at > NOW() - INTERVAL '2 hours' GROUP BY minute ORDER BY minute;

The storage model is simple. One Postgres table: spending_events(event_id, wallet_id, amount_usd, vendor, description, created_at). Write every event to it, index on wallet_id and created_at. That's enough to build a real-time dashboard, run cost attribution queries, and catch runaway loops before the bill gets out of hand.

Making the Slack alert actionable

A "wallet exhausted budget" Slack message is useful. An interactive Slack message that lets you fix the problem without leaving Slack is better.

When your webhook fires a budget.exhausted event, post a Slack message with a button. The button opens a modal with the current wallet balance, a budget increase input, and a confirm button. When the user confirms, your Slack endpoint calls Trove's wallet update API and posts a success message back to the channel.

The feedback loop: agent spends to limit → webhook fires → Slack alert with interactive fix → you increase budget → agent resumes. All in under 60 seconds. No Terraform, no Ops team, no email chains.

Delivery and retry behavior

Trove expects your handler to return HTTP 200 within 5 seconds. If it returns a non-200 status or times out, Trove retries with exponential backoff: 15 seconds, then 60 seconds, then 5 minutes, then 1 hour. After 4 failed attempts, the event is dropped.

Because you acknowledge immediately and process asynchronously, your handler should always return 200 fast. The retry logic protects against temporary network issues, not your handler's processing time.

If you need guaranteed delivery for critical events (like budget.exhausted), add your own dead-letter queue. On every failed downstream call (Slack, database write), push the event to a Redis list. A separate worker retries the queue every 60 seconds. This ensures you never drop a budget exhaustion alert even if your Slack webhook is down.

Get started with the /try playground

The playground lets you trigger a webhook locally by making purchases from a sandbox wallet. Watch your endpoint receive events in real time, test your signature verification, and see how fast your handler responds. No production infrastructure needed to validate the integration.

Test webhook integration in 2 minutes

The playground auto-provisions a sandbox wallet. Point it at your local endpoint (ngrok works) and watch every event fire as you make purchases.

Open the Playground

For the full webhook event reference and signature verification examples in Python, Ruby, and Go, see the Trove API documentation.