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.

Tally supports idempotency keys on POST /v1/payments so retries can’t accidentally double-spend. The semantics are simple: same key + same (account, mode) returns the original payment, without re-submitting anything to the chain.

The header… is a body field

Tally takes the idempotency key in the request body, not in an Idempotency-Key header (unlike Stripe). This matches the SDK’s PaymentCreateInput shape:
POST /v1/payments
{
  "agent_id": "research-bot",
  "wallet": "0x7a3f...",
  "to": "0xC0fee...",
  "amount_usdc": "4.50",
  "idempotency_key": "invoice-2026-04-117"
}
The key is up to 64 characters and scoped to (account, mode). The same key can be used in test mode and live mode independently, and across different accounts globally.

Semantics

When payments.create (or POST /v1/payments) is called with an idempotency_key:
  1. Tally looks up an existing transaction in this (account, mode) with the same key.
  2. If one exists, it returns that record. The body is not revalidated — even if your retry passed a different to or amount_usdc, you get the original payment back as-written the first time.
  3. If no record exists, the payment proceeds normally. Tally also forwards the key to Privy as privy-idempotency-key so the underlying signing call is itself idempotent on Privy’s side.
In rare races where two requests with the same key arrive simultaneously, the database’s unique-constraint catches the loser, and the loser fetches and returns the winner’s record. Effectively serialized; you’ll never see two on-chain transactions from one idempotency key.

Choosing keys

Use deterministic strings derived from your own data:
  • An invoice id: invoice-2026-04-117
  • A workflow run id: wf-run-7c4f-step-3
  • A (user_id, operation) tuple: user-882-summary-paid-2026-05-18
Avoid:
  • Timestamps (tx-1715990400) — they change on retry, defeating the purpose.
  • Fresh UUIDs (tx-${randomUUID()}) — same problem.
  • The agent id alone (research-bot) — too coarse; you’ll deduplicate unrelated payments.
A good rule: the key should be a deterministic hash of the intent. If two requests represent the same intent, they should use the same key.

When you don’t pass a key

idempotency_key is optional. If you omit it:
  • Each retry is treated as a fresh request.
  • A network blip after the chain accepts the transaction but before the response reaches you can lead to a duplicate payment when you retry.
For agent payments — where the same “this user owes vendor X $5” intent may be retried by your worker — always pass a key.

When idempotency is the wrong answer

Idempotency keys are scoped to one (account, mode) and one key per intent. Patterns where they’re insufficient:
  • Splitting a payment into installments. Use distinct keys per installment (invoice-2026-04-117-installment-1, …). Don’t reuse the parent key.
  • A payment that legitimately replays a previous one. Example: a user resubscribes, generating a new payment that happens to be identical to last month’s. Use a fresh key — this isn’t a retry, it’s a new operation.
If you find yourself wanting to reuse a key for a different intent, that’s a sign the key derivation is too coarse.

In the SDK

The TypeScript SDK passes idempotency_key through to the body — no special handling required. See SDK payments for the type.