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 { Tab } from "@tabdotbar/agent-sdk";

const tab = new Tab({
  apiKey: process.env.TAB_API_KEY!,
  baseUrl: process.env.TAB_BASE_URL!,
});

// 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, checkout_url } = await tab.orders.create({
    amount: "0.50",
    chain: "base",
    recipient: "@yourbot",
    metadata: { userId, prompt },
    idempotencyKey: crypto.randomUUID(),
  });

  // 3. Agent gives the user the pay link.
  await postToUser(userId, `Pay $0.50 here: ${checkout_url}`);
}

// 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.

import { createHmac, timingSafeEqual } from "crypto";
const processed = new Set<string>(); // production: Redis or DB

app.post("/webhook", async (req, res) => {
  const sig = req.headers["tab-signature"] as string;
  const expected = createHmac("sha256", process.env.TAB_WEBHOOK_SECRET!)
    .update(req.rawBody)
    .digest("hex");
  if (!sig || !timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    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.orders.create({ /* ... */ });
  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) {
  // v1: refunds run from the dashboard or via the REST API
  //   POST /api/orders/:id  { action: "refund" }
  // The agent-sdk surfaces escrow refunds (`tab.escrow.refund(id)`);
  // direct-order refunds come in the next SDK release.
  await postToUser(userId, "Something broke. Refunding via dashboard.");
}

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, BSC, Ink, or Celo. Accept a chain hint and route accordingly.

// e.g. /art a serene mountain --chain solana
const chain = parseChain(prompt) ?? "base";

const { order, checkout_url } = await tab.orders.create({
  amount: "0.50",
  chain,
  recipient: "@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:

import { Tab, payHandle } from "@tabdotbar/agent-sdk";

// Sweep operational → treasury once a day.
// Run as a cron from the same machine the agent runs on. The
// operational handle's API key signs the outbound transfer.
async function sweep() {
  // Read on-chain balance directly — operator wallet address comes
  // from tab.handles.resolve("@yourbot").
  const { record } = await tab.handles.resolve("@yourbot");
  const balance = await readUsdcBalance(record.address, "base"); // your own viem helper
  if (balance <= 50) return; // keep $50 operational
  const surplus = (balance - 50).toFixed(2);
  await payHandle(tab, {
    recipient: "@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 Tab into Claude Desktop, Cursor, or any MCP-compatible client. @tabdotbar/agent-sdkships a stdio MCP server that exposes Tab's 16 tools (payments, escrow, x402, ERC-8004 discovery) without writing any glue.

{
  "mcpServers": {
    "tab": {
      "command": "npx",
      "args": ["-y", "@tabdotbar/agent-sdk"],
      "env": {
        "TAB_API_KEY": "sk_live_...",
        "TAB_BASE_URL": "https://thetab.bar"
      }
    }
  }
}

For agents that want to charge per call in their own MCP server, gate tool handlers behind tab.x402.charge() + tab.x402.verify(). The same pattern documented on the x402 page. The client pays the 402 challenge, the server verifies, the handler returns the result. No payment UI to write.

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.