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"
}
| Field | Type | Description |
|---|
id | string | Tally’s internal payment id, used as the path param on GET /v1/payments/{id}. |
status | "pending" | "confirmed" | "failed" | pending until the chain confirms. |
tx_hash | string | null | On-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_usdc | string | Decimal USDC amount. Always a string to preserve precision. |
to | string | Recipient address. |
from | string | Sender wallet address. |
memo | string | null | Optional caller-provided memo. |
idempotency_key | string | null | The key the payment was created with, if any. |
created_at | ISO 8601 string | Record 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 field | Type | Required | Description |
|---|
agent_id | string (1–64) | Yes | The agent’s public id. Must have an active grant on wallet. |
wallet | string (EVM address) | Yes | Sender wallet, owned by this account, matching the key’s mode. |
to | string (EVM address) | Yes | Recipient. |
amount_usdc | string | Yes | Positive decimal, up to 6 decimal places (USDC’s native precision). |
memo | string (≤200) | No | Stored alongside the transaction. |
idempotency_key | string (1–64) | No | See 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:
- 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.
- 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
| Status | type | code | When |
|---|
| 400 | validation_failed | amount_invalid | amount_usdc is malformed. |
| 400 | validation_failed | address_invalid | wallet or to isn’t a valid EVM address. |
| 400 | validation_failed | — | Body shape failed Zod. details has the issues. |
| 401 | unauthenticated | — | API key invalid. |
| 403 | forbidden | amount_too_large | Exceeds the grant’s per-tx max. |
| 403 | forbidden | daily_cap_exceeded | Would push 24h spend over the cap. |
| 403 | forbidden | recipient_not_allowed | to is not in the grant’s recipient allowlist. |
| 403 | forbidden | contract_not_allowed | Destination contract isn’t in the allowlist (USDC is default). |
| 403 | forbidden | permission_expired | The grant’s expires_at has passed. |
| 404 | not_found | agent_not_found | No agent with that id in this (account, mode). |
| 404 | not_found | wallet_not_found | The wallet isn’t owned by this account. |
| 404 | not_found | permission_not_found | The agent has no active grant on the wallet. |
| 429 | rate_limited | rate_limit_exceeded | Payments bucket exceeded (30/min). |
| 500 | internal | privy_call_failed | Privy’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 param | Description |
|---|
id | Tally’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
| Status | type | When |
|---|
| 401 | unauthenticated | API key invalid. |
| 404 | not_found | No payment with that id, or it belongs to a different (account, mode). |
| 429 | rate_limited | Default 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.