# Email: an inbox for your agent

An inbox your agent owns with nothing but its wallet. Paid in USDC over
x402. No signup, no API key: the paying wallet owns the booth and is the
only account. Receiving is included; sends and wait-code draw from a
prepaid balance with no chain write. This page is the email slice of the
contract; the whole of it, every product on one page:
`GET https://agentgates-backend.vercel.app/api/account/docs`.

## The product

- `inbox`: an address that reads like a person on our domain, receiving
  from day one. FREE to create once your wallet has a balance (top up
  first), and it never expires. Create:
  `POST https://agentgates-backend.vercel.app/api/inboxes` owner-signed (X-Wallet-Auth) with
  `{"name": "maria lopez" (optional), "persona": "<persona id>" (optional)}`;
  you get `201 {"inbox": {"id", "address"}}`. With no plan a wallet holds
  1 inbox and every send meters at the walk-up price; a PLAN
  raises how many you hold (starter 1, plus 3, pro 5; starter buys as the
  bare `month`/`year` rung, the others as `plus-`/`pro-` prefixed) and
  carries a daily send allowance (the catalog's `sendsPerDay` and
  `inboxes` per rung). Buy the plan onto the paying wallet over x402:
  `POST https://agentgates-backend.vercel.app/api/account/plans` with `{"rung": "<rung>"}`
  (rungs: `month` (30 days), `year` (365 days), `plus-month` (30 days), `plus-year` (365 days), `pro-month` (30 days), `pro-year` (365 days)).

## The booth, walk-up

No plan at all: one x402 payment is one use.
`POST https://agentgates-backend.vercel.app/api/booth` with `{"kind": "mail-code",
"timeoutSeconds"?, "pattern"?}` mints a throwaway day address, blocks up to
90s for the verification code, and answers
`{"address", "code", "from", "receivedAt", "expiresAt"}`. The signup flow:
pay with `"timeoutSeconds": 0` and the address comes back at once; give it
to the form; repeat the call with the SAME `X-PAYMENT` header and it
re-enters your booth and waits for the code, charging nothing. The booth
expires on its own; nothing to cancel. Each kind's per-use price is in
the catalog's `booth` block.

Prices are read at request time: `GET https://agentgates-backend.vercel.app/api/account/catalog`.
Every entry of a `402`'s `accepts` carries its own `maxAmountRequired`:
the sticker, FLAT, the same number on every network (the leg's on-chain
settle is the lane's cost, never added to yours). A sticker too small to
carry its own costs falls back to a per-leg floor, loudly, in the quote.
Networks offered:
`base` (USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, pay to 0xa714076d4bcf17f577fa4d224465946334abe9d5), `polygon` (USDC 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359, pay to 0xa714076d4bcf17f577fa4d224465946334abe9d5), `arbitrum` (USDC 0xaf88d065e77c8cC2239327C5EDb3A432268e5831, pay to 0xa714076d4bcf17f577fa4d224465946334abe9d5), `solana` (USDC EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v, pay to C9SQSYNay1kYhfnnktUX7NttGvzgKmjWkTnu16ZoQiYu). An inbox purchase pays the mail treasury: the `payTo` in
YOUR 402's `accepts` is the authority for that purchase.

## Pay a 402

1. POST the purchase with no payment header; the reply is `402` with an
   `accepts` array (x402, scheme `exact`).
2. Sign an EIP-3009 `TransferWithAuthorization` for `maxAmountRequired`
   USDC to `payTo`. Assemble the EIP-712 domain from the offer: `name`
   and `version` from `extra`, `chainId` from the network you chose
   (the catalog's `byNetwork[].chainId`), `verifyingContract` = the
   offer's `asset` (the USDC contract). The authorization's six fields:
   `from` (your address), `to` (`payTo`), `value`
   (`maxAmountRequired`), `validAfter` / `validBefore` (unix seconds;
   back-date `validAfter` a few minutes for clock skew, set
   `validBefore` to now + the offer's `maxTimeoutSeconds`, the settle
   window), `nonce` (`0x` + 32 random bytes, single-use). uint256
   values cross as decimal strings. Retry the same POST with
   `X-PAYMENT`: the base64 of this, complete:

   `{"x402Version":1,"scheme":"exact","network":"base","payload":
   {"signature":"0x<65-byte EIP-712 signature>","authorization":
   {"from":"0xYourAddress","to":"<payTo>","value":"200000",
   "validAfter":"1756500000","validBefore":"1756501500",
   "nonce":"0x<32 random bytes>"}}}`

   On a `solana` leg, build the partially-signed transaction the compute
   docs describe (`GET https://agentgates-backend.vercel.app/api/compute/docs`).
3. Settlement is confirmed on-chain, then the booth exists.

A timeout after you send `X-PAYMENT` is safe to retry: send the SAME
`X-PAYMENT` to the SAME endpoint and the receipt keyed by your
authorization's own nonce hands back what that payment already bought,
charging nothing. Never sign a NEW authorization to retry a call that may
have gone through.

## The prepaid balance

`POST https://agentgates-backend.vercel.app/api/account/balance` with `{"amountUsd": <rung>}` tops
up a prepaid balance over one x402 settle (rungs today: $1, $5, $25, $100).
Sends and wait-code draw from it with no chain write;
an empty balance refuses and nothing is served on IOU. Your balance never
expires and is not refundable. `GET` the same path owner-signed for the
balance and its ledger.

Or fund it from anywhere: `GET https://agentgates-backend.vercel.app/api/account/deposit` owner-signed
answers an address that is yours alone (a keyless contract derived from your
signing address, the same address on base or polygon or arbitrum). USDC sent there is credited to
your balance after 3 confirmations, then swept to the treasury; nothing
else can ever leave it. `POST` the same path with `{"scan": true}` to have a
fresh arrival credited now instead of within five minutes.

## A send, priced

Within your wallet plan's daily allowance a send draws
$0.00039 from the prepaid balance. Past the allowance (or
on no plan at all) each send meters at $0.005, the
pay-as-you-go price (the catalog's `sendOverflow.priceUsd`), instead of
stopping. The hard ceilings stand above every plan, per address and
per wallet per UTC day; a wallet paused for deliverability review keeps
receiving. One refusal outranks even a paid allowance: a recipient on the
platform suppression list (a hard bounce, a spam complaint, an
unsubscribe the sending relay already reported) refuses 403
`recipient_suppressed` BEFORE any money moves: your balance is
untouched, and this lane will not mail that address again.

Top up any amount from $0.005 (the catalog's
`balance.minTopUpUsd`): the whole-dollar rungs are quick picks, never a
wall.

## Your account

Your account is a key. There is no signup, no session and no token: twelve
words (BIP-39, key at `m/44'/60'/0'/0/0`) sign every call, hold the
prepaid balance, own the browser tasks and lock the vault. Whoever holds
the words is the account, on any machine.

Onboard yourself: `curl -fsSL https://agentgates-backend.vercel.app/api/passwords/install | sh`, then
`agentgates init`. It mints the words once and prints them with what to
tell your human. `agentgates init --seed "<twelve words>"` brings words a
human gave you; `AGENTGATES_PASSWORDS_SEED` is the same with no file.
`agentgates browser balance` proves you are in and prints the address that
tops the balance up.

Your login key (Agent APIs): the words never travel. `agentgates init`
mints a login key the moment the account exists, `agk_…`, shown once,
and sends it as `Authorization: Bearer agk_…` on every door below
instead of a signature. Mint another by name or revoke one with the
words: `POST https://agentgates-backend.vercel.app/api/account/keys` owner-signed
(`{"name":"…"}` mints, `{"revoke":"<id>"}` revokes), `GET` lists them
with their last use; `agentgates keys` is the same. A revoked key
answers 401 `bad_api_key`; the keys door itself takes the signature
only.

Coming back: there is nothing to log in. The key stays on this machine
(`~/.agentgates/passwords-key.json`, or `AGENTGATES_HOME`) and signs every
call; `agentgates init` again answers "key already here" with your address
and never replaces it unless you say `--replace`. A new machine takes the
words: `agentgates init --seed "<twelve words>"` (`agentgates init
--reveal-seed` prints them where you have them).

Hand the session across on one machine, never through a server:
`agentgates open` opens the browser already logged in as you, for your
human; `agentgates receive` prints a code your human types on the site
(Your agent → Connect an agent on this computer) and makes this CLI their
account. On a different machine, your human pastes the words on the site:
Log in → Restore from a recovery phrase.

## Owner calls

Reads and actions on a booth are wallet-signed, no session. Header
`X-Wallet-Auth: base64({"address","timestamp","nonce","signature"})` where
`signature` is an EIP-191 personal_sign over the exact string
`METHOD\npath\nnonce\ntimestamp`
(e.g. `GET\n/api/inboxes/abc/messages\n<nonce>\n<unix seconds>`).
A Solana wallet signs the SAME string with its ed25519 key, `signature`
base64 of the raw 64 bytes, `address` base58. Nonces are single-use;
timestamps must be within the server skew. The signature is bound to
`/api/account`; a wallet that can sign for compute or runs cannot read
your inboxes with that signature (wallet verb: `wallet_signIdentityAuth`).

- `GET /api/inboxes`: every booth
  this wallet holds (the recovery read after a crash or restart; ids are
  never gone with your state file).
- `GET /api/inboxes/{id}/messages` (and `?since=<ISO>`): the mail.
- `GET /api/inboxes/{id}/messages/{mid}`: text, html, attachments.
- `POST /api/inboxes/{id}/wait-code` `{"timeoutSeconds"?,
  "pattern"?}`: blocks up to 90s for a
  verification code and returns `{"code","from","receivedAt"}`.
- `POST /api/inboxes/{id}/send` `{"to","subject","text"}`.
- `POST /api/personas` `{"name","voice"?,"instructions"}` and
  `GET`: your personas, attachable to any booth you hold.
- `POST /api/account/renew/inbox/{id}` `{"rung"}`: another rung on the
  same inbox, paid over x402 from the owning wallet.

## Boundaries, in code

A booth belongs to the wallet that paid and only that wallet reads or uses
it. Every settlement is screened; sanctioned wallets cannot buy. An expired
booth answers 410 with the renew call. wait-code accepts a custom
`pattern` up to 64 characters with no parentheses.
