# OtoCo MCP Server & REST API v1

OtoCo forms and manages on-chain legal entities (Delaware/Wyoming Series LLCs, standalone Delaware/Wyoming LLCs, Swiss Associations, Wyoming UNA/DUNA) on Ethereum (`main`), Polygon (`polygon`), Base (`base`), Sepolia (`sepolia`), and Base Sepolia (`basesepolia`). Network params accept canonical keys or short aliases (`eth`, `pol`, `sep`, `basesep`). Marshall Islands is not offered. This document describes the machine interface: a Streamable HTTP MCP server and a versioned REST API that share one capability registry. Use it to discover products, manage entities you own, and (with admin keys) operate admin workflows.

## Quick start

**Endpoints**

| Environment | MCP (Streamable HTTP) | REST base |
|---|---|---|
| Production | `https://api.otoco.io/mcp` | `https://api.otoco.io/v1` |
| Staging | `https://api-staging.otoco.io/mcp` | `https://api-staging.otoco.io/v1` |

> **Current live MCP host:** Use **staging** (`https://api-staging.otoco.io/mcp`) while production `api.otoco.io` is not yet serving `/mcp`. Production URLs above are the target once rollout completes.

MCP tool names are capability names with dots replaced by underscores (`entities.list` → `entities_list`). Only `POST /mcp` is supported; `GET`/`DELETE` return 405 (clients that open a GET SSE stream should treat 405 as “no standalone stream,” per Streamable HTTP). Stateless mode: no `Mcp-Session-Id` is issued or required.

**MCP client config (public tools only — no key)**

```json
{
  "mcpServers": {
    "otoco": {
      "url": "https://api-staging.otoco.io/mcp"
    }
  }
}
```

**MCP client config (user or admin tools — with key)**

```json
{
  "mcpServers": {
    "otoco": {
      "url": "https://api-staging.otoco.io/mcp",
      "headers": {
        "Authorization": "Bearer otoco_sk_<keyId>_<secret>"
      }
    }
  }
}
```

**What the Bearer unlocks**

| Key kind in `Authorization` | MCP tools registered |
|---|---|
| *(none)* | Public only |
| *(invalid / expired / malformed Authorization)* | HTTP 401 JSON-RPC error — does **not** fall back to public tools (fix or omit the header) |
| `kind: "user"` | Public + user tools (per key scopes) |
| `kind: "admin"` | Public + user + admin tools (requires a live enabled admin account; admin scopes intersect with your roles) |

Call `user_me` to introspect the current Bearer: `keyKind` is `"user"`, `"admin"`, or `null`; `mcpAdminToolsUnlocked` is `true` only when admin MCP tools are actually registered for this token. `isAdmin` means your account is in the `admins` collection — **it does not by itself unlock admin MCP tools** (an admin account using a `kind: "user"` key still gets user tools only).

**REST example**

```bash
curl -s https://api.otoco.io/v1/public/jurisdictions

curl -s -H "Authorization: Bearer otoco_sk_..." \
  https://api.otoco.io/v1/entities
```

## Authentication & keys

- Format: `otoco_sk_<keyId>_<secret>` (also accept the raw token without `Bearer`).
- Header: `Authorization: Bearer otoco_sk_...`
- No `Authorization` header → public tools only. If the user needs Swiss Association / Wyoming UNA (DUNA) or other user-scoped tools, tell them to authenticate by adding `Authorization: Bearer otoco_sk_...` (create a key at the developer page below) and reconnect.
- Present but invalid/expired/malformed `Authorization` → MCP returns HTTP 401 (`Invalid API key`) and does **not** register public tools for that request. Cursor/MCP clients will show discovery errors until the header is fixed or removed.
- Responses intentionally omit `WWW-Authenticate` so API-key clients are not pushed into OAuth discovery probes.
- `kind: "user"` Bearer → public + user tools (scopes control which user tools).
- `kind: "admin"` Bearer → public + user + admin tools (requires a live enabled admin record; admin scopes intersect with your roles).
- Create, rotate, and revoke keys at [https://otoco.io/dashpanel/account/developer](https://otoco.io/dashpanel/account/developer). The full token is shown once at creation.

**`user_me` fields (introspect the current Bearer)**

| Field | Meaning |
|---|---|
| `keyKind` | `"user"`, `"admin"`, or `null` — the kind of API key in the `Authorization` header |
| `mcpAdminToolsUnlocked` | `true` only when this Bearer actually registers admin MCP tools (`kind: "admin"` + live admin) |
| `isAdmin` | Account is in the `admins` collection; **alone it does not unlock admin MCP tools** |

### Scopes

Public capabilities need no scope. User scopes apply to `kind: "user"` keys. Admin scopes require `kind: "admin"` and a live enabled admin record; scopes are intersected with the member’s current roles.

| Scope | Kind | Grants |
|---|---|---|
| *(none)* | — | Public discovery tools |
| `entities:read` | user | List/get owned entities |
| `entities:write` | user | Entity formation checkout links |
| `documents:read` | user | List document definitions; generate temp download links |
| `files:read` | user | List entity files; temp download links |
| `files:write` | user | Delete entity files |
| `payments:read` | user | List payments and subscriptions |
| `payments:write` | user | Create payment/checkout links; cancel subscriptions |
| `finance:read` | user | Balances, transactions, notes |
| `finance:write` | user | Add/remove transaction notes |
| `points:read` | user | Points balance and history |
| `forms:read` | user | Available forms and submissions |
| `forms:write` | user | Submit entity forms |
| `banking:read` | user | EIN / Mercury / Meow / virtual mailing status |
| `banking:write` | user | Submit EIN; create Mercury/Meow applications; reset rejected Meow |
| `user:read` | user | Profile (`/me`), list own API keys |
| `user:write` | user | Revoke own API keys (confirmation-gated) |
| `admin:orders:read` | admin | Standalone orders, notes, stats, payment failures (roles: superadmin, filer, billing, support) |
| `admin:orders:write` | admin | Update/mint orders; add order notes (update/mint: superadmin, filer; notes: all admin roles) |
| `admin:entities:read` | admin | Admin entity view, file/document temp links (all admin roles) |
| `admin:content:write` | admin | Feed, broadcast, products refresh, resources (all admin roles) |
| `admin:admins:read` | admin | List admins (superadmin) |
| `admin:admins:write` | admin | Add/update/remove admins (superadmin) |
| `admin:audit:read` | admin | Admin audit log and API usage (superadmin) |
| `admin:keys:read` | admin | List all API keys (superadmin, support) |
| `admin:keys:write` | admin | Revoke any API key (superadmin) |

### Security properties

- Secret hashed at rest (SHA-256); compared with `timingSafeEqual`. Full token never stored or returned after creation.
- Shown once at creation; revocable; optional expiry enforced at auth time.
- Optional IP allowlist (`allowedIps`) — exact match.
- Max 10 active user keys per member; max 3 admin keys.
- Admin powers re-checked live against the `admins` collection on every request.

### Rate limits

| Caller | Limit | On exceed |
|---|---|---|
| No key (per IP) | 30 req/min | `429` + `Retry-After` |
| User key | 120 req/min per key | `429` + `Retry-After` |
| Admin key | 300 req/min per key | `429` + `Retry-After` |
| Auth failures (per IP) | 20 req/min | stricter bucket to slow brute force |

## Tool catalog

Parameter types below match the Zod input schemas. Path params and body/query fields are merged flat for both REST and MCP. Networks: canonical `main` \| `polygon` \| `base` \| `sepolia` \| `basesepolia` (aliases `eth`, `pol`, `sep`, `basesep` accepted). Payment `service` is Stripe-only (`stripe`; defaults to `stripe` if omitted). Radon is not supported — Stripe Checkout covers both fiat and crypto. Entity `id` is a non-negative integer (token id). Prices in previews and public catalog come from the live product catalog — never hardcode dollar amounts.

### Public (9) — no auth

| MCP tool | Scope | REST | Parameters | Description |
|---|---|---|---|---|
| `public_name_check` | — | `POST /v1/public/name-check` | `name` string **req**; `jurisdiction` string opt | Check entity name availability on **production networks** (`main`, `polygon`, `base`); testnet-only collisions (`sepolia`, `basesepolia`) are ignored. Without jurisdiction checks all supported (DELAWARE, WYOMING, SWISSASSOCIATION, UNADUNA — Marshall Islands excluded). |
| `public_products_list` | — | `GET /v1/public/products` | *(none)* | Live catalog products and pricing — strips payment-provider ids; **excludes Standalone Master LLC**. Prefer `public_checkout_options.formationPaths` for Instant vs Standalone. |
| `public_entity_count` | — | `GET /v1/public/entity-count` | *(none)* | Entity counts by network and jurisdiction. |
| `public_entity_lookup` | — | `GET /v1/public/entities/:network/:id` | `network` string **req**; `id` number **req** | Public entity fields only (no members/manager). |
| `public_legal_templates` | — | `GET /v1/public/legal-templates` | *(none)* | Legal template filenames available for public download. |
| `public_getting_started` | — | `GET /v1/public/getting-started` | *(none)* | Signup guide plus Instant vs Standalone `formationPaths` with live prices. |
| `public_jurisdictions` | — | `GET /v1/public/jurisdictions` | *(none)* | Jurisdiction metadata: suffix, networks, `instantSupported`, `standaloneSupported`. |
| `public_checkout_options` | — | `GET /v1/public/checkout-options` | *(none)* | **Instant vs Standalone vs Swiss/DUNA** `formationPaths` with live prices, plus STANDALONE email-checkout allowlist (Delaware/Wyoming Standalone LLCs, `startup-box`, add-ons). `formationPaths.swissDuna` is API-key-only. Never lists Standalone Master LLC. |
| `public_entity_checkout` | — | `POST /v1/public/entity-checkout` | `email` string **req**; `jurisdiction` string **req** (`DELAWARE`\|`WYOMING`; `SWISSASSOCIATION`/`UNADUNA` rejected with auth instructions); `entityName` string opt; `bundle` string opt (`"startup-box"`); `addOns` string[] opt; `discountCode` string opt | **Standalone only** (not Instant). Payment link emailed. Swiss/DUNA not supported — authenticate and use `entities_creation_checkout`. |
| `public_instant_checkout` | — | `POST /v1/public/instant-checkout` | `email` string **req**; `jurisdiction` string **req** (`DELAWARE`\|`WYOMING`; `SWISSASSOCIATION`/`UNADUNA` rejected with auth instructions); `entityName` string **req** (final at purchase); `bundle` string opt (`"series-box"`) | **Instant Series LLC** email checkout (DE/WY only) — **no API key**. Default = Annual Renewal only. With `bundle:"series-box"` = Series in a Box (Instant + Tax ID + Virtual Mailing + Mercury). Swiss/DUNA not supported here — require `Authorization: Bearer otoco_sk_...` and `entities_creation_checkout`. |

### User (37) — API key + scope

| MCP tool | Scope | REST | Parameters | Description |
|---|---|---|---|---|
| `entities_list` | `entities:read` | `GET /v1/entities` | `hideTestnets` boolean\|`"true"`\|`"false"` opt; `limit` number opt (max 500); `offset` number opt | List entities accessible to the current user. With `limit` or `offset`, returns `{items, total, limit, offset, hasMore}`; otherwise a plain array. |
| `entities_get` | `entities:read` | `GET /v1/entities/:network/:id` | `network` **req**; `id` **req** | Full member-view entity record. |
| `entities_creation_checkout` | `entities:write` | `POST /v1/entities/checkout` | `service` `"stripe"` (defaults to stripe); `network` opt (app default); `jurisdiction` **req** (`DELAWARE`\|`WYOMING`\|`SWISSASSOCIATION`\|`UNADUNA`); `name` string **req**; `email` string **req for Swiss/DUNA** (must match API-key account); `members` string[] (Ethereum addresses; min 1 for DE/WY, **min 2** for Swiss/DUNA; one must be linked) **req**; `redirectURL` string opt; `useSavedCard` boolean opt; `confirm` boolean opt | **Instant** formation (on-chain under OtoCo's master — not Standalone). **Only** path for Swiss Association / Wyoming UNA (DUNA). Confirmation-gated checkout/direct payment. |
| `entities_series_box_checkout` | `entities:write` | `POST /v1/entities/series-box-checkout` | `jurisdiction` `"DELAWARE"`\|`"WYOMING"` **req**; `network` **req**; `entityName` string (min 3) **req**; `walletAddress` address string **req** (wallet that will own the entity; must be a wallet the user controls); `redirectURL` string opt; `useSavedCard` boolean opt; `confirm` boolean opt | Series in a Box — Instant Series bundle (instant on-chain LLC + Tax ID + Virtual Mailing + Mercury + annual renewal). Confirmation-gated; entity mints after payment. |
| `entities_byoe_checkout` | `entities:write` | `POST /v1/entities/byoe-checkout` | `jurisdiction` `"DELAWARELLC"`\|`"WYOMINGLLC"` **req**; `network` **req**; `addOns` string[] opt; `walletAddress` address string opt (wallet that will own the entity; must be a wallet the user controls); `redirectURL` string opt; `useSavedCard` boolean opt; `confirm` boolean opt | Bring Your Own Entity — attach an existing state-registered Standalone LLC to OtoCo at the current catalog price. Confirmation-gated checkout/direct payment. |
| `documents_list` | `documents:read` | `GET /v1/documents/available` | *(none)* | List available generated document definitions. |
| `documents_download_link` | `documents:read` | `POST /v1/documents/:code/:network/:id` | `code` string **req**; `network` **req**; `id` **req**; `account` string opt | Generate document; return temporary presigned download link. |
| `files_list` | `files:read` | `GET /v1/files/:network/:id` | `network` **req**; `id` **req** | List entity files from the bucket path. |
| `files_download_link` | `files:read` | `POST /v1/files/:network/:id/link` | `network` **req**; `id` **req**; `filename` string **req** | Temporary presigned link for an entity file. |
| `files_delete` | `files:write` | `DELETE /v1/files/:network/:id` | `network` **req**; `id` **req**; `filename` string **req** | Delete a file from an entity bucket path. |
| `payments_list` | `payments:read` | `GET /v1/payments/:service` | `service` `"stripe"` (defaults to stripe) | List Stripe payments for the user. |
| `payments_entity_list` | `payments:read` | `GET /v1/payments/entity/:network/:id` | `network` **req**; `id` **req** | List payments for a specific entity. |
| `payments_create_link` | `payments:write` | `POST /v1/payments/:service/:network/:id` | `service` `"stripe"` (defaults to stripe); `network` **req**; `id` **req**; `products` string[] (min 1) **req**; `redirectURL` string opt; `useSavedCard` boolean opt; `confirm` boolean opt | Create Stripe checkout/direct payment for products on an entity (fiat or crypto chosen in Stripe Checkout). Confirmation-gated; `useSavedCard` charges only after `confirm:true`. |
| `subscriptions_list` | `payments:read` | `GET /v1/subscriptions/:service` | `service` `"stripe"` (defaults to stripe) | List Stripe subscriptions for the user. |
| `subscriptions_entity_list` | `payments:read` | `GET /v1/subscriptions/entity/:network/:id` | `network` **req**; `id` **req** | List subscriptions attached to an entity. |
| `subscriptions_cancel` | `payments:write` | `DELETE /v1/subscriptions/:service/:id` | `service` `"stripe"` (defaults to stripe); `id` string **req**; `confirm` boolean opt | Cancel a Stripe subscription by subscription id. Confirmation-gated. |
| `finance_balances` | `finance:read` | `GET /v1/finance/balances/:network/:id/:account` | `network` **req**; `id` **req**; `account` address string **req** | Wallet balances for an entity-linked account. |
| `finance_transactions` | `finance:read` | `GET /v1/finance/transactions/:network/:id/:account` | `network` **req**; `id` **req**; `account` address string **req** | Wallet transactions for an entity-linked account. |
| `finance_notes_list` | `finance:read` | `GET /v1/finance/notes/:network/:id` | `network` **req**; `id` **req** | List notes attached to an entity. |
| `finance_notes_add` | `finance:write` | `POST /v1/finance/notes/:network/:id` | `network` **req**; `id` **req**; `hash` string **req**; `message` string **req** | Add or upsert a transaction note. |
| `finance_notes_remove` | `finance:write` | `DELETE /v1/finance/notes/:network/:id/:hash` | `network` **req**; `id` **req**; `hash` string **req** | Remove an entity transaction note by hash. |
| `points_balance` | `points:read` | `GET /v1/points` | *(none)* | Points balances and trigger metadata for the current user. |
| `points_history` | `points:read` | `GET /v1/points/history` | *(none)* | Points earning history for the current user. |
| `forms_available` | `forms:read` | `GET /v1/forms/:network/:id` | `network` **req**; `id` **req** | Available forms for an entity (payment/submission status). |
| `forms_submissions` | `forms:read` | `GET /v1/forms/submissions/:network/:id` | `network` **req**; `id` **req**; `limit` number\|string opt; `offset` number\|string opt | Form submissions for an entity. |
| `forms_submit` | `forms:write` | `POST /v1/forms/:formId/submit/:network/:id` | `formId` string **req**; `network` **req**; `id` **req**; `answers` object (string keys → unknown values) **req** | Submit a form for an entity; form must be available for the entity (see `forms_available`). |
| `banking_ein_status` | `banking:read` | `GET /v1/banking/ein/:network/:id` | `network` **req**; `id` **req** | EIN status and related files for an entity. |
| `banking_mercury_status` | `banking:read` | `GET /v1/banking/mercury/:network/:id` | `network` **req**; `id` **req** | Mercury application status. |
| `banking_meow_status` | `banking:read` | `GET /v1/banking/meow/:network/:id` | `network` **req**; `id` **req** | Meow application status. |
| `banking_virtual_mailing` | `banking:read` | `GET /v1/banking/virtual-mailing/:network/:id` | `network` **req**; `id` **req** | Virtual mailing subscription status; the resolved address is returned **only** when the subscription is enabled/active (never for lapsed or never-purchased). |
| `banking_ein_submit` | `banking:write` | `POST /v1/banking/ein/:network/:id` | `network` **req**; `id` **req**; `userFullName` string **req**; `entityFullName` string **req**; `SSN` string opt; `street` string opt; `city` string opt; `state` string opt; `country` string opt; `zipcode` string opt; `confirm` boolean opt | Generate and store the EIN submission form PDF. Confirmation-gated. Requires EIN purchased first (`payments_create_link`). Address fields optional when the entity has an active Virtual Mailing subscription; otherwise required. |
| `banking_mercury_apply` | `banking:write` | `POST /v1/banking/mercury/:network/:id` | `network` **req**; `id` **req**; `paymentService` `"stripe"` opt (legacy; ignored — purchases use Stripe); `redirectURL` string opt; `confirm` boolean opt | Create a Mercury bank account application. Confirmation-gated. Requires Mercury Bank Application purchased first (`payments_create_link`). Returns Mercury signup link and application metadata when paid. |
| `banking_meow_apply` | `banking:write` | `POST /v1/banking/meow/:network/:id` | `network` **req**; `id` **req**; `userEmail` string opt; `confirm` boolean opt | Create a Meow Partner Onboarding application; returns hosted onboarding URL. Confirmation-gated. Optional `userEmail` overrides the key owner's Privy email. |
| `banking_meow_reset` | `banking:write` | `DELETE /v1/banking/meow/:network/:id` | `network` **req**; `id` **req**; `confirm` boolean opt | Reset a rejected Meow application so a new one can be created. Confirmation-gated. Only when status is `REJECTED`. |
| `user_me` | `user:read` | `GET /v1/me` | *(none)* | Sanitized profile: DID, wallets, entity count, `isAdmin`, `keyKind`, `mcpAdminToolsUnlocked`, `keyScopes`. |
| `user_keys_list` | `user:read` | `GET /v1/me/api-keys` | *(none)* | List API keys owned by the current user (no secrets). |
| `user_keys_revoke` | `user:write` | `DELETE /v1/me/api-keys/:keyId` | `keyId` string **req**; `confirm` boolean opt | Revoke one of the current user’s API keys. Confirmation-gated. |

### Admin (23) — admin key + scope + live admin roles

| MCP tool | Scope | REST | Parameters | Description |
|---|---|---|---|---|
| `admin_orders_list` | `admin:orders:read` | `GET /v1/admin/orders` | `status` string opt (comma-separated: `paid`,`filing`,`filed`,`minted`); `jurisdiction` string opt; `network` string opt; `formCompleted` string\|boolean opt; `orderType` `"standalone"`\|`"byoe"` opt; `q` string opt; `since` string opt; `until` string opt; `limit` number\|string opt; `cursor` string opt | List standalone orders with admin filters. |
| `admin_orders_get` | `admin:orders:read` | `GET /v1/admin/orders/:id` | `id` string **req** | Get one standalone order by id. |
| `admin_orders_update` | `admin:orders:write` | `POST /v1/admin/orders/:id` | `id` string **req**; `entityName` string opt; `status` `"paid"`\|`"filing"`\|`"filed"`\|`"minted"` opt; `formCompleted` boolean opt | Update order status/name/form completion. |
| `admin_orders_mint` | `admin:orders:write` | `POST /v1/admin/orders/:id/mint` | `id` string **req** | Mint standalone order on-chain; async post-mint finalize. |
| `admin_orders_notes_list` | `admin:orders:read` | `GET /v1/admin/orders/:id/notes` | `id` string **req** | List internal notes for a standalone order. |
| `admin_orders_notes_add` | `admin:orders:write` | `POST /v1/admin/orders/:id/notes` | `id` string **req**; `body` string **req** | Add a note to a standalone order. |
| `admin_orders_stats` | `admin:orders:read` | `GET /v1/admin/orders-stats` | *(none)* | Standalone order aggregate statistics. |
| `admin_payment_failures_list` | `admin:orders:read` | `GET /v1/admin/payment-failures` | `status` string opt; `network` string opt; `q` string opt; `since` string opt; `until` string opt; `cursor` string opt; `limit` number\|string opt | List recorded payment failures. |
| `admin_entities_get` | `admin:entities:read` | `GET /v1/admin/entities/:network/:id` | `network` **req**; `id` **req** | Entity plus bucket file listing. |
| `admin_entities_file_link` | `admin:entities:read` | `POST /v1/admin/entities/:network/:id/file-link` | `network` **req**; `id` **req**; `filename` string **req** | Temporary link for an entity file. |
| `admin_entities_document_link` | `admin:entities:read` | `POST /v1/admin/entities/:network/:id/documents/:code` | `network` **req**; `id` **req**; `code` string **req**; `account` string opt | Generate document; return presigned temp download link. |
| `admin_feed_update` | `admin:content:write` | `POST /v1/admin/feed` | `feed` string **req**; `timestamp` number **req** | Update dashboard feed content. |
| `admin_broadcast_send` | `admin:content:write` | `POST /v1/admin/broadcast` | `title` string **req**; `message` string **req**; `link` string opt; `filter` object **req** (`members` string opt; `jurisdiction` string opt; `network` string opt; `verified` boolean opt) | Broadcast email to filtered entity members. |
| `admin_products_refresh` | `admin:content:write` | `POST /v1/admin/products/refresh` | *(none)* | Refresh marketplace product catalog caches. |
| `admin_resources_list` | `admin:content:write` | `GET /v1/admin/resources` | *(none)* | List resource assets and lead counts. |
| `admin_admins_list` | `admin:admins:read` | `GET /v1/admin/admins` | *(none)* | List admin records. |
| `admin_admins_add` | `admin:admins:write` | `POST /v1/admin/admins` | `did` string **req**; `roles` (`"superadmin"`\|`"filer"`\|`"billing"`\|`"support"`)[] min 1 **req**; `email` string opt; `name` string opt | Create a new admin record. |
| `admin_admins_update_roles` | `admin:admins:write` | `POST /v1/admin/admins/:did/roles` | `did` string **req**; `roles` (`"superadmin"`\|`"filer"`\|`"billing"`\|`"support"`)[] min 1 **req** | Update role assignments for an admin DID. |
| `admin_admins_remove` | `admin:admins:write` | `DELETE /v1/admin/admins/:did` | `did` string **req** | Remove an admin record by DID. |
| `admin_audit_list` | `admin:audit:read` | `GET /v1/admin/audit` | `action` string opt; `adminDid` string opt; `targetType` string opt; `targetId` string opt; `since` string opt; `until` string opt; `limit` number\|string opt; `cursor` string opt | List admin audit log entries. |
| `admin_keys_list` | `admin:keys:read` | `GET /v1/admin/api-keys` | `member` string opt; `kind` string opt (`user`\|`admin`); `revoked` string\|boolean opt; `limit` number\|string opt; `offset` number\|string opt | List all API keys with filters/pagination. |
| `admin_keys_revoke` | `admin:keys:write` | `DELETE /v1/admin/api-keys/:keyId` | `keyId` string **req** | Revoke any API key by key id. |
| `admin_api_usage_list` | `admin:audit:read` | `GET /v1/admin/api-usage` | `keyId` string opt; `member` string opt; `capability` string opt; `from` string opt; `to` string opt; `limit` number\|string opt; `offset` number\|string opt | API usage audit entries (REST and MCP). |

**Registry totals:** 9 public + 37 user + 23 admin = **69** tools.

## Response formats & errors

### REST

Success:

```json
{ "ok": true, "data": { } }
```

Error:

```json
{ "ok": false, "error": "..." }
```

| Status | Meaning |
|---|---|
| `400` | Invalid input (Zod flatten object in `error`) |
| `401` | Invalid API key (uniform message; no oracle) |
| `403` | Missing scope, forbidden, or not authorized for entity (`Missing scope: X`, permission errors) |
| `404` | Not found |
| `429` | Rate limit exceeded — honor `Retry-After` |
| `500` | Internal error (no stack traces) |

### MCP

- Success: `content: [{ type: "text", text: JSON.stringify(result, null, 2) }]`
- Failure: `isError: true` with a safe text message (no Stripe/Mongo internals)
- Tools annotated with `readOnlyHint` from the capability `readOnly` flag
- **No Authorization header** → public tools only
- **Invalid/expired Authorization** → JSON-RPC unauthorized error (does **not** silently fall back to public tools)

## Usage guidance for agents

1. Start with public tools (`public_jurisdictions`, `public_products_list`, `public_name_check`, `public_getting_started`, `public_checkout_options`) for discovery — no key required.
2. Entity-scoped user calls need `network` + `id` (token id). The key’s owner must have permission on that entity.
3. File and document tools return **presigned temporary links** — download promptly; binaries are never streamed through MCP.
4. User-tier payment tools (`entities_creation_checkout`, `entities_series_box_checkout`, `entities_byoe_checkout`, `payments_create_link`) **can** charge a saved card one-click (`useSavedCard: true`), but **only** after a confirmation preview **and** an explicit human yes in a later message — see [Confirmation & spending safety](#confirmation--spending-safety). Without `confirm: true` they return a preview (no side effect). Without `useSavedCard`, they create a checkout/payment URL for a human to complete. Never confirm in the same turn as the preview.
5. Destructive / consequential tools (`subscriptions_cancel`, `files_delete`, `forms_submit`, `user_keys_revoke`, `banking_ein_submit`, `banking_mercury_apply`, `banking_meow_apply`, `banking_meow_reset`) are likewise confirmation-gated. EIN/Mercury/Meow submissions still require prior purchase (Meow may be opted out of payment via env on staging only).
6. Respect scopes: REST `403` with `Missing scope: …` means the key owner must grant that scope (or use a key that already has it). On MCP, tools outside the key’s scopes are simply not registered. Key revoke requires `user:write`.
7. Do not present Marshall Islands as available. Supported formation jurisdictions: Delaware, Wyoming (series and standalone LLCs), Swiss Association, Wyoming UNA/DUNA.

### Confirmation & spending safety

Money-moving and destructive user tools accept an optional `confirm` boolean. The **first call never charges or mutates** — it returns a preview. A second call with `confirm: true` executes.

**Agent rule (mandatory):** Real users say things like “create a Wyoming company called alphabeat.” They will **not** say “ask me first,” “use my saved card,” or “wait for confirmation.” After every confirmation preview you **must**:

1. Show the preview `summary` to the user in plain English (what they get, the price, which card if charging).
2. Ask a clear yes/no question.
3. **Stop** and wait for their explicit yes in a **later message**.
4. Only then call again with `confirm: true` (plus `paymentAttemptId` / `confirmationToken` for money-moving tools).

Never set `confirm: true` in the same turn as the preview. Never treat the original request as approval.

**Money-moving tools** (`entities_*_checkout`, `payments_create_link`) additionally bind preview → confirm:

1. Preview returns `details.paymentAttemptId` (UUID) and `details.confirmationToken` (short-lived HMAC).
2. Confirm **must** pass both back with the **same purchase arguments**. A bare `confirm: true` without those fields is rejected.
3. Changing products/jurisdiction/amount/entity between preview and confirm invalidates the token.
4. Reusing a fresh random `paymentAttemptId` on confirm is rejected — this prevents unbound double-charges.

**Preview shape** (returned when `confirm` is omitted or `false` — **no side effects**):

```json
{
  "requiresConfirmation": true,
  "action": "charge_saved_card",
  "summary": "CAUTION: Your Visa card ending in 4242 will be charged $299.00 for Tax ID. This is a real one-click charge — it happens immediately and is not reversible from here.",
  "details": {
    "willCharge": true,
    "amountUsd": 299,
    "amountDisplay": "$299.00",
    "paymentAttemptId": "xxxxxxxx-xxxx-4xxx-xxxx-xxxxxxxxxxxx",
    "confirmationToken": "<hmac-token>"
  },
  "howToConfirm": "STOP — do not charge yet. Show this summary to the user verbatim in plain English and ask a clear yes/no... Wait for their explicit yes in a later message..."
}
```

`details.amountUsd` is included when catalog prices resolve; it may be `null` when the amount is unknown until checkout.

**Example flow (saved-card charge):**

1. User: “Create a Wyoming company called alphabeat.”
2. Agent calls `entities_creation_checkout` with `useSavedCard: true` (no `confirm`) → preview with `CAUTION:` summary, catalog-derived `amountUsd`, `paymentAttemptId`, and `confirmationToken`.
3. Agent replies to the user: “Ready to form alphabeat as a Wyoming series LLC on Sepolia for **$49** on your Visa ending in **4242**. Should I go ahead?”
4. User: “Yes.”
5. Agent calls again with the same args plus `confirm: true`, `paymentAttemptId`, and `confirmationToken` → card is charged.

Gated tools: `entities_creation_checkout`, `entities_series_box_checkout`, `entities_byoe_checkout`, `payments_create_link`, `subscriptions_cancel`, `files_delete`, `forms_submit`, `user_keys_revoke`, `banking_ein_submit`, `banking_mercury_apply`, `banking_meow_apply`, `banking_meow_reset`. Public `public_entity_checkout` is unchanged (email-gated; no saved-card one-click).

### Agent-driven formation

**Instant vs Standalone vs Swiss/DUNA (mandatory):** If the user says “create a company/entity/LLC” without specifying the path, call `public_checkout_options` (or `public_getting_started`), show **Instant**, **Standalone**, and **Swiss/DUNA** from `formationPaths`, and ask which they want **before** any checkout.

| Path | What it is | Price SKU (example) | Tool |
|------|------------|---------------------|------|
| **Instant** | On-chain Series LLC (DE/WY) under OtoCo’s master — faster/cheaper | `Annual Renewal (DE)` (e.g. $99) | `public_instant_checkout` (email + entity name, **no API key**) or `entities_creation_checkout` (API key + confirm) |
| **Instant + EIN/VMA** | Series in a Box (Instant + Tax ID + Virtual Mailing + **Mercury**) — DE/WY only | Series-in-a-Box catalog price | `public_instant_checkout` with `bundle:"series-box"` (no API key) or `entities_series_box_checkout` (API key) |
| **Standalone** | Independent state-filed LLC (DE/WY) | `Standalone LLC (DE)` (e.g. $299) | `public_entity_checkout` (email link) |
| **Swiss / UNA (DUNA)** | Swiss Association or Wyoming UNA/DUNA (on-chain Instant) | `Annual Renewal (Swiss Association)` / `Annual Renewal (UNA)` | **`entities_creation_checkout` only** — **API key required**; not public email checkout |

Never offer **Standalone Master LLC** ($499) — it is not an MCP formation product. “Series” / “instant” means Instant Series LLC, not Standalone Master.

**Instant email checkout (DE/WY):** After the user picks Instant, collect **email + entity name** (name is final at purchase), call `public_instant_checkout`. The Stripe link is emailed; after payment the Series LLC mints automatically. Do **not** ask the user for an API key for Instant DE/WY.

**Add EIN / virtual mailing to Instant:** Instant-only Annual Renewal cannot take those add-ons. Call `public_instant_checkout` again with the same email/name/jurisdiction and `bundle:"series-box"`. Disclose that Mercury is included and show the Series-in-a-Box price from `formationPaths.instant.bundle`. Prior unpaid Instant checkout sessions for that email+name+jurisdiction are expired when possible.

**Standalone email checkout:** Use `public_checkout_options` then `public_entity_checkout` for a non-registered buyer. **Do not require a company name from the user** — `email` + `jurisdiction` are enough. The buyer picks the company name in the formation form after payment. Optional `entityName` may be passed if the user already has one; when provided it is availability-checked as a courtesy, but the name is still finalized later in the form. The Stripe checkout link is **delivered by email** to the provided address and is never returned in the tool response — only the inbox owner can pay (anti-abuse). Tell the user to check their inbox for the secure payment link. Optional `discountCode` is validated against active Stripe promotion codes and pre-applied; when no code is passed, buyers can enter one themselves at checkout (a pre-applied code disables checkout entry). For standalone formations, add-ons are billed after formation rather than in the initial checkout amount; the emailed summary states this explicitly. Errors include name taken (only when `entityName` was provided), invalid discount code, and rate limited (max 3 emails/day per address). After payment, the entity appears in the dashboard for that email; OtoCo’s filing/minting pipeline handles the rest.

**Swiss Association / Wyoming UNA (DUNA) — authenticated only:**

1. If the MCP session is **not** authenticated (only public tools available), tell the user to authenticate by adding header `Authorization: Bearer otoco_sk_...`. Create a key at [https://otoco.io/dashpanel/account/developer](https://otoco.io/dashpanel/account/developer) under their OtoCo account, then reconnect. Do **not** attempt `public_instant_checkout` / `public_entity_checkout` for Swiss/DUNA (those tools return an explicit auth-required error).
2. Collect the **entity name** (user often leads with this).
3. Ask for **email** (must match the OtoCo account for that API key) **and at least 2 member wallet addresses** (one must be a wallet linked to the account).
4. Call `entities_creation_checkout` with `jurisdiction` `SWISSASSOCIATION` or `UNADUNA`, `name`, `email`, and `members` (≥2). Show the confirmation preview, wait for an explicit yes, then confirm.

**Instant (API key / saved card, DE/WY):** Optional advanced path — `entities_creation_checkout` with linked wallet among `members`, confirmation preview, then `confirm:true`.

## Server config (operators)

Money-moving MCP/REST tools bind preview → confirm with short-lived HMAC tokens. The Oracle host must set **`MCP_CONFIRMATION_TOKEN_SECRET`** (at least 32 characters). Staging and production fail closed if it is missing. Local development (`NODE_ENV=development`) may auto-generate an ephemeral secret per process when unset.

## Support / links

- Developers overview: [https://otoco.io/developers](https://otoco.io/developers)
- Docs: [https://docs.otoco.io](https://docs.otoco.io)
- API keys & MCP setup: [https://otoco.io/dashpanel/account/developer](https://otoco.io/dashpanel/account/developer)
- This file: [https://otoco.io/mcp.md](https://otoco.io/mcp.md)
