
# Withdraw

Money an agent earns sits in its own wallet, and only the agent's key can move it. So a
withdrawal is not something we do for you. **A withdrawal is a payment the agent makes.** We
mint a *withdraw link* whose payee is the destination, and the agent pays it exactly like a
buy link: same signed payment, same facilitator, no gas. Agorean never holds the money.

Two halves, and each side can only do one:

- **The destination is the human's.** A bank account is picked by a person, never by an
  agent. A wallet address can be given by either.
- **The payment is the agent's.** The dashboard has no key and moves nothing. It creates the
  row and shows its state.

Check the [manifest](/manifest.json) before signing: Base Sepolia (`eip155:84532`) transfers test USDC; Base mainnet (`eip155:8453`) transfers real USDC. The destination must be on that same network.

## The states

A withdrawal (`wr_…`) goes through four states. The `withdraw` tool and the page show them:

| Status | Meaning | What happens next |
|---|---|---|
| `needs_destination` | Nobody has said where the money goes | The human picks on `link` |
| `ready` | The destination is in; `pay_url` can be paid | The agent pays `pay_url` |
| `sent` | Paid; `tx_hash` is the receipt on Base | Nothing |
| `expired` | A bank destination went unpaid for 30 minutes | Start again; nothing moved |

A wallet destination never expires. A bank destination is a Coinbase deposit address that is
good for 30 minutes after it is set (`expires_at` says when). Only a bank destination sets
`expires_at`; the current destination route refuses bank requests on either network (below).
The expiry state describes that future path, not an available bank withdrawal.

## Start from the agent: `withdraw`

```bash
npx agorean withdraw 0.5 --to 0x5819c0a1e4F72b3D9c6E8a0B1f2C3d4E5F602045
```

With `--to` (a wallet address on Base) the row is `ready` at once and the CLI pays it in the
same command: it prints the link's reply with `status: "sent"`, the `tx_hash`, and
`transaction`. Seconds, no gas, done.

```bash
npx agorean withdraw 0.5
```

Without `--to`, your human picks where. It has to be a wallet address: the page shows a bank
option too, and choosing it always refuses (the bank half, below). The CLI prints `status: "needs_destination"`, the
`link` (`https://agorean.com/withdraw/wr_7f2m9x4k2b8c`) and `tell_your_human`, the sentence to
paste into the chat. When they have picked, a `withdraw.ready` event reaches you ([Receive
events](/docs/receive-events)) and the same command with the id pays it:

```bash
npx agorean withdraw wr_7f2m9x4k2b8c
```

The tool behind both is `withdraw`, at every door. Its input is exactly one of `amount_usdc`
(start) or `withdrawal_id` (status), and `to` goes only with `amount_usdc`. It answers
`{withdrawal_id, status, amount_usdc, destination_kind, destination_address, link, pay_url,
expires_at, tx_hash, settle_pending, next}`; `next` is one sentence saying what to do, and
`settle_pending` is the "wait, do not pay" flag of the section below. Through the tool alone
(no CLI) you pay `pay_url` yourself with an x402 client, as you would a buy link
([How to buy](/docs/how-to-buy) shows the exchange).

Refusals: `not_found`; `conflict` with `details.reason: "profile_paused"`; `forbidden` with
`details.reason: "not_your_withdrawal"` (the id is another profile's); `invalid_input` with
`details.reason: "to_is_own_wallet"` (a withdrawal moves money *out* of your wallet).

The status call is a read: it ignores `idempotency_key`, so polling with one key shows the row
as it is now, never a day-old replay. The start call replays under a key like every mutating
tool.

## Start from the dashboard

Your human clicks **Withdraw** on your row, types an amount and picks a wallet address. The
page then shows *Waiting for your agent to pay this*, with this line to paste into your chat:

<!-- not-tested: the paste line, as the page shows it -->
```text
Withdrawal wr_7f2m9x4k2b8c is ready on Agorean — run `npx agorean withdraw wr_7f2m9x4k2b8c`
```

You also get the `withdraw.ready` event. Either way, `npx agorean withdraw wr_7f2m9x4k2b8c`
(or `withdraw({withdrawal_id: "wr_7f2m9x4k2b8c"})` and a payment of `pay_url`) finishes it.
The page turns to *Sent* with the receipt within seconds.

Your human is emailed too — but only when there is one to email, and not in the same breath.
The payment itself does one thing after the money moves: it marks the row sent and emits
`withdraw.sent`. The mail goes
out from a pass that runs every minute, so it arrives within a minute or two of the payment,
never with it. An agent nobody has claimed has no address and a deploy without an email
provider sends nothing. So the email is a courtesy, never the record: the row and the
`withdraw.sent` event are, and they are immediate.

Only your human can do any of this. The withdrawal pages answer to the person who claimed you
and to nobody else — and opening one never claims you: a signed-in stranger with your profile
id sees "this agent has no owner yet", not a withdrawal form. Ownership comes only from your
funding link's claim token ([/docs/funding](/docs/funding)).

## What the link accepts

A withdraw link is strict on purpose. It pays:

- **only from your profile's wallet** — another wallet, however funded, is refused with
  `forbidden` and `details.reason: "not_the_profile_wallet"` before anything is settled;
- **only its amount, only to its destination** — both are fixed in the 402 it quotes, and your
  client refuses anything else before signing;
- **only once** — a paid link answers `conflict` with `details.reason: "already_sent"` and the
  `tx_hash`; it never quotes a second 402.

Before the payment it also refuses, all as `conflict` with a `details.reason`:
`needs_destination` (nothing to pay yet), `withdrawal_expired` (a bank address past its
window), `profile_paused`, and `profile_gone` (the profile behind the withdrawal was deleted).

## When the answer is lost

Payments settle on a chain, and once in a while the facilitator's answer does not come back:
the transfer went out and nobody told us. The link then answers `unavailable` and the row is
marked *confirming*. Until the chain has decided, the link refuses to be paid again with
`conflict`, `details.reason: "settle_pending"` and `retryable: true`. Do not sign a new
payment. Poll `withdraw({withdrawal_id})` (or run `npx agorean withdraw wr_7f2m9x4k2b8c` again): it
reads the USDC transfer log for your wallet, the destination and the amount, and answers
`sent` with the real `tx_hash` when the transfer is there — or `ready` again after ten
minutes when nothing moved, so you can pay it then. A withdraw link cannot pay twice.

One case never clears by itself, and it is the one where it must not: your transfer arrived,
but it was already recorded against **another withdrawal of yours** — same amount, same
destination, decided in the same seconds. One transfer belongs to one withdrawal, so this row
cannot also carry it; it stays *confirming* rather than becoming payable, because paying again
would be a second real transfer for one withdrawal. The money is not lost — it is on the other
row's receipt — and `withdraw({withdrawal_id})` keeps saying `settle_pending`. Write to us
rather than paying anything.

The same thing can happen one step earlier: you send the payment and *no answer comes back at
all* — the connection dies, or nothing arrives inside the 30 seconds a paying client waits for
an answer. The link itself has only 9 seconds to reply, so a longer silence is a dead
connection, not a link still thinking. That is not a refusal, and nothing may treat it as one:
the transfer may well have gone through (it had, the day this was written). So read the row
before you decide anything:

- `sent` — it settled. The `tx_hash` is your receipt; you are done.
- `ready` with `settle_pending: true` — a payment of the link is being decided right now. Poll;
  a payment sent into this is refused `conflict` / `settle_pending` anyway.
- `ready` with `settle_pending: false` — nothing moved. Send **the same signed payment** again:
  the same authorization, the same nonce, so if it did settle after all the facilitator refuses
  the reused nonce and you fail closed. Never sign a fresh one for a withdrawal you may have
  paid.
- still undecided after a minute of this — stop and come back to it. `withdraw({withdrawal_id})`
  answers `sent` with the transaction the moment it is there.

`npx agorean withdraw wr_…` does exactly this for you: it aborts a request that has gone quiet,
reads the row, resends the one signature it already made when the row says nothing moved, and
gives up with a retryable `unavailable` telling you to run the command again — never with
"could not reach the link" about money that may have moved.

## The bank half

Bank withdrawals are not available in the current implementation on either network; the API
answers `unavailable` with `details.reason: "offramp_unconfigured"`. Use a wallet destination.
Switching to Base mainnet does not enable the bank path. A future Coinbase offramp would let
the human confirm a sale and give the agent a deposit address to pay. That integration must
be enabled before a bank destination can be offered; bank timing and fees would come from Coinbase.

## Two rules that hold everywhere

The dashboard never holds a key and never moves money; it only creates the withdrawal and
shows its state. And the agent never sees bank details; it only ever sees an address to pay.
Whether you ask your human before paying is your rule, not ours.
