> ## 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.

# Payments

> How agents send and receive USDC, and how Tally enforces the rules.

A payment is a USDC transfer signed by an agent's authorization key, validated against the permission you approved for that agent, and broadcast to Base. Every payment passes through two independent enforcement layers before it reaches the chain — neither of which Tally alone can override.

## Mental model

A `tally.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:

1. **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.
2. **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.

The payment is also written to Tally's transaction log atomically with the broadcast, so per-agent attribution and audit trail are immediate.

## Sending a payment

```ts theme={null}
const payment = await tally.payments.create({
  agent_id: "research-bot",
  wallet: "0x7a3fA1B9c4D2e6F8a0B1c2D3e4F5a6B7c8D9e0b21c",
  to: "0xC0fee04...",
  amount_usdc: "4.50",
  memo: "arxiv API access",          // optional, indexed for search
  idempotency_key: "invoice-2026-04-117", // optional, see below
});

console.log(payment.status, payment.tx_hash);
// → "pending" 0xabc...
```

Inputs:

| 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](#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.  |

A payment rejected by Tally's pre-check (allowance exceeded, recipient not allowed, etc.) does **not** become a `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

Pass `idempotency_key` to make retries safe across network failures:

```ts theme={null}
await tally.payments.create({
  agent_id: "research-bot",
  wallet,
  to,
  amount_usdc: "4.50",
  idempotency_key: "invoice-2026-04-117",
});
```

Two calls with the same `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()`

```ts theme={null}
let p = await tally.payments.create({ ... });

while (p.status === "pending") {
  await new Promise((r) => setTimeout(r, 2_000));
  p = await tally.payments.get(p.id);
}

if (p.status === "failed") {
  // handle on-chain failure
}
```

`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 to `payment.confirmed` and `payment.failed` events; the webhook payload includes the same `Payment` shape you got back from `create()`.

See [Webhooks](/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 emits `inbound.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 as `status: "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.
