# Phone: a number for your agent

A phone number 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; texts, wait-code and call
minutes draw from a prepaid balance with no chain write. This page is the
phone slice of the contract; the whole of it, every product on one page:
`GET https://agentgates-backend.vercel.app/api/account/docs`.

## The product

- `phone`: a dedicated number on a WALLET PLAN, SMS both ways,
  wait-code, calls by the minute (your own handset bridged in, or the
  audio streamed to your own agent, or the AI operator).
  TODAY'S WALL: outbound texts open when our carrier registration (10DLC) clears, days away; receiving and wait-code work now (the catalog's
  `phoneUsage.sendingNote`; `POST .../sms` answers 503 `sms_dark`
  while it stands). The plan sells how many numbers your
  wallet holds (starter 1, plus 2, pro 5; the catalog's `phones` per rung,
  starter buying as the bare `month` rung);
  texts and call minutes METER per unit from the prepaid balance (the
  catalog's `phoneUsage`). Buy the plan onto the paying wallet over x402:
  `POST https://agentgates-backend.vercel.app/api/account/plans` with `{"product": "phone", "rung":
  "<rung>"}` (rungs: `month` (30 days), `plus-month` (30 days), `pro-month` (30 days)). Then order each number free
  of charge, owner-signed:
  `POST https://agentgates-backend.vercel.app/api/phones` with `{"country": "US" (optional),
  "persona": "<persona id>" (optional)}`; you get
  `201 {"phone": {"id", "number", "expiresAt"}}`. No plan = the walk-up
  booth below. Enterprise (more numbers, your own registration) is by
  quote.

## Usage, metered

The plan sells the numbers; usage meters per unit from the prepaid
balance, and the catalog's `phoneUsage` block carries today's `textUsd`,
`waitCodeUsd` and `minuteUsd`, read at request time
(`GET https://agentgates-backend.vercel.app/api/account/catalog`). A call runs at most
10 minutes and bills off the carrier's clock (a
bridge is two carrier legs and prices accordingly); a call that is never
answered bills nothing. The abuse ceilings stay hard on every plan: texts
per number and per wallet per UTC day, and a wallet paused for review
keeps receiving.

## The booth, walk-up

No plan at all: one x402 payment is one use.
The sign on the booth: `GET https://agentgates-backend.vercel.app/api/booth` answers
`open` (with `perMinuteUsd` and the minute windows) or the reason it is
closed, plus `now`, the server's own clock. It never says the house
number. There is NO sms booth: a verification code needs a number nobody
else is using, so codes over a shared house number are not sold; a
`mail-code` booth mints a FRESH address every time, and a phone PLAN's
own number does wait-code privately.
`"kind": "call"` is the walk-up call, payphone style:
`{"kind":"call","minutes":1|5|10|30,"to","bridgeTo"|"streamUrl"}` buys
its minutes upfront at the booth's per-minute price (the catalog's
`booth.call`; a bridge is two carrier legs), dials from the house
number, and the network itself hangs up at the cap. `bridgeTo` (E.164)
dials YOUR OWN handset first and connects it to `to`; `streamUrl`
(`wss://`) hands the live audio both ways to your own agent.
A bridge SPEAKS to your handset the instant you answer, before the far
side rings, so whoever picks up knows what they answered; its seconds are
ours, not yours. `announce` (bridge only, up to 120 characters)
replaces that line with your own, and `""` silences it. A stream call needs none: your agent speaks the moment audio
flows. A call that never connects re-enters FREE with the SAME
`X-PAYMENT`, and `{"kind":"call","hangup":true}` with that same header
ENDS the call when your agent is done: the line is yours until you hang up
or the window closes, and neither charges.
No handset and no agent of your own: `{"kind":"call","minutes","to","task",
"persona"?:{"name","instructions"?,"voice"?,"speed"?},"greeting"?}` puts
OUR operator on the line for the window (its price is the window's
`operatorPriceUsd` in the catalog's `booth.call`; `"brain":"yours"` with
a `webhook` runs your brain instead). `{"kind":"call","read":true}` with
the SAME `X-PAYMENT` reads that call: status, seconds, the transcript and
the recording url once it ends, and it never dials. 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). A phone purchase pays the phone 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).
Texts, wait-code and call minutes 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.

## 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/phones/abc/sms\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 numbers with that signature (wallet verb: `wallet_signIdentityAuth`).

- `GET /api/phones`: every booth
  this wallet holds (the recovery read after a crash or restart; ids are
  never gone with your state file).
- `GET /api/phones/{id}/sms` and `POST` `{"to","text"}`.
- `POST /api/phones/{id}/wait-code`: blocks up to 90s for a
  verification code and returns `{"code","from","receivedAt"}`.
- `POST /api/phones/{id}/calls`: the number places a call.
  `{"to","bridgeTo"}` (both E.164) dials YOUR OWN handset (`bridgeTo`)
  first, then the peer, and connects the two: you talk through the leased
  number and the peer sees the leased number. `{"to","streamUrl"}`
  (`wss://`) streams the live call audio both ways (mulaw 8 kHz) to your
  own endpoint: your agent is the voice on the line.
  `{"to","task","persona"?,"voice"?,"brain"?: "ours"|"yours","webhook"?,
  "greeting"?}` puts the AI operator on the call; `persona` is a persona
  id you own OR an inline `{"name","instructions"?,"voice"?,"speed"?}`;
  `greeting` (up to 300 chars) is spoken the instant the call connects.
  The catalog's `operator` block lists the `voices` the line can speak
  and the per-minute price with our brain on the call (`minuteUsd`).
  → `{"callId"}`; at most 10 minutes, billed per
  minute from the balance off the carrier's clock (a bridge is two carrier
  legs and prices accordingly); the operator's minute is ONE
  price, all in (the catalog's `minuteUsd`: the line, the brain, the voice
  and the ears), and a call never runs past what it paid for.
  A call that is never answered bills nothing.
- `GET /api/phones/{id}/calls/{cid}`: status, seconds, the
  draw so far (`billedUsd`), transcript, recording url.
- `POST /api/calls`: THE HOUSE CALL, no number of your own
  needed: the same operator body (`{"to","task","persona"?,"voice"?,
  "brain"?,"webhook"?,"greeting"?}`) goes out from the house number,
  metered from your balance at the same operator minute, the ceiling
  reserved first and released as the call bills (`GET /api/account/docs/voice`
  is the whole of it). `GET /api/calls` lists them,
  `GET /api/calls/{cid}` reads one.
- `POST /api/phones/{id}/answer` `{"persona","brain","webhook"?,"role"?}`:
  how the number answers inbound calls. `role` is one of the playbooks'
  inbound roles (`GET /api/playbooks`: receptionist, screener,
  order-taker, reservationist, after-hours, callback-line, hotline,
  answering-voicemail) and frames the operator on the call; the persona's
  `instructions` are its brief (the business name, the hours, the menu,
  who gets messages). An answered call meters from the balance at the
  operator minute like a call you place; with the balance short, the
  number does not answer.
- `POST /api/personas` `{"name","voice"?,"instructions"}` and
  `GET`: your personas, attachable to any booth you hold.
- Numbers renew with the phone PLAN (POST /api/account/plans), never
  one by one.

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