# AgentOS AgentOS is a wallet-isolated REST ASP for autonomous agents on OKX.AI. It provides real Resend email and AgentPhone telephony with fixed per-call USDT prices through the OKX Agent Payments Protocol. It is not MCP and never simulates provider success. ## Canonical resources - [Service catalog](/api/v1/services): canonical service IDs, fixed prices, inputs, errors, and next actions. - [OpenAPI](/openapi.json): complete OpenAPI 3.1 contract. - [Operational guide](/docs): authentication, service flows, WebSocket protocol, deployment, and recovery. - [API root](/api/v1): compact discovery. ## Start Here - Email starts with email.mailbox.create: POST /api/v1/email/mailboxes. - Phone starts with phone.number.us.30d or phone.number.ca.30d. - Domain registration is unavailable; do not send payment. - A first provisioning call uses x402 and returns authentication.accessToken after real fulfillment. - Every secondary endpoint requires that credential. Missing credentials return HTTP 428 ONBOARDING_REQUIRED before payment settlement. ## Authentication Wallet/payer identity creates the tenant boundary. First send the provisioning body, receive PAYMENT-REQUIRED, use an OKX x402 quote/pay flow with explicit payer approval, and replay the identical request with PAYMENT-SIGNATURE and Idempotency-Key. Save the returned at_v1 access token. It has no automatic expiry and is shared across all services for the same wallet. Every paid endpoint has an independent fixed charge. The realtime token is different: obtain it from GET /api/v1/events/realtime-token. It expires after 15 minutes and authenticates only the WebSocket event session. ## Service Catalog - discovery.api: GET /api/v1; price=free; startHere=false; auth=none; available=true; guide=/docs#discovery; purpose=Discover AgentOS authentication, guides, and service groups - discovery.services: GET /api/v1/services; price=free; startHere=false; auth=none; available=true; guide=/docs#service-catalog; purpose=List the canonical public AgentOS service catalog - events.list: POST /api/v1/events/list; price=free; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#event-delivery; purpose=List tenant-scoped durable events with filters and cursor pagination - events.get: POST /api/v1/events/get; price=free; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#event-delivery; purpose=Get one tenant-owned durable event - events.ack: POST /api/v1/events/ack; price=free; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#event-delivery; purpose=Idempotently acknowledge one tenant-owned event after durable handling - events.ack-all: POST /api/v1/events/ack-all; price=free; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#event-delivery; purpose=Acknowledge all matching tenant events up to an explicit cutoff - events.realtime-token: GET /api/v1/events/realtime-token; price=free; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#websocket-protocol; purpose=Issue a short-lived credential for the private AgentOS WebSocket gateway - email.mailbox.create: POST /api/v1/email/mailboxes; price=0.25 USDT; startHere=true; auth=x402 bootstrap; available=true; guide=/docs#email-flow; purpose=Provision one real mailbox on the configured Resend domain - email.mailbox.list: GET /api/v1/email/mailboxes/query; price=free; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#email-flow; purpose=List mailboxes owned by the authenticated wallet tenant - email.mailbox.update: POST /api/v1/email/mailboxes/update; price=0.01 USDT; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#email-flow; purpose=Update one owned mailbox - email.mailbox.delete: POST /api/v1/email/mailboxes/delete; price=0.01 USDT; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#email-flow; purpose=Delete one owned mailbox and its stored messages - email.message.send: POST /api/v1/email/messages/send; price=0.02 USDT; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#email-flow; purpose=Send one real email from an owned mailbox through Resend - email.message.query: GET /api/v1/email/messages/query; price=free; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#email-flow; purpose=List messages or retrieve one complete owned email - phone.number.us.30d: POST /api/v1/phone/purchase-us-number-30-days; price=5.00 USDT; startHere=true; auth=x402 bootstrap; available=true; guide=/docs#phone-flow; purpose=Provision one US AgentPhone number with a 30-day AgentOS entitlement - phone.number.ca.30d: POST /api/v1/phone/purchase-canada-number-30-days; price=5.00 USDT; startHere=true; auth=x402 bootstrap; available=true; guide=/docs#phone-flow; purpose=Provision one Canadian AgentPhone number with a 30-day AgentOS entitlement - phone.number.renew.30d: POST /api/v1/phone/renew-number-30-days; price=5.00 USDT; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#number-renewal; purpose=Extend one active owned phone-number entitlement by 30 days - phone.call.outbound.1m: POST /api/v1/phone/call-1-minute; price=0.30 USDT; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#phone-flow; purpose=Start one real outbound AgentPhone call authorized for up to 60 connected seconds - phone.call.outbound.5m: POST /api/v1/phone/call-5-minutes; price=1.50 USDT; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#phone-flow; purpose=Start one real outbound AgentPhone call authorized for up to 300 connected seconds - phone.call.extend.1m: POST /api/v1/phone/extend-call-1-minute; price=0.30 USDT; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#phone-flow; purpose=Extend one active owned call by exactly 60 seconds - phone.call.inbound.add.10m: POST /api/v1/phone/add-inbound-minutes-10; price=3.00 USDT; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#phone-flow; purpose=Add 600 inbound voice seconds to one active owned phone number - phone.number.release: POST /api/v1/phone/release-number; price=free; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#number-renewal; purpose=Irreversibly release one owned AgentPhone number - phone.number.list: GET /api/v1/phone/numbers; price=free; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#phone-flow; purpose=List owned numbers and entitlement state - phone.call.get: GET /api/v1/phone/calls/{callId}; price=free; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#phone-flow; purpose=Read one owned call - phone.call.transcript: GET /api/v1/phone/calls/{callId}/transcript; price=free; startHere=false; auth=Bearer at_v1; available=true; guide=/docs#phone-flow; purpose=Read the transcript for one owned call; recordings are never exposed - domain.register: POST /api/v1/domains/register; price=free; startHere=true; auth=x402 bootstrap; available=false; guide=/docs#domain-flow; purpose=Unavailable until fixed per-TLD prices and stable allowlisted registrar egress are configured ## Email Flow Create mailbox → save access token → send/list/read → connect WebSocket → handle email.received safe summary → fetch full message with emailId → POST /api/v1/events/ack. Create body: {"localPart":"research-agent","displayName":"Research Agent"} Send body: {"mailboxId":"uuid","to":["recipient@example.com"],"subject":"Subject","text":"Body"} ## Phone Flow Buy number → save token/phoneNumberId/callback secret → accept signed live voice callbacks → return dynamic text/hangup → buy outbound call packages or inbound allowance → monitor phone.number.expiring → renew for 5.00 USDT / 30 days → release safely when no longer needed. Buy body: {"agentName":"support-agent","agentWebhookUrl":"https://agent.example.com/voice","areaCode":"415"} Call body: {"phoneNumberId":"uuid","toNumber":"+14155550123"} Renew body: {"phoneNumberId":"uuid"} Release body: {"phoneNumberId":"uuid","confirmRelease":true} Recordings are never enabled or exposed. Use transcripts. ## Domain Flow Unavailable today. The real adapter requires fixed per-TLD services and static allowlisted Namecheap egress. available=false means do not pay and do not register the service on OKX. ## Event Delivery WebSocket gives immediate delivery; Supabase v1_events is the durable inbox. Authenticate the socket with session.authenticate; wait for session.ready; process event.delivery in order; send event.ack after durable local handling. On reconnect the gateway replays unacknowledged non-expired events. REST recovery: POST /api/v1/events/list, /get, /ack, /ack-all. Email events contain IDs/from/subject/time, not body or attachments. Supported events are documented in /docs; AgentOS internal events identify their source. ## Errors and Recovery - ONBOARDING_REQUIRED: call error.startHere; do not pay the failed request. - AUTH_REQUIRED or INVALID_TOKEN: use the correct at_v1 token. - PAYMENT_REQUIRED: quote, confirm, pay, replay unchanged. - PAYMENT_PENDING: do not pay twice. - IDEMPOTENCY_CONFLICT: retry the original body/key or start a new logical operation. - RESOURCE_NOT_FOUND / RESOURCE_NOT_OWNED: refresh owned IDs; never guess tenant IDs. - PROVIDER_TEMPORARY_FAILURE: retry unchanged with backoff. - ALLOWANCE_EXHAUSTED: buy inbound allowance. - NUMBER_EXPIRING: renew now. - NUMBER_RELEASED: buy a new number. ## Idempotency Send Idempotency-Key on every paid mutation. Reuse it only with the identical method, endpoint, body, and payment proof after an ambiguous response. ## Fixed Pricing Public prices come only from the AgentOS service catalog and do not change automatically with provider pricing. ## Complete example 1. GET /api/v1/services/email.mailbox.create. 2. POST the mailbox body; receive 402. 3. Quote/pay with explicit approval and replay with PAYMENT-SIGNATURE plus Idempotency-Key. 4. Save authentication.accessToken. 5. GET /api/v1/events/realtime-token with Authorization: Bearer at_v1_... 6. Connect to websocketUrl and send {"type":"session.authenticate","token":"..."}. 7. On email.received, GET /api/v1/email/messages/query?messageId=... with the bearer token. 8. POST /api/v1/events/ack with {"eventId":"..."}.