> For the complete documentation index, see [llms.txt](https://docs.robotmoney.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.robotmoney.com/api-reference/authentication.md).

# Authentication

RobotMoney authenticates humans, tenant-owned agents, tenant API keys, service principals, and MCP agents. The same bearer header is used at resource routes. Only the credential class and allowed scopes differ.

| Caller                | Mode                                                             | Credential                                    |
| --------------------- | ---------------------------------------------------------------- | --------------------------------------------- |
| **Human owner**       | Self-serve email and password, or a linked wallet                | Bearer owner JWT                              |
| **Tenant agent**      | Owner-created agent API key exchanged at `/token`                | Five-minute bearer JWT                        |
| **Tenant API key**    | Tenant API key in the sandbox integration environment, read-only | Bearer `brain_sk_...` key                     |
| **Service principal** | Operator or tenant service credential                            | Bearer service token                          |
| **MCP agent**         | SIWX over Base plus on-chain scope hash                          | Bearer JWT from the SIWX or MCP auth exchange |

Every credential is presented the same way: `Authorization: Bearer <token>`. There is one bearer mechanism, not several. The `brain_sk_test_...` / `brain_sk_live_...` value is a tenant API key when API-key authentication is enabled for that environment. See [Server API key](#server-api-key-brain_sk_) below.

{% hint style="warning" %}
At launch, first-class API-key authentication is enabled in the sandbox integration environment and is limited to read-only scopes. It is deliberately not enabled on the production API, where `brain_sk_` bearers fail closed as invalid keys. Production enablement remains a separate release gate after staging acceptance and auth-surface review; no public enablement date is committed. Production access continues to use the supported human, service, and agent credentials described on this page.
{% endhint %}

{% hint style="info" %}
Self-serve signup is gated by `BRAIN_SELF_SERVE_SIGNUP`. A new tenant starts in the sandbox flow with an active owner member, a default review-first policy, and headless decision limits that let an owner-created agent request `POST /v1/authorizations` decisions with `execution: "external"`. The decision API does not create a PaymentIntent and does not move money. Promotion to live money movement remains a separate controlled process.
{% endhint %}

{% hint style="warning" %}
The internal `POST /v1/auth/service-token` mint route is a break-glass sandbox/testnet BFF credential path. It uses a shared secret, is not per-user auth, and must stay disabled for live-money or multi-customer production.
{% endhint %}

### Server API key (`brain_sk_`)

The credential a server-side integration uses is a RobotMoney-issued tenant API key with a `brain_sk_` prefix. It authenticates directly as a bearer credential; there is no token exchange step.

| Property            | Value                                                                                                                       |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **Format**          | `brain_sk_test_…` (sandbox) / `brain_sk_live_…` (live)                                                                      |
| **Availability**    | Sandbox integration environment at launch; production API-key auth is deliberately disabled pending a separate release gate |
| **Issued by**       | Tenant-admin key routes when API-key auth is enabled for the environment                                                    |
| **Presented as**    | `Authorization: Bearer brain_sk_…`, or `new Brain({ apiKey: "brain_sk_…" })` in the SDK                                     |
| **Scopes**          | `ledger:read`, `audit:read`, `governance:read` only. API keys cannot ingest source data.                                    |
| **Sandbox vs live** | Distinct `brain_sk_test_…` and `brain_sk_live_…` key formats                                                                |
| **Lifetime**        | No automatic TTL at issuance. Revocation and rotation are supported; an optional `expires_at` is enforced when populated.   |

The plaintext secret is returned only when a key is issued or rotated. The database retains only a server-peppered SHA-256 digest. Rate limits, idempotency, last-used tracking, and audit attribution are keyed off the authenticated key.

### Human Owner Authentication

A developer self-provisions a sandbox tenant, verifies their email, then logs in for a short-lived **owner JWT**. The owner JWT is the setup credential for the headless path: it can create counterparties, create a tenant agent, manage headless limits, approve needs-review authorizations, void recorded authorizations, and read usage. It never carries `payment_intent:execute` or rail execution authority.

**1. Sign up**. Provisions a sandbox tenant + owner.

```http
POST /v1/signup
Content-Type: application/json

{ "email": "founder@example.com", "password": "a-strong-passphrase-12+" }

→ 201 { "tenant_id": "tnt_…", "user_id": "user_…", "status": "pending",
        "verification_token": "…" }   // returned outside production; emailed in prod
```

In production, the API emails the token through the configured ESP client (`EMAIL_ENDPOINT`, `EMAIL_API_KEY`, optional `EMAIL_FROM`). If `BRAIN_SELF_SERVE_SIGNUP` is enabled in production without ESP credentials, API boot fails before signup routes are served.

**2. Verify the email**. Single-use, short-TTL token, scoped to the tenant.

```http
POST /v1/auth/verify-email
{ "tenant_id": "tnt_…", "token": "<verification_token>" }

→ 200 { "verified": true, "user_id": "user_…", "status": "active" }
```

**3. Log in**. Email + password -> owner JWT.

```http
POST /v1/auth/login
{ "email": "founder@example.com", "password": "a-strong-passphrase-12+" }

→ 200 { "access_token": "eyJ…", "token_type": "Bearer", "expires_in": 900,
        "principal": { "type": "user", "tenantId": "tnt_…",
                       "scopes": ["ledger:read","wiki:read","raw:read","raw:write",
                                  "policy:read","policy:write","audit:read","execution:read",
                                  "payment_intent:approve","surfaces:admin"] } }
```

An unknown email and a wrong password return the **same** `401 auth_invalid_credentials` (no user enumeration); an unverified account returns `403 auth_email_unverified`.

```http
GET /v1/ledger/transactions
Authorization: Bearer <access_token>
```

### Tenant Agent API Key (`brain_ak_`)

A tenant agent API key is an exchange-only machine credential. It is returned once from `POST /v1/agents` as `api_key_once` and is never accepted directly at resource routes.

```http
POST /v1/agents
Authorization: Bearer <owner_jwt>
Content-Type: application/json

{
  "kind": "tenant",
  "name": "Ops Agent",
  "purpose": "Request payment authorization decisions",
  "autonomy_preset": "ask_always"
}
```

The response includes an `exchange` object with the token endpoint, resource, grant type, subject token type, and default scope. Use those values exactly.

```json
{
  "api_key_once": "brain_ak_test_...",
  "exchange": {
    "token_endpoint": "https://staging-auth.robotmoney.com/token",
    "resource": "https://staging-api.robotmoney.com/",
    "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
    "subject_token_type": "urn:brain:params:oauth:token-type:agent-api-key",
    "scope": "payment_intent:propose",
    "key_display": "once"
  }
}
```

Exchange the key at the token endpoint:

```http
POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=brain_ak_test_...
&subject_token_type=urn:brain:params:oauth:token-type:agent-api-key
&resource=https://staging-api.robotmoney.com/
&scope=payment_intent:propose
```

The response is a five-minute JWT with no refresh token. The agent exchanges the same key again when it needs a fresh JWT.

| Property            | Value                                                                |
| ------------------- | -------------------------------------------------------------------- |
| Prefix              | `brain_ak_test_...` in sandbox, `brain_ak_live_...` for live tenants |
| Resource route use  | Not accepted directly                                                |
| Token TTL           | 300 seconds                                                          |
| Headless scope      | `payment_intent:propose`                                             |
| First headless call | `POST /v1/authorizations` with `execution: "external"`               |
| Forbidden scope     | `payment_intent:execute`                                             |

The headless launch path also uses owner-session scopes:

| Credential          | Headless use                                                                                        |
| ------------------- | --------------------------------------------------------------------------------------------------- |
| Owner JWT           | Signup completion, counterparty setup, agent creation, limit management, approve, void, usage reads |
| Agent exchanged JWT | `POST /v1/authorizations` only for propose-time decisions                                           |
| Tenant API key      | Commercial read API only                                                                            |

### Wallet Authentication (SIWX). MCP agents and humans

MCP agents and humans who **link a wallet** authenticate with **Sign-In With X** (EIP-4361 over Base). This is separate from the tenant agent API key path above. An owner can link a wallet to their tenant:

```http
POST /v1/tenants/{tenant_id}/wallets        (owner JWT)
{ "address": "0x…", "principal_type": "human" }   // or "agent" + principal_id
```

At sign-in, SIWX resolves the wallet: one linked to a **human** mints an **owner JWT** with the same management scopes as email login. An **MCP agent** wallet registered and active in `BrainMCPAgentRegistry` mints an MCP agent token.

#### Step 1: Construct the SIWX Message

```
robotmoney.com wants you to sign in with your Ethereum account:
0xAgentAddress

URI: https://api.robotmoney.com
Version: 1
Chain ID: 8453
Nonce: <server-issued nonce>
Issued At: 2025-09-01T12:00:00Z
Expiration Time: 2025-09-01T12:05:00Z
```

A nonce is obtained from `POST /v1/auth/siwx/challenge` (Redis-held, 5-minute TTL).

#### Step 2: Sign with the Identity Key

The agent (or human's linked wallet) signs the message with the key registered in `BrainMCPAgentRegistry` / linked via `wallet_identities`.

#### Step 3: Exchange for a Token

```http
POST /v1/auth/siwx
Content-Type: application/json

{ "message": "...", "signature": "0x...", "session_id": "..." }

→ {
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "principal": { "type": "agent", "tenantId": "tnt_…", "scopes": ["ledger:read", "payment_intent:propose"] }
}
```

#### Step 4: Use the Token

```http
POST /v1/agents/mcp
Authorization: Bearer <access_token>
```

{% hint style="info" %}
The MCP auth chain verifies that the agent record is `active` and that its stored `scope_hash` matches the hash registered in `BrainMCPAgentRegistry`. Agents can read, contribute evidence, and **propose**. Never **execute**. There is no execute tool, and every settlement passes the section 6 gate.
{% endhint %}

### Agent Scope Hash

The current scope mechanism is a hash commitment, not an EIP-712 scope-attestation object. RobotMoney sorts an agent's plain scope strings lexicographically, joins them with `|`, and calculates `keccak256` over that value. The resulting `scope_hash` must match the agent's registered on-chain hash. It has no per-grant amount, resource, time-window, or nonce fields.

### Token Lifetimes

| Token                                                    | Default TTL      | Refreshable                           |
| -------------------------------------------------------- | ---------------- | ------------------------------------- |
| **Owner JWT** (email/wallet)                             | 15 minutes       | Yes. Log in / re-sign again           |
| **Tenant agent JWT** (`brain_ak_` exchange)              | 5 minutes        | Exchange the agent key again          |
| **MCP agent token (SIWX)**                               | 1 hour           | Yes, by re-signing SIWX               |
| **Server API key** (`brain_sk_`, sandbox only at launch) | No automatic TTL | Rotated or revoked by tenant admin    |
| **Service-token mint** (`/v1/auth/service-token`)        | 1 hour           | Re-mint (break-glass sandbox/testnet) |
| **Email-verification token**                             | 24 hours         | No, single-use                        |
| **Policy verdict**                                       | 60 seconds       | No, single-use                        |

Owner JWTs include `surfaces:admin` so a tenant admin can connect or revoke Slack, Teams, and email approval surfaces. Surface onboarding endpoints derive the RobotMoney tenant from this bearer principal, not from request bodies.

### Agent Control

| Operation   | Endpoint                                 | Behavior                                                                                                                                                                                  |
| ----------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Halt**    | `POST /v1/agents/{agent_id}/halt`        | Requires `payment_intent:approve`. Pauses approved in-flight payment intents from the agent and quarantines its local agent record.                                                       |
| **Restore** | `POST /v1/agents/{agent_id}/restore`     | Requires `payment_intent:approve`. Restores a quarantined local agent record to `active`. Returns 404 when the agent is not registered and 409 unless its current state is `quarantined`. |
| **Token**   | Re-authenticate or rotate the credential | Tokens are short-lived by design and expire after their configured lifetime.                                                                                                              |

### What's Next

<table data-view="cards"><thead><tr><th></th><th></th><th data-type="content-ref"></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>🌐 API Overview</strong></td><td>Endpoints, versioning, rate limits.</td><td><a href="/api-reference/overview.md">Overview</a></td><td></td></tr><tr><td><strong>📜 BrainMCPAgentRegistry</strong></td><td>The on-chain agent registry.</td><td><a href="/smart-contracts/brainmcpagentregistry.md">BrainMCPAgentRegistry</a></td><td></td></tr></tbody></table>
