AGENTS
Building agents that last
Tab gives an autonomous agent the same payment surface a SaaS business has (payable orders, on-chain settlement, webhooks, analytics) without the human bottleneck. This page is the opinionated playbook for shipping an agent that earns reliably and stays running for years, not weeks.
Each section is a copy-pasteable pattern. None of them require custom contracts or off-Tab infrastructure. If you finish this page you have what a production agent business needs.
The agent loop
Every successful agent on Tab runs the same five-step loop. Keep each step small and the agent stays predictable.
import { TabClient } from "@tab/agent-sdk";
const tab = new TabClient({
apiKey: process.env.TAB_API_KEY!,
handle: "yourbot",
});
// 1. User asks the agent for something.
async function handleRequest(userId: string, prompt: string) {
// 2. Agent quotes a price + creates a payable order.
const order = await tab.createCheckout({
amount: "0.50",
chain: "base",
handle: "yourbot",
metadata: { userId, prompt },
});
// 3. Agent gives the user the pay link.
await postToUser(userId, `Pay $0.50 here: ${order.checkoutUrl}`);
}
// 4. Webhook fires when the user pays.
export async function onOrderCompleted(event) {
const { userId, prompt } = event.payload.order.metadata;
// 5. Agent delivers the work.
const result = await doTheWork(prompt);
await postToUser(userId, result);
}That's the whole pattern. Everything below is variations on it: how to make step 4 idempotent, how to handle failures in step 5, how to rate-limit step 2.
Idempotent webhooks
Tab retries failed deliveries on a 1m → 5m → 15m → 1h → 6h → 24h ladder. That means your handler should expect to see the same event.idmore than once. Track which event ids you've processed and skip duplicates.
const processed = new Set<string>(); // production: Redis or DB
app.post("/webhook", async (req, res) => {
if (!tab.verifyWebhook(req.headers["tab-signature"], req.body)) {
return res.status(401).send("bad sig");
}
const eventId = req.body.id as string;
if (processed.has(eventId)) {
return res.json({ ok: true, deduped: true });
}
// ... do the work ...
processed.add(eventId);
res.json({ ok: true });
});Acknowledge the webhook with a 2xx as fast as you can, within five seconds is the standard. Heavy work goes on a queue; the webhook handler just enqueues + ACKs.
Rate-limit before Tab does
Tab caps per-IP and per-handle to keep the rails healthy. Your agent should cap per-user too, because the IP rate-limit is too coarse for one user spamming through a Telegram client.
const pendingPerUser = new Map<string, number>();
const MAX_PENDING = 3;
async function handleRequest(userId: string, prompt: string) {
const open = pendingPerUser.get(userId) ?? 0;
if (open >= MAX_PENDING) {
await postToUser(userId, "You have 3 pending orders already. Pay or wait.");
return;
}
const order = await tab.createCheckout({ /* ... */ });
pendingPerUser.set(userId, open + 1);
}Decrement on settlement, expiry, or cancellation. The new order-expiry cron (every 15 minutes) will close abandoned orders for you, so the counter will drift back down on its own.
Graceful failure + refunds
Sometimes the upstream API errors, the model returns garbage, or the work simply can't be done. You've already taken the user's money. The right move is an automatic refund.
try {
const result = await doTheWork(prompt);
await postToUser(userId, result);
} catch (e) {
await tab.refundOrder(order.id, {
reason: "Generation failed. You weren't charged.",
});
await postToUser(userId, "Something broke on our side. Refunded.");
}Refunds in Tab settle in seconds. They're on-chain, the user sees the money come back to their wallet, no support ticket. Treat refunds as a feature, not a defect; users who get a clean refund the one time things break come back twice as often.
Let the user pick the chain
Your default chain might be Base, but the user might hold their USDC on Solana, or USDT on BSC. Accept a chain hint and route accordingly.
// e.g. /art a serene mountain --chain solana
const chain = parseChain(prompt) ?? "base";
const order = await tab.createCheckout({
amount: "0.50",
chain,
handle: "yourbot",
});Tab's checkout page also auto-suggests the chain the buyer is connected to, so even without a flag the user can usually pay from whichever wallet is open.
The treasury pattern
As your agent grows, keep operational balance separate from treasury balance, both on Tab, both yours, but with different roles.
@yourbot: the agent's public handle. Holds a working balance (say, $50). Pays for its own API calls via a TabBot allowance. Posts pay links here.@yourbot-treasury: your accumulation handle. You sweep here on a schedule (daily, weekly). This is what you check at the end of the month.
Both handles live on Tab. Both have dashboards, receipts, analytics, push notifications. The sweep is one API call:
// Sweep operational → treasury once a day.
// Run as a cron from the same machine the agent runs on.
async function sweep() {
const balance = await tab.getBalance({ handle: "yourbot", chain: "base" });
if (Number(balance.amount) <= 50) return; // keep $50 operational
const surplus = (Number(balance.amount) - 50).toFixed(2);
await tab.pay({
fromHandle: "yourbot",
toHandle: "yourbot-treasury",
amount: surplus,
chain: "base",
});
}Why this matters: the operational handle's API key only ever has access to a small float. If the agent is compromised, the blast radius is whatever was in the operational wallet at that moment, not your entire year's earnings. The treasury handle's PIN never touches the agent's server.
MCP integration
Drop your agent into Claude Desktop, Cursor, or any MCP-compatible client by exposing it as an MCP server. The Tab SDK ships a base server you can extend.
// agent-mcp-server.ts
import { TabMcpServer } from "@tab/agent-sdk/mcp";
const server = new TabMcpServer({
apiKey: process.env.TAB_API_KEY!,
handle: "yourbot",
tools: [
{
name: "generate_summary",
price: "0.25",
chain: "base",
schema: { topic: "string" },
handler: async ({ topic }) => {
return { summary: await summarize(topic) };
},
},
],
});
server.listen();Claude or any MCP client now sees generate_summary as a paid tool. The runtime quotes the price up front, the user confirms once, payment + execution happen in one turn. You don't need to glue payment UI into the client, the MCP server template handles it.
Production checklist
Before you turn the agent loose on a public surface (Discord, Telegram, Farcaster, an embedded widget):
- Webhook secret in env, not source. Generated per-endpoint by Tab; copy once at creation, never logged.
- Idempotency by event.id. Pick a store: Redis, a Postgres table with the event id as primary key, even a local SQLite file is fine for low-volume agents.
- Per-user pending-order cap. Three or five is a sane default. Decrement on settlement and on the auto-expire cron tick.
- Refund-on-failure wrapped around the work step. If anything in the "do the work" path throws, refund and tell the user.
- Operator passkey on the treasury handle. The treasury handle is the one that holds real money, give it the strongest unlock (Passkey: Face ID / Touch ID / Windows Hello) instead of a PIN.
- Push notifications on the operator account. Turn them on under
/dashboard/settings. You get a ping on your phone every time the agent earns, plus alerts when the operational balance drops below your floor. - Monitor the dashboard's Events page. Every webhook delivery, success or failure, is logged at
/dashboard/events. If your endpoint is down for an hour, you see exactly which events are pending retry.
Why agents stay on Tab
Tab isn't just where the payment lands, it's where the operator lives between checks. The dashboard pulls receipts across every chain into one ledger, the analytics page shows revenue trends, the API exposes the same data you'd query from a Stripe export. Funds in your handle earn nothing custodial, they're live on-chain, in your wallet, ready to sweep, pay other agents, or fund a TabBot allowance for another of your bots.
The agent economy isn't a one-shot integration. The agents that win will spend years quietly running, accumulating balances, paying for their own infrastructure, occasionally getting a push notification on their operator's phone. Tab is built for that shape.