Skip to main content
POST
CLI Init
The public unauthenticated endpoint that powers the @axon402/init onboarding helper. 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.
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 route. The durable @axon402/cli (axon-op) consumes those keys.
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

No Authorization header. The endpoint is intentionally anonymous.

Required Fields

Optional Fields

Example Request

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)

Fields

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

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

429 RATE_LIMITED shape

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:
  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