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

## Buying a plan while one is held

What a plan purchase does depends on the plan the account holds for that
product (`GET https://agentgates-backend.vercel.app/api/account/plans` shows each plan and the one
waiting behind it, `next`):
- the same rung again renews: its days are added after the current end;
- the other period of the same tier extends: it starts when the current
  plan ends (a month held and a year bought prepays the next twelve months);
- a higher tier upgrades now, prorated, and the unused value of the plan
  held comes off. Same period: the higher tier from now until the current
  end, charged the higher tier for the time left less the time left on the
  plan held. Other period: a new window from now, its price less that value;
- a lower tier waits for the current plan to end, paid now; the current
  plan is never cut short.
One plan may wait behind the current one. Buying it again adds to it;
buying anything else that must wait replaces it, and all it cost comes off.
Value left over beyond a price returns to the balance.

Ask before paying: `GET https://agentgates-backend.vercel.app/api/account/plans/quote?product=<inbox|phone|passwords|memory>`
(optional `&rung=<rung>`), owner-signed, answers per rung the `action`,
`chargeUsd` (what the balance is charged now), `creditUsd`,
`refundUsd`, `startsAt`/`endsAt` and a `summary` in words. Buy
from the balance with the quote echoed:
`POST https://agentgates-backend.vercel.app/api/account/plans` `{"rung", "product", "quote": {"at", "chargeMicro"}}`.
It is charged exactly that quote, or answers 409 `quote_stale` with a
fresh one when the plan or the price moved (a quote holds 15 minutes).
Over x402, name the paying wallet (`"payer": "<address>"`) so the 402 is
priced for its plan; a change that costs nothing now is made from the
balance.

## 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; `POST /api/calls/sample-brain` is
a webhook brain you can point at to hear the shape: two lines and a
goodbye). `{"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/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 access): 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.

Keys per section: the SCHEME is the scope. `{"name":"…","scope":"voice"}`
mints `agv_…`, a VOICE key. It places and reads calls, and reads what
they cost (your balance, your usage, the catalog and these docs); every
other door answers 403 `key_scope` and names what the key does open.

THE GRID (2026-09-18): a key is minted with `grants`, one entry per
section and verb, `phone|mail|logins|browser|balance|memory` × `read|write`.
Read opens the section's GET doors; Write its POST doors and implies Read.
The account reads (balance, usage, catalog, docs) open for every key; the
keys door and the seal doors take the signature only. POST /api/account/keys
{"name":"…","grants":["phone:read","mail:write"]}; no grants = full access,
the login key as it always was. The list carries each key's grants. A key
that opens Memory may be narrowed to chosen folders: `memoryFolders` at the
mint, or `POST https://agentgates-backend.vercel.app/api/account/keys/<id>/memory` after it.
`agk_…` is unchanged and opens everything the words open, which is what
an absent `scope` still mints. Hand an agent that only has to ring
somebody the voice key, so a leak never reaches your saved logins.

A key is TWO PARTS IN ONE STRING (2026-09-19):
`agk_<64 hex>.<64 hex>`. The part BEFORE the dot is the bearer and the
only half that ever reaches us: send that one, `Authorization: Bearer
agk_<64 hex>`. The part AFTER it is the key's reading part, made in your
browser at the mint, and it opens the key's copy of the account's data
key; it never goes on the wire, and a bearer carrying it answers 401
`key_reading_part` without being hashed or looked up. `agentgates
login <key>` takes the whole string and splits it for you. A key minted
before that date is one part and keeps working as it did.

Keys decrypt (2026-09-18): a key whose grid reads Phone or Mail holds
its own copy of the account's data key, so it opens sealed calls,
transcripts, recordings and mail on its own. The copy is made at the
mint on Agent access (the page seals the data key to a keypair derived
from the key's reading part) and stored beside the key row; `agentgates keys
use agk_…` runs the CLI on that key with no words on the machine, and
`agentgates calls` / `agentgates email read` open what they fetch.
`GET https://agentgates-backend.vercel.app/api/account/keys/me/wrap` (Bearer, the key's own)
answers the copy; a key without one hears 404 `no_wrap` and reads
clear rows only. Revoking the key ends the door and drops the copy. The
list marks such a key `sealed: true`.

What your keys spent: `GET https://agentgates-backend.vercel.app/api/account/usage`, the signature
or a login key, `?from=2026-09-01&to=2026-10-01` (dates or ISO
instants, `to` exclusive, this UTC month by default). It answers the
window's total, `byKey` (every key the account has held, with its own
`byLane` breakdown) and `byLane` across the account. Draws the
ledger cannot name a key for, an owner-signed call among them, land under
`unattributed` and are never folded into a key's row.

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"}`; `minutes` sets the window (1 to 10,
  the ceiling when you name none) and is what the call reserves and is cut
  off at, 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`), the transcript, whether a recording exists (`recording`), the grade and your own tap.
  `GET /api/phones/{id}/calls/{cid}/recording` streams the recording
  (owner-signed, never a url); `POST .../{cid}/grade` `{"did":"yes"|"partly"|"no"}`
  is your own answer to "did it do what you asked", kept beside the
  machine's grade (`grades`: a result word, booleans, a score and counts,
  never a word of the call).
- SEALED CALLS, ONE WAY. Every account's calls are sealed to the account's
  own key: the AI on the line seals the transcript (your task inside it)
  and the audio to your data key the moment the call ends and keeps no
  copy it can open; the row's `task` is null and `transcript` is one
  sealed blob (`sealed: true`), the recording route streams ciphertext
  (`x-call-sealed: 1`). The CLI and the page open both with the key your
  phrase or your wallet derives. The key is made on your first open of the
  Voice page or your mail, or by the CLI on its first call; `POST
  /api/calls` on an account with no key yet answers 409 `seal_missing`
  and charges nothing. What we still see: who called whom, for how long,
  what it cost, and the grade. The AI on the line hears the call: OpenAI's
  GPT-Live is the ears and the voice, Claude is the brain through
  OpenRouter; Telnyx carries the audio. GET /api/calls/cvm is the voice
  CVM's attestation: the Intel TDX quote bound to a key derived inside it,
  and the measurement of the code it runs. An agent on its own key opens
  your sealed calls two ways: a GRANT (`agentgates passwords allow --agent
  <it>` wraps your data key to its sealing key; it reads with
  `?owner=<you>` and opens locally) or the CVM DOOR (`POST .../{cid}/read`
  `{"permitId"}`, signed by the agent under a permit that lists `calls`:
  the passwords CVM opens the transcript and answers the words, never the
  audio).
- `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, `GET .../{cid}/recording` streams
  the recording, `POST .../{cid}/grade` takes your tap, `POST .../{cid}/read`
  is the agent's door to a sealed 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. `"brain":"agent"` puts your agent on the
  calls (the agent must be on, 404 `no_agent`; no persona needed): a
  caller bound on the site (the cell you bound for texts) is heard, what
  they say lands in the agent's thread, the agent's answer is spoken on
  the call or texted to that cell after it; any other caller hears one
  line and the call ends.
- `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.
