Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.tallyforagents.com/llms.txt

Use this file to discover all available pages before exploring further.

The payments endpoints are where money moves. See Payments (concept) for the enforcement model and SDK payments for the TypeScript wrapper.

The payment object

{
  "id": "tx_01HXYZ...",
  "status": "pending",
  "tx_hash": "0xabc...",
  "amount_usdc": "4.50",
  "to": "0xC0fee...",
  "from": "0x7a3f...",
  "memo": "arxiv API access",
  "idempotency_key": "invoice-2026-04-117",
  "created_at": "2026-05-18T12:00:00Z"
}
FieldTypeDescription
idstringTally’s internal payment id, used as the path param on GET /v1/payments/{id}.
status"pending" | "confirmed" | "failed"pending until the chain confirms.
tx_hashstring | nullOn-chain transaction hash. Set as soon as Privy accepts the signed RPC; null only on rejected pre-checks (which don’t produce a payment record).
amount_usdcstringDecimal USDC amount. Always a string to preserve precision.
tostringRecipient address.
fromstringSender wallet address.
memostring | nullOptional caller-provided memo.
idempotency_keystring | nullThe key the payment was created with, if any.
created_atISO 8601 stringRecord creation time.

POST /v1/payments

Sign and broadcast a USDC transfer.

Request

curl https://api.tally.example.com/v1/payments \
  -X POST \
  -H "Authorization: Bearer $TALLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "research-bot",
    "wallet": "0x7a3fA1B9c4D2e6F8a0B1c2D3e4F5a6B7c8D9e0b21c",
    "to": "0xC0fee04...",
    "amount_usdc": "4.50",
    "memo": "arxiv API access",
    "idempotency_key": "invoice-2026-04-117"
  }'
Body fieldTypeRequiredDescription
agent_idstring (1–64)YesThe agent’s public id. Must have an active grant on wallet.
walletstring (EVM address)YesSender wallet, owned by this account, matching the key’s mode.
tostring (EVM address)YesRecipient.
amount_usdcstringYesPositive decimal, up to 6 decimal places (USDC’s native precision).
memostring (≤200)NoStored alongside the transaction.
idempotency_keystring (1–64)NoSee Idempotency.

Response

201 Created:
{
  "payment": {
    "id": "tx_01HXYZ...",
    "status": "pending",
    "tx_hash": "0xabc...",
    "amount_usdc": "4.50",
    "to": "0xC0fee...",
    "from": "0x7a3f...",
    "memo": "arxiv API access",
    "idempotency_key": "invoice-2026-04-117",
    "created_at": "2026-05-18T12:00:00Z"
  }
}
The endpoint returns as soon as Privy accepts the signed RPC. The on-chain receipt arrives a few seconds later — poll GET /v1/payments/{id} or subscribe to payment.confirmed webhooks.

Enforcement layers

In order, before the response is built:
  1. Tally pre-check. The agent must exist in this (account, mode); the wallet must belong to the account; an active grant must exist; the policy (per-tx max, daily cap, recipient/contract allowlist, expiry) must permit the payment.
  2. Privy enclave. Independently re-validates the per-tx max and allowlists when signing. A compromised Tally server can’t sneak a payment past this check.
Pre-check failures (layer 1) return 4xx with a structured error code — see below. Enclave failures (layer 2) result in status: "failed" on the resulting payment.

Errors

StatustypecodeWhen
400validation_failedamount_invalidamount_usdc is malformed.
400validation_failedaddress_invalidwallet or to isn’t a valid EVM address.
400validation_failedBody shape failed Zod. details has the issues.
401unauthenticatedAPI key invalid.
403forbiddenamount_too_largeExceeds the grant’s per-tx max.
403forbiddendaily_cap_exceededWould push 24h spend over the cap.
403forbiddenrecipient_not_allowedto is not in the grant’s recipient allowlist.
403forbiddencontract_not_allowedDestination contract isn’t in the allowlist (USDC is default).
403forbiddenpermission_expiredThe grant’s expires_at has passed.
404not_foundagent_not_foundNo agent with that id in this (account, mode).
404not_foundwallet_not_foundThe wallet isn’t owned by this account.
404not_foundpermission_not_foundThe agent has no active grant on the wallet.
429rate_limitedrate_limit_exceededPayments bucket exceeded (30/min).
500internalprivy_call_failedPrivy’s API failed during signing. Retry with the same idempotency key.
See Errors for the response envelope.

Rate limit

POST /v1/payments uses the stricter payments bucket: 30 requests / 60 seconds per API key. See Rate limits.

GET /v1/payments/{id}

Retrieve a single payment by its Tally id. If the payment is still pending on Tally’s side and has a tx_hash, this call lazily refreshes from the chain — so polling GET /v1/payments/{id} is the canonical way to wait for a confirmation.

Request

curl https://api.tally.example.com/v1/payments/tx_01HXYZ... \
  -H "Authorization: Bearer $TALLY_API_KEY"
Path paramDescription
idTally’s internal payment id (the id field from a previous response).

Response

200 OK. Same shape as the POST /v1/payments response. status reflects the latest known state — if the chain has confirmed since the last call, status will have flipped to confirmed or failed.

Errors

StatustypeWhen
401unauthenticatedAPI key invalid.
404not_foundNo payment with that id, or it belongs to a different (account, mode).
429rate_limitedDefault bucket exceeded.

Patterns

Poll until confirmed

async function waitForConfirmation(id: string, timeoutMs = 30_000) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    const res = await fetch(`${baseUrl}/v1/payments/${id}`, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    const { payment } = await res.json();
    if (payment.status !== "pending") return payment;
    await new Promise((r) => setTimeout(r, 2_000));
  }
  throw new Error(`payment ${id} did not confirm within ${timeoutMs}ms`);
}
Don’t poll faster than every 2 seconds — you’ll burn rate-limit budget without seeing a meaningfully fresher status.

Production: subscribe to webhooks instead

For production workloads, replace the polling loop with a webhook subscription to payment.confirmed and payment.failed. The event carries the same payment shape; verification helper lives in the SDK webhooks page.

Not yet exposed

  • GET /v1/payments — listing with status/agent/direction filters.
  • POST /v1/payments/{id}/refund — first-class refund handling.
For listing today, the dashboard’s Transactions tab is the source of truth.