Mental model
Atally.payments.create() call is an instruction: “transfer this much USDC from this wallet to this address, attributed to this agent.” Everything from there is enforcement and accounting.
Two-layer enforcement is the load-bearing part:
- Tally pre-checks the request against the policy — per-tx max, recipient and contract allowlists, expiry, and the rolling daily cap. If anything fails, the payment is rejected before any Privy call.
- Privy’s secure enclave independently re-checks the per-tx max and the allowlists when it signs. A compromised Tally server can’t sneak a payment past the enclave, because the enclave doesn’t trust Tally — it trusts the policy the user signed.
Sending a payment
| Field | Required | Description |
|---|---|---|
agent_id | Yes | The agent’s stable id. Must have an active grant on wallet. |
wallet | Yes | Sender wallet address. Must be in the API key’s account + mode. |
to | Yes | Recipient EVM address. |
amount_usdc | Yes | Decimal string, e.g. "10" or "2.50". Up to 6 decimal places. |
memo | No | Up to 200 chars. Indexed; useful for invoice/order references. |
idempotency_key | No | Up to 64 chars. Scoped to (account, mode). |
create returns immediately once Privy accepts the signed RPC. It doesn’t wait for block confirmation — see waiting for confirmation below.
Statuses
| Status | Meaning |
|---|---|
pending | Signed by Privy and broadcast to the network. Awaiting block confirmation. |
confirmed | Mined and indexed by Tally. |
failed | The transaction reverted on-chain, or Privy rejected the signing request. |
failed payment — it raises a ValidationError from create() and no record is written. The transaction log only contains things that made it past pre-check.
Idempotency
Passidempotency_key to make retries safe across network failures:
idempotency_key within the same (account, mode) return the original Payment without resubmitting on-chain. Use deterministic keys derived from your own data — invoice numbers, request IDs, anything that won’t change on retry. Avoid timestamps or UUIDs you regenerate, since those let a retry accidentally double-spend.
Waiting for confirmation
create() returns a pending payment with the broadcast tx_hash. To wait for the chain to confirm, you have two options.
Poll with payments.get()
payments.get() lazily refreshes from the chain if Tally hasn’t yet caught up, so polling is the canonical way to wait. Base Sepolia confirmations are typically a few seconds; Base mainnet is similar.
Subscribe to webhooks
For production workloads, prefer webhooks — they remove the polling latency and free up your runtime. Subscribe topayment.confirmed and payment.failed events; the webhook payload includes the same Payment shape you got back from create().
See Webhooks for the subscription mechanics and signature verification.
Receiving USDC
Agent wallets receive USDC the same way any Ethereum-compatible wallet does — send to the address. Tally indexes inbound transfers automatically and emitsinbound.received webhook events, attributed to the receiving wallet’s account.
Wallet addresses are visible in the dashboard. Share the address with the sender out-of-band, or record it from your own UI when you create the wallet.
Inbound transfers don’t require a grant — anyone can send to a wallet. They’re attributed in your dashboard to the receiving wallet (and, by extension, the account that owns it).
Failures and retries
Tally retries network-level failures (RPC flakes, timeouts hitting Privy) internally — you won’t see those bubble up. Application-level failures (a revert because the recipient is invalid, the chain rejected the gas estimate, the policy edge case Privy enforced server-side) surface asstatus: "failed".
You won’t be billed for failed transactions. The transaction record stays in the log so audit and reconciliation aren’t affected.
What’s not yet implemented
- Refund flow — sending USDC back to the sender on-chain is just another payment, but a first-class
payments.refund()that handles the bookkeeping is on the roadmap. - Multi-asset support — USDC only today. The architecture supports other ERC-20s via the contract allowlist, but the SDK and dashboard are USDC-shaped.