# Your account: the products, the owner-signed calls, the balance

Inboxes, numbers, passwords, a browser and a voice operator 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; the small units (a
send, a text, a wait-code, a call minute) draw from a prepaid balance with
no chain write.

## Products

- `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://api.agentgates.ai/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://api.agentgates.ai/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)).

- `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://api.agentgates.ai/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://api.agentgates.ai/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.

- `passwords`: an agent's own logins, which it uses without ever
  seeing them. IT NEEDS NO HUMAN AND NO BROWSER: it mints a key, stores its
  logins and permits itself, all over signed HTTP. A person is needed only to
  hand over a login that is theirs, or to hold the guardrail (the ask-me
  switch below). The whole lane, from a terminal (`npm i -g @agentgates/cli`,
  or `curl -fsSL https://api.agentgates.ai/api/install | sh`):
  `agentgates passwords init` mints twelve words and the key this machine
  signs with (ONE key: it signs every call and its one signature over the
  frozen vault message locks what it stores; it never leaves the machine);
  `agentgates passwords add --label <name> --host <site> --username <u>
  --password <p>` seals a login here and stores it (the store call below);
  `agentgates passwords allow [--agent <addr>] [--ask-me]` signs the permit
  (the permit call below); `agentgates passwords use --url <login url>`
  logs in through the CVM (the use call below); `agentgates passwords list`
  reads what the key holds and may use; `agentgates passwords invite`
  prints a link a PERSON opens to take those logins under their own wallet
  or twelve words (the same key signs and locks) and set the guardrails
  (which uses, how many a day, for how long, "ask me before each use"). The link carries
  the key to those logins until it is used, withdrawn or expires (15
  minutes by default, 60 at most); once accepted the rows are the person's
  and the agent uses them under their permit. Every command prints JSON
  when stdout is not a terminal, and every refusal is one of the codes
  below, verbatim. The
  wallet iframe seals each password twice, to the key you sign with (its
  signature over the frozen vault message, or your passkey) and to an
  attested confidential VM's key, and this platform stores ciphertext it
  cannot open. A use happens inside the CVM; the secret is scrubbed from
  everything that comes back. Store (owner-signed, FREE at every tier):
  `POST https://api.agentgates.ai/api/passwords` with the wallet-built
  `{"id","label","kind","hosts","username"?,"credentialId","passkeyBlob",
  "cvmBlob","cvmKeyId"}`. Permit an agent (owner-signed):
  `POST https://api.agentgates.ai/api/passwords/permits` with `{"typedData",
  "signature"}` (the wallet's EIP-712 PasswordPermit, v1 or v2; v2 carries
  `approveEachUse`, the owner's "ask me before each use"; revoke with
  `POST .../permits/{id}/revoke`). The agent lists what it may use
  (`GET https://api.agentgates.ai/api/passwords`, agent-signed) and uses a row:
  `POST https://api.agentgates.ai/api/passwords/{id}/use` with `{"use":
  "header"|"totp"|"login"|"session"|"reveal"|"signup","request":{...}}` →
  `{"ok","result","seconds","chargedUsd"}`. WHAT THE AGENT GETS is the
  permit's `uses`, and the owner picks one of two: `session` (the CVM
  logs in and hands back the site's cookies in the clear, `result.cookies`,
  for your own browser; no steps; priced as a login) or `reveal` (the
  secret itself, `result.secret`, a connected account's jar, an api
  connection's refresh token; never a 2FA seed). `login`/`totp`/`header`
  act inside the CVM and hand back nothing usable elsewhere; every permit
  lists them, and an agent that would rather work inside the CVM uses
  them. A use the permit does not list answers 403 `use_not_allowed`;
  ask-me parks all of them the same way. `signup` REGISTERS THE ACCOUNT
  OWNER on another site, under the permit they signed, using a contact the
  account already holds (a Gmail login or connected account the owner
  connected, or an IMAP mailbox row): `{"use":"signup","request":
  {"url":"https://<site>/signup","password":"<the one you generated>",
  "email"?:"<the mailbox's address or a plus alias of it>","name"?,
  "username"?,"steps"?}}` → `result.{ok,code,verified,filled,email,
  username,digest,cookies}`: the CVM fills the form, agrees to the terms,
  submits, reads the code or the link the site emails from that mailbox and
  finishes; `verified` is `code`, `link` or `none`, and
  `cookies` is the new account's session for your own browser. The CVM
  keeps nothing: store the login yourself (`agentgates passwords add`,
  under your own key; `agentgates passwords signup` does both). Priced as
  a login. THE CODE SOURCE CAN BE AN INBOX OR A NUMBER THIS ACCOUNT OWNS,
  so no personal address and no second row is involved: `"request":
  {"url", "password", "source": {"kind": "inbox"|"phone", "id": "<its id>"}}`
  posted on `/api/passwords/<that id>/use` (the use runs on its source; an
  inbox lends its address, a number is typed into the form's number field
  and the code comes by SMS). Exactly one source: `{"kind":"mailbox","id":
  "pw_…"}` is the row the use runs on, and two named is a 400. The CVM
  opens nothing for an inbox or a number: at the code step it holds the page
  and this API reads the code through the same wait-code door you would
  (`POST /api/inboxes/{id}/wait-code`, `/api/phones/{id}/wait-code`, the
  wait's own draw, said in the answer as `codeRead`, never charged twice)
  and the sign-up goes on by itself. A SEALED inbox (your mail sealed to
  your own key) is read by that key alone: the use parks 202
  `code_pending` naming the `source`; read the code with your key
  (`agentgates email wait-code`) and call again with `useId` and
  `"request":{…,"otpCode":"<the six digits>"}`, or type it on your board.
  The permit lists `mail` to read an inbox's codes and `sms` to read a
  number's; neither implies the other. `{"use":"code","request":{"from":
  "<site>","source":{...}}}` on an inbox or a number reads the newest code
  the same way, without the CVM. A code from a service mailing for the site is read when the
  message NAMES the site; a link is opened only when, after every redirect
  the CVM walks itself without your session, it ENDS on the site's own
  domain. THE USE DOOR TAKES YOUR SIGNATURE (X-Wallet-Auth over
  that exact path), never a login key: the CVM re-verifies it before it
  opens anything, and a key answers 401 `signature_required` there
  (`agentgates passwords use` signs that one door with the words on its
  own). TRY IT FIRST: with no plan a wallet holds
  50 passwords and gets 50 uses a day, free
  (the catalog's `payg.passwords` and `payg.passwordsUsesPerDay`); then a
  plan. Within the plan's daily allowance a use is the plan's;
  past it, it meters from the AGENT's prepaid balance (the catalog's
  `passwordsUsage`), 402 `balance_empty` when that is dry (the sentence
  says what ran out and what the next use costs; `balance.topUp` names
  the top-up route, `allowance.resetsAt` when the day's uses come back).
  A use the
  owner has to answer parks: 202 `approval_pending` (the permit asks
  before each use) or 202 `code_pending` (the site sent the owner a
  code) with `useId` and `expiresAt`; call the same route again with
  `{"use","request","useId","wait":25}` and it waits on the owner's
  answer, then serves as above, or answers 403 `approval_refused` |
  410 `approval_expired` | 409 `approval_mismatch` (a different
  request). `approvalWindowSeconds` on the first call shortens your own
  wait. A login may also carry `"steps"`, up to 12 of
  them, which the CVM's browser runs inside the sealed session once the
  login lands: `{"kind":"goto","url"}`, `{"kind":"click","selector"}`,
  `{"kind":"fill","selector","text"}`, `{"kind":"wait","text",
  "seconds"?}` (at most 15 s each),
  `{"kind":"read"}`, `{"kind":"select","selector"|"label","option"}` (a
  dropdown or a picker, the option by its text), `{"kind":"code","selector"}`
  (the code the site emailed, read from the mailbox and typed) and
  `{"kind":"owner","wall"}` (stops so the owner finishes a step only they can
  complete) and `{"kind":"look"}` (the page as the CVM reads it, once settled:
  inputs and controls by exact selector, any verification widget and the
  frames by host, the words). They get 60 seconds together
  (`"stepsSeconds"` asks for less), a `goto` may only reach the hosts
  the row was stored for, and `result.steps` says where each one got to;
  a click may name the text on the control instead of a selector
  (`{"kind":"click","text":"Try another way"}`), and
  `{"kind":"press","key"}` presses Enter, Tab, Escape, Space or an arrow on
  the focused element. WORKFLOWS THE OWNER TAUGHT: the owner shows the CVM a
  task once at the controls of their own sign-in (Teach, then Save under a
  name) and each row in `GET /api/passwords` lists its `workflows`
  (`name`, `host`, `steps`, `inputs` [{name, note}], `runs`,
  `lastCode`). Run one by name on the login use:
  `"request":{"workflow":"Latest statement","inputs":{"searchStatements":"april"}}`
  (`url` may be left out: the workflow's own sign-in address is used; a
  value you leave out types what the owner typed). The CVM signs in, then
  runs the owner's steps; a control the site changed is found by its other
  names, then by the CVM's own model, and the mended steps are kept for the
  next run. `result.workflow` says `{name, ran, of, mended}`, and
  `result.steps` says where each step got to; a run takes up to
  120 seconds after the login. 404 `workflow_not_found` names
  a workflow the row does not have. THE ACCOUNT'S SIGN-IN
  VERIFICATION STEP: the owner provisions the account's own material into
  the same sealed row and the CVM completes the site's step from it. A
  login row may carry `backupCodes` (the account's own list, pasted once)
  and a `device` credential (the owner's own, provisioned through the CVM
  on the account's security page: `"request":{"url","enrollDevice":true,
  "steps":[...to the page where the account adds a device...]}` signed by
  the OWNER's own key, which gets `result.device` back to seal into its
  own copy). The permit's `uses` lists the factors an agent may complete:
  `device` (the CVM answers the site's device step; `result.mfa` reads
  `device`) and `codes` (the CVM spends the next unused backup code and
  marks it used; `result.mfa` reads `codes`, `result.codesLeft` counts),
  and `mail` (the code a site emails, read by the CVM from the owner's
  connected mailbox: a `mailbox` row, the IMAP host as its host, the
  address as its username, an app password as its password; a login use
  names it as `"request":{"mailbox":"pw_…"}` or the account's one mailbox
  row is taken, and `result.mfa` reads `mail`; an inbox this account
  owns reads under `mail` too), and `sms` (a code texted to a number this
  account owns, read by this API for a sign-up on that number; `result.mfa`
  reads `sms`).
  `"codeSteps"` (at most 4 clicks or waits) reach a code field a site
  keeps behind a "try another way" menu. A step the row cannot complete
  parks for the owner as above; a site that asks the owner to approve on
  their phone parks as 202 `tap_pending` (`prompt` says what the page
  asks) while the CVM holds the page open for a minute; call again with
  `useId` and it serves when the owner has tapped, or answers 410
  `tap_expired`;
  a `read` and the step that stopped the run carry the page's digest,
  scrubbed. A stopped run answers `result.code` `step_failed` |
  `step_not_allowed` (a fill aimed at a password field, never allowed) |
  `step_text_not_seen` | `steps_timed_out`.
  A sign-in the site met with a verification step instead of its account answers
  `result.code` `challenge` with `wall` naming it (`cloudflare` |
  `datadome` | `captcha` | `verification`) and `error` quoting the
  page's words; the password was never judged, so it is not a failed login
  and does not count toward the lock.
  BOT CHECKS the CVM passes, when the account owner says so: Cloudflare's
  page, a Turnstile or reCAPTCHA box. It ticks the box the way a person
  does, and a Turnstile or reCAPTCHA that asks for more goes to the CVM's
  solver service. Never on Amazon or a ticket seller (Amazon's agent terms
  forbid it; the BOTS Act makes it federal law for tickets). The owner's word is one of three. Once, on the use in hand; from
  now on, for every use (`POST https://api.agentgates.ai/api/passwords/settings`
  `{"solveChecks":"solve"}`, owner-signed; `ask` is the default); or
  none, the owner passing the check at the controls. A use that meets a
  check the owner has not ruled on parks as 202 `check_pending` (`check`
  names it) and the owner's board asks; call again with `useId` (and
  `"wait"`) and it runs again with the answer. The owner's own key may say
  `"solve": true` on its own use. `result.solved` lists the checks the CVM
  passed (`kind`, `how`: `hand` | `service`, `ms`); a refusal a check
  stood behind carries `result.check` (`kind`, `passable`, `tried`,
  `why`). hCaptcha, DataDome, Arkose, GeeTest and AWS WAF are named, never
  passed, and a site that blocks the CVM's address outright has no check to
  pass.
  THE SITE BOOK says what we already know about a site's sign-in page:
  `GET https://api.agentgates.ai/api/passwords/sites` (public, no signature) answers the
  sites that have been walked, the shape of each one's sign-in, how many
  seconds it took, the wall where one stood, and whether a sign-up is
  allowed there; a site with `status` `not-walked` is one nobody has
  measured, not one that will not work. A rig re-walks the book and rewrites
  it, so a site that changes is measured again rather than guessed at. When
  the book holds the site your use names, the CVM is handed that record's
  shape and the name of the control that moves the page forward, and it
  skips the waits a generic finder needs; `result.hinted` says which hints
  it used. A hint only narrows what the browser does. It never widens where
  the browser goes: the row's sites, your permit's hosts and the CVM's own
  sign-in families are the walls either way, so a site we have wrong costs
  you a slower sign-in and nothing else, and a site we do not hold at all
  works exactly as before.
  Two row kinds are CONNECTED ACCOUNTS, stored the same way and used with
  one use each. `"kind":"session"` holds a site's cookie jar the owner
  sealed from their own browser: the `login` use starts already signed in,
  types nothing, runs its steps there, and answers `result.code`
  `session_dead` when the site has signed that connection out (the owner
  reconnects on their board). `"kind":"oauth"` holds a provider refresh
  token: the CVM spends it inside the enclave and the `header` use injects
  the minted access token through the new template field
  `{accessToken}` (`"inject":{"where":"header","name":"Authorization",
  "template":"Bearer {accessToken}"}`), so your call reads the provider's
  answer and never a token. Either kind's material is refreshed by the CVM
  itself and written back sealed; nothing is handed to you. Their refusals:
  403 `oauth_no_token` | `oauth_refresh_rejected` (reconnect it),
  502 `oauth_no_access_token`, and 403 `use_not_allowed` when a
  connected row is asked for the other use.
  Refusals, verbatim codes: 403 `no_permit` | `use_not_allowed` |
  `host_not_allowed` | `step_host_not_allowed` | `login_locked` (five
  failed logins lock a row until midnight UTC), 400 `too_many_steps` |
  `steps_too_long`, 429 `use_at_capacity`, 503 `product_dark` with
  `startingInSeconds` while the CVM boots, 503 `box_busy` with
  `retryInSeconds` while the CVM's login slots are full (a 503 that names
  its seconds is a wait, not a refusal: sleep them and call again; the CLI
  does), 503 `approval_unavailable`
  while the ask-me queue is not open on this deployment. A plan raises the
  rows held and the daily uses: `POST https://api.agentgates.ai/api/account/plans` with
  `{"product":"passwords","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)).
  A signed call in Python, nothing of ours installed
  (`pip install eth-account requests`; the seed is the 12 words
  `agentgates passwords init` printed, or any BIP-39 phrase):
```python
import base64, json, os, secrets, time, requests
from eth_account import Account
from eth_account.messages import encode_defunct

ORIGIN = "https://api.agentgates.ai"
Account.enable_unaudited_hdwallet_features()
agent = Account.from_mnemonic(os.environ["AGENTGATES_PASSWORDS_SEED"])

def call(method, path, body=None):
    nonce, now = secrets.token_hex(16), int(time.time())
    signed = agent.sign_message(encode_defunct(text=f"{method}\n{path}\n{nonce}\n{now}"))
    sig = "0x" + signed.signature.hex().removeprefix("0x")
    auth = base64.b64encode(json.dumps({"address": agent.address, "timestamp": now, "nonce": nonce, "signature": sig}).encode()).decode()
    r = requests.request(method, ORIGIN + path, json=body, headers={"X-Wallet-Auth": auth})
    return r.status_code, r.json()

print(call("GET", "/api/passwords"))
```

- `browser`: a browser agent your agent sends to a site with a task in
  words. A worker opens a real Chrome, our own shelf model decides each
  step and our code executes it, on any public site the task leads to,
  and the answer comes back in the agent's words with the transcript of
  what it did. It never signs, never pays and never types a key. Name
  `hosts` and the task is fenced instead: the site's own origin and
  those hosts, no link that leaves them followed. The site and every
  host is a public address: a loopback, private or link-local one is
  refused before money, and never browsed on the way. From a terminal
  (`npm i -g @agentgates/cli`, or `curl -fsSL https://api.agentgates.ai/api/install | sh`,
  then `agentgates init` for the account and its login key; every browser
  door takes `Authorization: Bearer agk_…` or the signature):
  `agentgates browser run <url> --task "<words>" [--steps N] [--host h] [--wait]`
  queues a task and, with `--wait`, prints the answer when it lands;
  `agentgates browser tasks` lists yours; `agentgates browser task <id>`
  reads one with its transcript; `agentgates browser pricing` is this
  catalog. The wire, owner-signed (X-Wallet-Auth over the exact path):
  `POST https://api.agentgates.ai/api/browser/tasks` with
  `{"url": "https://site", "goal": "what to do, in words", "hosts": ["extra.host"]?, "maxSteps": 20?, "maxSeconds": 900?, "login": {"row", "auth"}?, "cart": true?}`
  (no `hosts`: any public site, and the task's `anywhere` reads true;
  a list, even an empty one, fences the task to the site and those hosts.
  `login` runs the task INSIDE the saved login's own browser on the
  passwords CVM: `row` is the row, `auth` your X-Wallet-Auth over
  `POST /api/passwords/<row>/use` signed for this task; the permit
  lists `session`, the task starts on the row's site and goes where
  the task leads, the answer carries `join: "door"`, and a login
  refusal is that door's own sentence with nothing queued; `agentgates
  browser run --login <row>` does it. `cart: true`, only beside a
  `login`, fills a cart in that account and never checks out: the task
  answers on the cart page with every line and the total as the shop
  showed them, a click that would check out or pay is refused by the
  worker, and you open the same cart in your own account to buy it)
  → `201 {"task": {"id", "status": "queued", "reservedUsd", …}}`. Money
  (the catalog's `browserUsage`): $0.005 a step, reserved for every step the
  task may run when it is queued, the steps that ran charged from the
  prepaid balance when it answers, nothing charged when it fails; 402
  `balance_empty` names the reservation and what is available. Ceilings:
  `maxSteps` 1 to 40 (default 20), `maxSeconds` 30 to 900,
  `hosts` at most 10, `goal` at most 2000 characters (400 `bad_request`
  otherwise); 503 `product_dark` while no worker is connected. Read it
  back: `GET https://api.agentgates.ai/api/browser/tasks` (yours, newest
  first) and `GET https://api.agentgates.ai/api/browser/tasks/{id}` (the task
  with `answer`, `reached`, `wall`, `finalUrl`, `transcript`,
  `findings`, `ledger`); `status` is `queued` | `running` | `done` |
  `failed` | `cancelled`, and a failed task carries `code` and
  `error` (`no_answer`: the step ceiling or the clock ended it with no
  answer; `worker_lost`: the worker went quiet for 30 minutes).
  `POST https://api.agentgates.ai/api/browser/tasks/{id}/cancel` cancels a
  queued task and releases its reservation (409 `not_queued` once it is
  claimed). Poll the read every few seconds while a task runs; a task
  takes seconds to minutes. From a wallet instead of the balance:
  `POST https://api.agentgates.ai/api/browser/tasks/pay` with the same body
  answers 402 with the legs (USDC on the lane's networks, paid to the
  browser treasury); retry with `X-PAYMENT` for `maxSteps` at the step
  price, the paying wallet owns the task, the steps that run are charged
  and the rest lands on that wallet's balance.
  Measured 2026-09-07 with the driver `deepseek/deepseek-v4-flash`: public sites: 6 of 10 tasks (median 1 step, 11 s, $0.005 a task). logged in with a stored password: not in v1. pays, signs, types a key or creates an account: 3 of 4 refused (median 1 step, 12 s, $0.005 a task). The catalog's `browserUsage.proficiency`
  carries the same numbers.

- `memory`: one sealed folder tree per account: folders of markdown
  files (front matter with `id`, `type`, `aliases`, dated facts,
  `[[links]]`) that an agent lists, greps and reads in its own process.
  Every name and body is sealed to the account's ONE data key with the mail
  envelope (`v1.<epk>.<nonce>.<ct>`) before it reaches this API; the
  folder slug and the object id are the only clear facts. Agentgates stores
  ciphertext and can open none of it. A key minted with Read on Memory holds
  its own copy of the data key (`GET /api/account/keys/me/wrap`); a key
  without it fetches ciphertext it cannot open. No vector index: retrieval
  is grep over small files, the way the best memory products work.
  Doors (owner signature or a key whose grid opens Memory; GET is Read,
  POST is Write):
  `GET https://api.agentgates.ai/api/memory` (sealed?, folders with counts, capacity),
  `POST https://api.agentgates.ai/api/memory/folders` `{ folder }` (a slug, the grant unit),
  `GET https://api.agentgates.ai/api/memory/folders/<folder>` (ids, sealed names, sizes),
  `GET|POST https://api.agentgates.ai/api/memory/folders/<folder>/objects/<id>` (`{ blob, nameBlob }`; the id is 22 base64url chars the writer mints),
  `POST .../objects/<id>/delete`, `POST .../folders/<folder>/delete` (signature only).
  About you is what the owner tells every agent up front (addresses, sizes,
  allergies), one sealed object, one fact per line (`- <line>`), read at the
  start of every task: `GET https://api.agentgates.ai/api/memory/about` (`{ id, blob }` or
  `{ id: null }`), `POST https://api.agentgates.ai/api/memory/about` `{ id, blob }` (the
  owner's signature only; a key answers 403 `signature_required`). Seal it
  under the object AAD with the folder `profile`.
  Files are photos, videos, PDFs, any file, sealed on the writer's side in
  file seal v1: a fresh 32-byte content key per file, 4 MiB blocks each
  stored as `nonce (24) || XChaCha20-Poly1305` under the AAD
  `AGENTGATES-MEMORY-v1|block|<owner>|<id>|<index>|<1 if last else 0>`, and a
  header `{ v: 1, name, type, size, block, blocks, key }` sealed with the mail
  envelope under `AGENTGATES-MEMORY-v1|file|<owner>|<folder or empty>|<id>`.
  The bytes go straight to storage on presigned URLs and never cross this API:
  `GET https://api.agentgates.ai/api/memory/files` (the top level and its folders;
  `?folder=<f>&cursor=<c>` for one folder, 48 to a page),
  `POST https://api.agentgates.ai/api/memory/files` `{ id, folder, headerBlob, thumbBlob, bytes }`
  (`bytes` is the ciphertext length; answers a presigned `put`, or a
  `multipart` upload whose parts are two sealed blocks each),
  `POST .../files/<id>/done` (the stored size must equal `bytes`, or the file
  is removed and the door answers 409 `not_uploaded`),
  `GET .../files/<id>/url` (a presigned GET, five minutes),
  `POST .../files/<id>/move` `{ folder, headerBlob, thumbBlob }`,
  `POST .../files/<id>/delete`. A key moves and deletes only the files it
  added (`signature_required` otherwise); a narrowed key never reaches the top
  level. A file is never overwritten: a new version is a new file.
  A key may carry a narrowing to some folders (the accounting agent reads
  `accounting` and nothing else); outside it a door answers 403
  `folder_scope`, and a narrowed key never reaches the Files top level.
  The owner sets it on the key sheet, or with the signature:
  `POST https://api.agentgates.ai/api/account/keys/<id>/memory` `{ folders: ["accounting", "profile"] }`
  (`{ folders: null }` clears it: every folder again). One list covers Files
  folders, Agent memory folders and About you (`profile`); a connected
  account's folders answer to their own folder name, or to the provider's
  (`github`, `google-drive`, `dropbox`). A mint may carry the same list
  as `memoryFolders`; the key list carries each key's `memoryFolders`
  (null = every folder). Capacity is what a plan sells: folders and bytes; past
  it a write answers 429 `memory_full` naming the cap. A memory plan is
  bought at `POST https://api.agentgates.ai/api/account/plans` `{ rung, product: "memory" }`
  (the catalog's memory rungs and prices, with the bytes each holds). Writes,
  storage and reads cost nothing inside the capacity. From a terminal:
  `agentgates memory`, `agentgates memory ls <folder>`,
  `agentgates memory put <folder>/<name> --file <path>`,
  `agentgates memory cat <folder>/<name>`, `agentgates memory grep <pattern>`,
  `agentgates memory about`, `agentgates files`, `agentgates files put <path>`,
  `agentgates files get <folder>/<name>`.
  Kept until you delete it; a delete removes the only copy.

## Buying a plan while one is held

What a plan purchase does depends on the plan the account holds for that
product (`GET https://api.agentgates.ai/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://api.agentgates.ai/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://api.agentgates.ai/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.

## The booth, walk-up

No plan at all: one x402 payment is one use.
`POST https://api.agentgates.ai/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 sign on the booth: `GET https://api.agentgates.ai/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://api.agentgates.ai/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 and an inbox
purchase the mail treasury, so `payTo` differs by product: the one 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://api.agentgates.ai/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://api.agentgates.ai/api/account/balance` with `{"amountUsd": <rung>}` tops
up a prepaid balance over one x402 settle (rungs today: $1, $5, $25, $100).
Sends, 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://api.agentgates.ai/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://api.agentgates.ai/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://api.agentgates.ai/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://api.agentgates.ai/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://api.agentgates.ai/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://api.agentgates.ai/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/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` and `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/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"}`.
- `DELETE /api/inboxes/{id}`: release the address and mint another.
  TWO RULES, and they are the whole rule: an address that has EVER SENT
  stays with this wallet for good (`has_sent`), and a wallet may release
  2 addresses in its whole life (`change_cap`). A released
  address is retired for good and goes to no other wallet, so nothing of
  yours arrives in a stranger's inbox later. Fix a typo before you send;
  after that, the name is yours.
- `GET /api/phones/{id}/sms` and `POST` `{"to","text"}`.
- `POST /api/phones/{id}/wait-code`: same as the inbox.
- `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.
- `POST /api/account/renew/inbox/{id}` `{"rung"}`: another rung on the
  same inbox, paid over x402 from the owning wallet. Numbers renew with
  the phone PLAN (POST /api/account/plans), never one by one.

## Your email, your name, and talking to us

Give your account an email once, so we can reach you (a machine about to be
deleted, an answer to your message). It is never shown to anyone. The same
owner signature as every call above; a key cannot change it.

- `POST /api/account/email` `{"email"}`: mails a 6-digit code (15 minutes, five
  codes an hour).
- `POST /api/account/email/verify` `{"code"}`: proves the address.
- `GET /api/account/email`: `{"email","verified","pending"}`.
- `POST /api/account/name` `{"name"}`: a name or an alias for the account.

Talk to the founder: `POST /api/contact/threads` `{"name","email"?,"text"}`
answers `{"threadId","token"}`; post more with `POST /api/contact/threads/{id}/messages`
`{"text"}` and read the conversation with `GET /api/contact/threads/{id}`, both with
`Authorization: Bearer <token>`. Replies come from a person.

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