> ## Documentation Index
> Fetch the complete documentation index at: https://docs.axon402.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# CLI Init

> One-shot org bootstrap + agent wallet + starter API key from a single email + client-derived addresses

The public unauthenticated endpoint that powers the [`@axon402/init` onboarding helper](https://docs.axon402.com/docs/cli/installation). One round-trip creates a Supabase auth user, bootstraps a browser-mode org + vault wallet + agent wallet + starter mandate, and mints a **wallet-scoped starter API key**.

> [!NOTE]
>
> This endpoint is **one-shot per email** — it is the first-run bootstrap, not a general key-issuance API. For ongoing operator keys (org-wide, rotatable, scoped), use the authenticated [POST /v1/api-keys](https://docs.axon402.com/docs/api-reference/organizations/create-api-key.md) route. The durable [`@axon402/cli`](https://docs.axon402.com/docs/cli/installation#step-2-the-operator-cli) (`axon-op`) consumes those keys.

> [!WARNING]
>
> This is the **only public unauthenticated endpoint** on the Decision API. It is registered before the global rate limiter and before `requireAuth`. Per-IP DB-backed rate limiting (10 requests/hour/IP) is the real protection — there is no token gate.

## Request

```http theme={null}
POST /v1/cli/init
Content-Type: application/json
```

No `Authorization` header. The endpoint is intentionally anonymous.

### Required Fields

| Field | Type | Description |
| :- | :- | :- |
| `email` | string | Valid email address, ≤ 254 chars. Becomes the Supabase auth user. Receives an invitation/confirmation email immediately. |
| `vault_address` | string | Ethereum address derived at `m/44'/60'/0'/0/0`. The deposit target — USDC sent here is what the agent spends from. |
| `root_address` | string | Same as `vault_address`. Browser-mode invariant: vault and root come from the same key, so they must match. The endpoint rejects mismatches. |
| `agent_address` | string | Ethereum address derived at `m/44'/60'/0'/0/N` where `N >= 1`. The spend source — the API key returned by this endpoint is bound to this wallet. **Must differ from `vault_address`.** |
| `agent_derived_path` | string | The HD path used to derive `agent_address`, e.g. `m/44'/60'/0'/0/1`. The server validates the format and the index. |

### Optional Fields

| Field | Type | Description |
| :- | :- | :- |
| `org_name` | string | Display name for the organization, ≤ 64 chars. Defaults to the local part of the email (the part before `@`), truncated to 64 chars. |

### Example Request

```bash theme={null}
curl -X POST https://api.axon402.com/v1/cli/init \
  -H "Content-Type: application/json" \
  -d '{
    "email": "team@example.com",
    "vault_address": "0x1234567890123456789012345678901234567890",
    "root_address":  "0x1234567890123456789012345678901234567890",
    "agent_address": "0xabcdef0123456789abcdef0123456789abcdef01",
    "agent_derived_path": "m/44'\''/60'\''/0'\''/0/1",
    "org_name": "Example Team"
  }'
```

> [!WARNING]
>
> The two addresses must come from a wallet you control. Never paste random addresses — the resulting API key would mint payments from a wallet you don't have the mnemonic for. The CLI does the derivation client-side from a fresh BIP-39 mnemonic; if you're calling this endpoint by hand, do the same.

## Response

### Success (201 Created)

```json theme={null}
{
  "user_id": "uuid",
  "organization_id": "uuid",
  "wallet_id": "uuid (the agent wallet, the spend source — what the api_key is bound to)",
  "agent_address": "0x…",
  "vault_wallet_id": "uuid (the deposit target)",
  "api_key": "axon_sk_… (returned EXACTLY ONCE)",
  "funding_address": "0x… (the vault address, where USDC is sent)",
  "email_confirmation_sent": true,
  "dashboard_url": "https://axon402.com/",
  "trace_id": "echo of X-AXON-REQUEST-ID, optional"
}
```

### Fields

| Field | Type | Description |
| :- | :- | :- |
| `user_id` | string | Supabase auth user UUID |
| `organization_id` | string | Newly created organization UUID |
| `wallet_id` | string | The **agent** wallet UUID (HD index ≥ 1). This is the spend source. |
| `agent_address` | string | The agent's Ethereum address (echoed from the request for client-side verification) |
| `vault_wallet_id` | string | The vault wallet UUID (HD index 0) |
| `api_key` | string | A `axon_sk_*` token, returned **exactly once**. Store it immediately or you'll have to mint a new one from the dashboard. |
| `funding_address` | string | The vault Ethereum address — where USDC should be deposited to top up the agent |
| `email_confirmation_sent` | boolean | Always `true` on success. Set if `inviteUserByEmail` succeeded. |
| `dashboard_url` | string | The hosted dashboard URL, currently `https://axon402.com/` |
| `trace_id` | string \| undefined | Optional. Echoes the `X-AXON-REQUEST-ID` header from the request, useful for support debugging. |

> [!WARNING]
>
> **The plaintext `api_key` is returned exactly once.** It is not stored anywhere the user can retrieve it later — only the SHA-256 / HMAC hash lives in the database. The CLI persists the plaintext to `~/.axon/config.json` (chmod 0600); a hand-rolled caller must do the same or accept that the key is lost.

### Error Responses

| Status | Code | Cause |
| :- | :- | :- |
| `400` | `VALIDATION_ERROR` | Email format invalid, address format invalid, vault ≠ root mismatch, agent == vault, HD path malformed or index 0, org\_name > 64 chars |
| `409` | `CONFLICT` | An auth user with this email already exists. Response includes `dashboard_url` + `login_hint`. |
| `429` | `RATE_LIMITED` | More than 10 requests from this IP in the last hour. Response includes `details.retry_after_seconds` and a `Retry-After` header. |
| `500` | `INTERNAL_ERROR` | Supabase auth admin call failed, or the bootstrap helper threw. The auth user is best-effort rolled back. `recoverable: true` for most cases. |

#### 400 VALIDATION\_ERROR examples

The exact `message` field varies by which check fired:

* `email is required and must be a valid address ≤254 chars`
* `vault_address must be a valid Ethereum address`
* `root_address must be a valid Ethereum address`
* `vault_address must equal root_address in browser mode`
* `agent_address must be a valid Ethereum address`
* `agent_derived_path must match m/44'/60'/0'/0/N where N >= 1`
* `agent_address must differ from vault_address (vault is index 0; agents start at index 1)`
* `org_name must be ≤64 chars`

#### 409 CONFLICT shape

```json theme={null}
{
  "code": "CONFLICT",
  "message": "An account already exists for this email.",
  "details": {
    "dashboard_url": "https://axon402.com/",
    "login_hint": "Sign in to the dashboard to retrieve or rotate your API key."
  },
  "recoverable": false
}
```

#### 429 RATE\_LIMITED shape

```json theme={null}
{
  "code": "RATE_LIMITED",
  "message": "Too many CLI init requests from this IP. Try again in 3421s.",
  "recoverable": true,
  "details": {
    "retry_after_seconds": 3421
  }
}
```

The response also sets `Retry-After: 3421` as an HTTP header, per RFC 7231.

## Behavior

When the request is valid and within the rate limit, the endpoint performs the following side effects in order:

1. **Per-IP rate limit check**. Distributed via the Postgres `rate_limits` table so it survives Cloud Run horizontal scaling. 10 events per hour per `req.ip` (Cloud Run trust-proxy hop is stripped, so `req.ip` is the real client).

2. **Supabase auth user creation**. Calls `auth.admin.inviteUserByEmail(email, { data: { cli_origin: true, signup_source: 'cli_init_v1' } })`. This creates the user as unconfirmed AND dispatches a confirmation email using the Supabase project's Site URL + email template. The `dashboard_url` returned in the response is informational — the actual link in the email is generated server-side by Supabase GoTrue.

3. **Org + vault bootstrap**. Calls the shared `bootstrapBrowserOrgForUser` helper (the same one the dashboard uses for browser-mode signups) with the user-provided `vault_address`. Creates the organization, the vault wallet at HD index 0, and the owner membership.

4. **Agent wallet + mandate creation**. Calls `createAgent` with the `wallet_override` branch so the server skips its own derivation (impossible in browser signing mode — there's no server-side mnemonic). Creates the agent wallet at the user-provided `agent_derived_path` index, with a starter mandate:

   | Limit | Default |
   | - | - |
   | Daily | 10 USDC |
   | Per transaction | 0.5 USDC |
   | Monthly | 300 USDC |
   | Networks | `eip155:8453` (Base mainnet) |
   | Enforcement | `hard_stop` |
   | Purpose | "CLI-bootstrapped agent (default starter limits)" |

5. **API key minting**. Generates a `axon_sk_*` wallet-scoped key bound to the **agent** wallet (not the vault). Inserts the HMAC-SHA256 hash into the `api_keys` table; the plaintext is returned exactly once. The key has scopes `["read", "write"]`.

6. **Auth user rollback on failure**. If steps 3, 4, or 5 throw, the endpoint best-effort calls `auth.admin.deleteUser(userId)` to undo step 2. Otherwise every helper failure would leak an orphaned `auth.users` row that locks the email out of future signups.

## Security notes

* **The mnemonic is generated client-side and never sent to the server.** Only the derived addresses are POSTed. This is the browser-mode invariant, and it's load-bearing: in production the server has no master mnemonic and cannot derive addresses on its own.
* **The endpoint is rate-limited by IP, not by token**, because there is no token. Spam protection is a single line of defense — the `rate_limits` Postgres table — and `S-2` in the audit backlog tracks tightening this further.
* **Email enumeration is possible**: the 409 vs 201 distinction tells an attacker whether a given email is registered. The audit (`S-3`) tracks this trade-off; we accepted it because flattening to a generic 200 would break the synchronous-key UX the CLI depends on.
* **HMAC-SHA256 with server-side pepper** is used for storing API key hashes. A database leak alone is not enough to brute-force or rainbow-table the keys; the attacker also needs the pepper, which lives in Cloud Run's secret store.

## See also

* [`@axon402/init` CLI Reference](https://docs.axon402.com/docs/cli/reference) — the canonical caller of this endpoint
* [`@axon402/cli`](https://docs.axon402.com/docs/cli/installation#step-2-the-operator-cli) — the durable operator CLI for everything that comes after the first wallet
* [POST /v1/api-keys](https://docs.axon402.com/docs/api-reference/organizations/create-api-key.md) — authenticated route for minting org-wide tenant keys
* [Bootstrap Organization](https://docs.axon402.com/docs/api-reference/organizations/bootstrap-org.md) — the dashboard's authenticated equivalent (same helper underneath)
* [Error Codes](https://docs.axon402.com/docs/api-reference/error-codes.md) — full reference for `code` field values


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.