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.
Every error response from the Tally API uses a consistent envelope. The HTTP status code tells you the rough category; the type and code in the body tell you the specifics.
Envelope
{
"error": {
"type": "validation_failed",
"message": "Amount exceeds per-tx max.",
"code": "amount_too_large",
"details": { "max_per_tx_usdc": "10", "requested": "15" }
}
}
Every error has type and message. code is set when there’s a more specific failure mode within a type (most payment policy violations carry one). details carries structured context when relevant — typically the failed Zod issue list for validation errors.
Error types
type | HTTP | Meaning |
|---|
unauthenticated | 401 | Missing, malformed, or invalid API key. |
forbidden | 403 | The request authenticated, but a policy check rejected it. Inspect code. |
not_found | 404 | The resource doesn’t exist in this (account, mode). |
validation_failed | 400 | The request body or query failed validation. details carries the Zod issues. |
conflict | 409 | The request collides with existing state. Not used by v1 endpoints today but reserved for future use. |
rate_limited | 429 | The account is over its per-second ceiling. See Rate limits. |
internal | 500 | A dependency failed (Privy, the chain). Retryable; see specific code for the cause. |
The SDK maps each type to a typed exception class — see SDK errors. REST callers branch on type and code directly.
Codes on POST /v1/payments
The payments endpoint is the only v1 route with structured code values today. Branch on error.type first, error.code second.
code | type | Meaning |
|---|
agent_not_found | not_found | No agent with that agent_id in this (account, mode). |
wallet_not_found | not_found | The wallet address isn’t owned by this account, or is in the wrong mode. |
permission_not_found | not_found | The agent has no active grant on that wallet. |
amount_invalid | validation_failed | amount_usdc is malformed (non-decimal, too many decimal places, etc.). |
address_invalid | validation_failed | wallet or to is not a valid EVM address. |
amount_too_large | forbidden | Exceeds the grant’s per-transaction max. |
daily_cap_exceeded | forbidden | Would push the rolling 24-hour spend over the grant’s daily cap. |
recipient_not_allowed | forbidden | to is not in the grant’s recipient allowlist. |
contract_not_allowed | forbidden | The destination contract is not on the grant’s contract allowlist (USDC is the default). |
permission_expired | forbidden | The grant’s expires_at has passed. |
privy_call_failed | internal | Privy’s API failed during signing. Retryable. |
The forbidden-typed codes are policy violations, not auth failures. Surface them in your UI as “this permission doesn’t permit that,” not “your credentials are bad.”
Recovery patterns
| Type | Should you retry? |
|---|
unauthenticated | No. Fix the key or rotate. |
forbidden | No, not the same way. Either fix the request shape (different to, smaller amount_usdc) or wait for a new grant. |
not_found | No. Either the id is wrong or the resource doesn’t exist yet. |
validation_failed | No. Fix the input. |
conflict | Maybe. Depends on the specific endpoint; today no v1 route emits this. |
rate_limited | Yes, after Retry-After. Use exponential backoff. |
internal | Yes, with an idempotency key. The cause is downstream. |
Always read error.message for human context before deciding — the message often points at the specific field or constraint that failed.
Sample handlers
Branching on type and code
async function pay(input: PaymentInput) {
const res = await fetch(`${baseUrl}/v1/payments`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(input),
});
if (res.ok) return res.json();
const { error } = await res.json();
switch (error.type) {
case "forbidden":
if (
error.code === "amount_too_large" ||
error.code === "daily_cap_exceeded"
) {
// Policy violation — prompt to top up the permission.
return notifyToTopUpPermission(error);
}
throw new PolicyError(error);
case "rate_limited":
await sleep(Number(res.headers.get("Retry-After") ?? 1) * 1000);
return pay(input); // retry with the same idempotency key
case "internal":
// Tally or Privy hiccup; retry idempotently
return pay(input);
default:
throw new ApiError(error);
}
}
Logging unknown errors
When you encounter a type your handler doesn’t branch on, log the entire envelope and surface a generic failure to the user. Don’t drop the body — the details field often has exactly the info you need to diagnose later.