# Identity, MCP, login & agent-assisted signup

> Level-3 reference for [skill.md](../skill.md). Fetch: `https://cai.com/skill-references/identity-mcp-openclaw.md`

**Rules:** One personal verification email → one @cai.com account. **`verification_email` must not be an `@cai.com` address** — use a personal inbox (Gmail, Outlook, etc.) so the OTP is delivered outside CAI. After signup, **password** must be set on **https://cai.com/app** (not in chat). **API keys** for OpenClaw: Dashboard → generate key → `openclaw secrets set` (not in chat).  
**Registration HTTP errors:** On any **non-2xx** from `request-signup-verification`, `confirm-registration-code`, or `create-account`, parse the JSON **`error`** field when present and **tell the user that message** (do not answer with only “failed” or a generic error).  
**Tool / approval gateways:** Some agent hosts block outbound HTTP until the user approves a **per-request** id. If the call never reaches CAI (e.g. unknown or expired approval id), explain that as a **host approval** issue and ask the user to approve the **current** pending request right away — do not attribute that failure to CAI’s API.  
**Human eval / registration scenarios** (`R-*` / `L-*` case IDs, matrix stub): [docs/Goal_register_workflow.md](../docs/Goal_register_workflow.md).

### MCP hosts: connecting OpenClaw (or Cursor / Claude Desktop) to CAI

When the user asks **how to connect OpenClaw to my CAI account**, **use CAI from my AI assistant**, **set up MCP for cai.com**, or similar:

1. **Account:** They need an **@cai.com** account. If they are not registered, use §1 *Registration workflow* / *Agent-assisted signup*. If they have an account but the agent has no CAI session, mint **`POST /create-hosted-action`** with **`{ "action_type": "login" }`** and return the response **`url`** so they enter their password **only** on cai.com (§1 *Login assistance*, §8) — **never** collect passwords in chat.
2. **User API key:** On **https://cai.com** go to **Dashboard → Account → API Keys → Generate.** The plaintext key is shown **once** (prefix **`cai_...`**). **Do not** ask them to paste it in chat.
3. **Host secrets:** They store the key **only** on their machine or host — e.g. **`openclaw secrets set CAI_API_KEY "cai_..."`** (OpenClaw), or **`CAI_API_KEY`** inside the MCP server **`env`** (Cursor / Claude / OpenClaw). **`crt_...` registration tickets are not API keys** — **never** use them as `CAI_API_KEY` (see *Agent-assisted signup*).
4. **MCP server:** Register the official **`@cailab/mcp`** stdio server with **`CAI_SUPABASE_URL`**, **`CAI_SUPABASE_ANON_KEY`**, and **`CAI_API_KEY`** (see repo **`packages/cai-mcp/README.md`**). Confirm the host loaded it (e.g. **`openclaw mcp list`**). Staging vs production URL/key pairing: same runbooks as **`packages/cai-mcp/README.md`**.
5. **Skill file:** Point the host at **`https://cai.com/skill.md`** (or a **staging** skill URL during internal QA per **docs/agent/OPENCLAW_INTERNAL_TEST_RUNBOOK.md**). CAI exposes the **same** HTTP tools to all agents; there is **no** separate OpenClaw-only API.
6. **How to phrase the answer:** Use **numbered steps** in plain text. For shell one-liners, prefer **inline code** (single backticks around the full command) so the reply cannot end with a **stray or unclosed** triple-backtick fence.
7. **Mail tools (§5):** Use a CAI API key with scope **`mail`** or **`full`**, or **`CAI_USER_JWT`**. If the host only has a **`read`** (or **`pay`** / **`platform`**) key, say honestly that §5 mailbox tools are **not** available — user opens **Mail** on cai.com or pastes **short OTPs** in chat.

### Login assistance (existing accounts)

When the user asks to **log in / sign in to CAI.com or cai.com** — including **“please login to my cai.com”**, **“please login to cai.com”**, **“help me login”**, **“can you login cai.com for me”**, or **“generate a login link”** — **you are expected to help** using this API **in the same turn**, not to refuse or to ask what they mean first.

**Equivalence:** There is **no difference** between “log me in” and “give me a login link”; both require an immediate `POST /create-hosted-action` with `{ "action_type": "login" }` and the **`url` in your reply**.

1. **What you do:** Call `POST /create-hosted-action` (never GET — **405** if GET) with JSON `{ "action_type": "login" }` at the Base URL, with `Authorization: Bearer` + `apikey` as in §9. If the user names an account (e.g. “login link for **bernard@cai.com**”), include `"local_part": "bernard"` in the same JSON (normalize to the part before `@cai.com`). If that account exists, the link is bound like an API-key-created link (pre-filled, readonly). If not, the page still pre-fills that hint for the user to correct. Anon JWT works for a generic link; user’s CAI API key also pre-binds when the host supplies it.
2. **What you send the user:** The `url` field from the JSON response — they open it in a browser and **type their own password on CAI’s page**. You never receive or relay their password.
3. **What not to say:** Do not claim you “cannot access external websites” or “cannot authenticate on behalf of the user” **for this flow** — minting the hosted login link *is* the supported integration.
4. **After the link (GOAL workflow):** In the **same message**, after the URL, you may add **one short line** (keep it light): e.g. **Want the assistant to remember you next time?** After you finish logging in on cai.com, you can generate an **API key** there (Dashboard / Account → API Keys). **Do not** ask them to paste the key in chat. If they want help, invite them to say so **after** login — you can then guide them to create a key and store it **securely in their AI agent host** (OpenClaw secrets, env, etc., depending on their setup). **There is no CAI API tool** that stores or accepts a user’s long-lived API key from chat; you only **guide** the user and rely on their host’s normal secret mechanism. Do **not** block the link on “do you have an API key?”; the link works without one.

Full parameters and `fetch_url`-style notes: §8 *Hosted Action Links*.

### Registration workflow (Web + Agent)

1. `POST /request-signup-verification` — JSON `{ "display_name", "verification_email", "desired_local_part"?, "locale"? }`. Optional **`desired_local_part`**: the user’s chosen @cai.com local part (5–32 chars, `a-z`, `0-9`, `.`, `-`); web UI sends the value from the address field. Validated with `check_alias_availability` before email is sent. Optional **`locale`**: `en` | `zh-Hant` | `zh-Hans` (matches web UI; email subject/body use this language; defaults to `en` or `Accept-Language`). Returns `{ ok, signup_token }`. Sends a **registration link** (24 h) to the email. **`verification_email` must not be `@cai.com`** (400). Same personal email cannot register twice (409). **503** if email is not configured; **503** if sending fails after validation (no orphan session).
2. `POST /confirm-registration-code` — JSON `{ "signup_token", "code" }` **or** `{ "verification_email", "code" }` (for agents when user only shares email + code). Returns `{ ok, registration_ticket }` (≈30 min TTL).
3. `GET /check-availability?local_part=` — optional before step 4.
4. `POST /create-account` — JSON `{ "local_part", "password", "registration_ticket" }` (user completes on **cai.com** in browser). Returns `{ ok, user_id, email }`.

| Tool | Description | Parameters | Endpoint |
|------|-------------|------------|----------|
| `request_signup_verification` | Start signup; email verification link. | `display_name`, `verification_email`, optional `desired_local_part`, optional `locale` | `POST /request-signup-verification` |
| `confirm_registration_code` | Verify OTP; get ticket. | `signup_token`+`code` or `verification_email`+`code` | `POST /confirm-registration-code` |
| `check_alias` | Check @cai.com local_part available. | `local_part` | `GET /check-availability` |
| `create_cai_account` | Final step (browser). | `local_part`, `password`, `registration_ticket` | `POST /create-account` |
| `get_identity` | Current user (requires Bearer). Response includes **`linked_wallets`** (metadata: `chain`, `address`, `scope`, `source`) for WalletConnect-linked subwallets when present. | — | `GET /get-identity` |
| `create_login_link` | Mint a **Hosted action URL** for CAI login; JSON response field is `url`. User opens it in a browser and enters password on CAI only. Pre-fill via API key, `user_id`, or `local_part` (see §8). *Alias (L3):* `create-hosted-action` + `action_type: "login"`. | `user_id?`, `local_part?` | `POST /create-hosted-action` with `action_type: "login"` |
| `create_deposit_link` | Mint a **Hosted action URL** for custodial deposit (addresses / QR; optional card checkout embedded on-page). Response `url`. *Alias:* `create-hosted-action` + `action_type: "deposit"`. | `user_id?`, `constraints?`: optional `chain` (single-chain view), `payment_method` (`crypto` \| `fiat`; skips the “crypto vs card” step), `crypto_asset` (UI hint e.g. `USDT_ERC20`, `USDC_ERC20`, `ETH_NATIVE`), `default_currency_code` (card receive preset, e.g. `usdt_eth`) | `POST /create-hosted-action` with `action_type: "deposit"` |
| `create_walletconnect_link` | Mint a **Hosted action URL** for WalletConnect. **Transfer flow:** requires `walletconnect_uri`. **Link subwallet (read-only):** `constraints.purpose` = `link_subwallet`, `constraints.chain` (e.g. `polygon`), optional `walletconnect_uri`; user completes on `/act/…` and address is stored in `user_linked_wallets`. *Alias:* `create-hosted-action` + `action_type: "walletconnect"`. | `user_id?` (required for `link_subwallet`), `walletconnect_uri?`, `chain?`, `purpose?` | `POST /create-hosted-action` with `action_type: "walletconnect"` |
| `create_human_verification_link` | Mint a **Hosted action URL** for **human-in-the-loop** steps (e.g. third-party **CAPTCHA** per [GOAL_website_workflow.md](../docs/GOAL_website_workflow.md) **W8**). Shows instructions + optional target URL; user completes verification elsewhere, then confirms on the page. *Alias:* `create-hosted-action` + `action_type: "human_verification"`. | `user_id?`; `constraints` must include **`target_url`** (https URL to open) and/or **`instructions`** (short text). | `POST /create-hosted-action` with `action_type: "human_verification"` |
| `create_wechat_bind_link` | Mint a **Hosted action URL** to **link the user’s WeChat** (Open Platform website app OAuth) to the current CAI account. User opens `/act/…` → **`GET /wechat-oauth-start?ha_token=…`**. Multiple WeChat identities can link to one email account (`user_wechat_links`). *Alias:* `create-hosted-action` + `action_type: "wechat_bind"`. | `user_id?` (required unless Bearer resolves user) | `POST /create-hosted-action` with `action_type: "wechat_bind"` **Currently not operational** — returns `503` + `GAP_WECHAT_*` until WeChat merchant is configured. |
| `wechat_pay_prepare` | **`POST /wechat-pay-prepare`** — WeChat Pay **JSAPI** prepay; returns **`url`** (open in **WeChat**), **`payment_intent_id`**. Use **`GET /payment-intent-status`** to poll. Requires **`pay`/`full`** and server-side WeChat merchant config. **Currently not operational** — returns `503` + `GAP_WECHAT_*` until WeChat merchant is configured. | JSON: **`amount_cny_fen`** (integer, CNY fen), optional `description`, `payer_openid` | `POST /wechat-pay-prepare` |
| `create_website_login_link` | **Legacy / optional** third-party browser handoff (`action_type: "website_login"`). **/act** UI: copyable @cai.com, dual checkboxes, **new-tab-first** when catalog/embed requires; optional **`constraints.registration_run_id`** (must match JWT user) triggers **`registration_run_user_ack`** on submit. Does **not** capture cookies or vault tokens. **F-16:** **`platform_one_click_register`** first. Requires `constraints.target_url` (`https://`); optional `embed_mode`, `intent`, `title`, `resolved_email`, `hitl_official_urls`. | `user_id?`, `constraints` | `POST /create-hosted-action` with `action_type: "website_login"` |

### Agent-assisted signup (OpenClaw / chat UIs)

**Do not** default to “go fill the form yourself” **before** attempting HTTP to CAI. Registration steps 1–3 are **Edge functions on the Base URL**; they do **not** require the user’s CAI API key. If your runtime says it “cannot call external APIs,” that is a **host configuration** issue (enable outbound `fetch` / web tools to `https://api.cai.com`, or inject the project **anon** key the same way the public **https://cai.com/app** page does). Only fall back to fully manual signup if the HTTP call fails with a clear error.

**Recommended sequence** when the user asks to register a specific address (e.g. `agent@cai.com`) with display name and a personal verification email:

1. Normalize **local part**: strip to the part before `@` (e.g. `agent`); reject invalid characters (server will also sanitize).
2. `GET /check-availability?local_part=agent` — if `available` is not `true`, stop and explain (taken, reserved, etc.).
3. `POST /request-signup-verification` with `{ "display_name", "verification_email", "desired_local_part"?, "locale"? }` (include **`desired_local_part`** when the user chose a specific @cai.com address; pass UI language when known). Persist **`signup_token`** from the response for the next step; tell the user the verification link was sent to their inbox.
4. When the user pastes the **6-digit code**, `POST /confirm-registration-code` with `{ "signup_token", "code" }` **or** `{ "verification_email", "code" }`. Persist **`registration_ticket`** (`crt_...`, short TTL).
5. **Never ask for the account password in chat.** After step 4, send the user to CAI’s app page **only** to set the password and call `create-account` from the browser:

   `https://cai.com/app?registration_ticket=<registration_ticket>&local_part=<local_part>`

   Treat `registration_ticket` like a secret (single-use flow, limited lifetime); the `/app` page strips these query params after load — do not paste tickets into public logs. **Always share this deep link with `/app`, not `/app.html`**, so query parameters are not lost on redirect.

**After step 4 — same message as the link (required copy points):**

- Ask the user to open the link **within the ticket lifetime** (~30 minutes) and **set their password on cai.com only**. The `crt_...` value is a **registration ticket**, **not** a CAI User API key. **Do not** tell them to run `openclaw secrets set` with `crt_...` or treat it as `CAI_API_KEY`.
- **After** they finish creating the account (password submitted in the browser), if they want OpenClaw (or any agent host) to call balance / identity tools: they must **log in to cai.com → Dashboard → Account → API Keys → generate** a key (prefix **`cai_...`**). They should store it **only** in the host’s secret store, e.g. `openclaw secrets set CAI_API_KEY "cai_..."` in **their terminal** — **never** paste `cai_...` in chat.
- If the user asks for **balance** or other Bearer-authenticated tools **right after** OTP verification but **before** they confirm they finished the browser step: explain that the account is not complete until **`create-account`** runs in the browser; send the deep link again if needed, **then** guide Dashboard API key + host secrets.

**Optional browser OTP step:** `https://cai.com/app?signup_token=<signup_token>` opens the code-entry step if the user prefers typing the OTP on the site instead of in chat.

**Rate limits:** `429` from step 3 may mean too many signups from the IP or too many pending verifications — advise waiting or trying another network; do not tight-loop retries.

### Registration complete signals (OpenClaw / chat)

When the user says registration is **done**, **successful**, or shares a **`cai_...`** User API key (even once in chat):

1. **Stop** the signup OTP / `crt_` / “check your inbox for activation” flow — that path is **finished** for agent integration purposes.
2. **Treat `cai_` as agent-ready:** the user completed Dashboard (or equivalent) API key issuance. Proceed to §1 *MCP hosts* steps 3–5 (`openclaw secrets set`, `@cailab/mcp`, **`https://cai.com/skill.md`** or staging skill URL).
3. If your host can call CAI with that Bearer token, **`GET /get-identity`** in the **same turn** and confirm `@cai.com` / `user_id` — do **not** say “no session” when the key works.
4. **One-time paste in chat** is acceptable only to confirm setup; guide the user to store the key in **host secrets** and **not** paste it again. If the key was exposed in a public channel, advise **revoke + regenerate** on **Account → API Keys**.
5. **Do not** block OpenClaw/MCP setup with “set a web password first”, “click the activation link”, or “API key is not for web login” unless the user **explicitly** asks to sign in to the **website** in a browser (then use *Login assistance* / `create-hosted-action` login — separate from agent API access).
