TheDocumentation 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.payments resource is where money moves. See Payments (concept) for the two-layer enforcement model and statuses.
Types
Payment
PaymentCreateInput
Methods
payments.create(input)
Signs and broadcasts a USDC transfer. Returns once Privy accepts the signed RPC — does not wait for chain confirmation.
amount_usdc accepts up to 6 decimal places (USDC’s native precision); higher precision is rejected.
Returns: Promise<Payment> with status: "pending" and a populated tx_hash.
Enforcement layers (in order):
- Tally pre-check. Validates the agent has an active grant on
wallet, that the policy allows this payment (per-tx max, recipient/contract allowlist, expiry, daily cap), and that the API key is in the right mode. Failures throwValidationErrorwith structurederror.code. - Privy enclave. Independently re-checks the per-tx max and allowlists when signing. Failures bubble up as
status: "failed"on the resulting Payment — there’s no record-less rejection at this layer.
| Error | When |
|---|---|
ValidationError | Input shape failed Zod validation. err.code may be amount_invalid or address_invalid. |
NotFoundError | The agent, wallet, or grant doesn’t exist in this (account, mode). err.code: agent_not_found, wallet_not_found, or permission_not_found. |
AuthenticationError | The API key is invalid, or a policy check rejected the payment (per-tx max, daily cap, recipient/contract allowlist, expiry). The SDK class is named for auth because the server returns 403/forbidden; inspect err.code to distinguish policy violations from real auth failures. |
RateLimitError | The account is over its payments-per-minute ceiling (30/min). Honor Retry-After. |
TallyError | err.code === "privy_call_failed" if Privy’s API call failed. Retry with the same idempotency key. |
payments.get(id)
Fetches the current state of a payment by id. If still pending on Tally’s side, this call lazily refreshes from the chain — so polling get is the canonical way to wait for confirmed or failed.
Promise<Payment> reflecting the latest known state. If the chain has confirmed since the last call, status will already be flipped.
Throws: NotFoundError, AuthenticationError.
Idempotency
idempotency_key makes retries safe across network failures:
create() calls with the same idempotency_key within the same (account, mode) return the original Payment without resubmitting on-chain. The payload is not revalidated — if your second call passes a different to or amount_usdc, the server still returns the original payment as written the first time. Make sure your key derivation is tied to the payload (e.g. include the invoice id in the key), not just the operation.
Use deterministic keys derived from your own data — invoice IDs, request IDs, anything stable. Avoid timestamps or fresh UUIDs; those defeat the purpose by making every retry a fresh request.
Waiting for confirmation
create() returns pending. The chain confirms a few seconds later. Two ways to wait:
Poll
payments.get() lazily refreshes from the chain when called on a pending payment, so polling it is the right shape. Don’t poll faster than every 2 seconds; you’ll burn rate-limit budget without seeing a meaningfully fresher status.
Subscribe to webhooks
For production, prefer webhooks. Subscribe topayment.confirmed and payment.failed and use the SDK’s verifier — see Webhooks.
Error codes
payments.create() can fail with one of these structured err.code values. The first column tells you which SDK exception class wraps the response — branch on instanceof first, then on err.code.
err.code | SDK class | HTTP | Meaning |
|---|---|---|---|
agent_not_found | NotFoundError | 404 | No agent with that id in this (account, mode). |
wallet_not_found | NotFoundError | 404 | The wallet address isn’t owned by this account or wrong mode. |
permission_not_found | NotFoundError | 404 | The agent has no active grant on this wallet. |
amount_invalid | ValidationError | 400 | Malformed amount_usdc (non-decimal, >6 decimals, etc). |
address_invalid | ValidationError | 400 | to or wallet is not a valid EVM address. |
amount_too_large | AuthenticationError | 403 | Exceeds the grant’s per-transaction max. |
daily_cap_exceeded | AuthenticationError | 403 | Would push the rolling 24h spend over the grant’s daily cap. |
recipient_not_allowed | AuthenticationError | 403 | to is not in the grant’s recipient allowlist. |
contract_not_allowed | AuthenticationError | 403 | The destination contract isn’t on the policy’s contract allowlist (USDC is the default). |
permission_expired | AuthenticationError | 403 | The grant’s expires_at has passed. |
privy_call_failed | TallyError | 500 | Privy’s API failed during signing. Retry with the same idempotency key. |
AuthenticationError because that’s what 403 maps to in the class hierarchy; the semantic distinction lives in err.code.
Not yet in the SDK
payments.list({ filters? })— cursor-paginated list with status/agent/direction filters. Currently dashboard-only.payments.refund(id)— first-class refund flow. Today, send a fresh payment in the reverse direction.