# Welcome to Brain 🧠

AI that runs finance for you. Brain is the financial intelligence layer for businesses, transforming financial activity into memory, intelligence, and autonomous execution.

You point Brain at a business's existing financial sources (banks, ERP, invoicing tools, on-chain wallets) and you get back a continuously updated, policy-aware record that humans and autonomous software can both read, reason over, and act on safely.

Brain holds neither funds nor rail access. It sits between an account holder and their financial world as the structured intelligence layer: ingest, normalize, remember, govern, execute, prove.

## How Brain Is Organized

Brain is a layered protocol; information flows up and control flows down.

<table><thead><tr><th width="200">Layer</th><th>Job</th></tr></thead><tbody><tr><td>Raw</td><td>Lossless ingestion from any authorized source</td></tr><tr><td>Ledger</td><td>Deterministic normalization into immutable financial truth</td></tr><tr><td>Wiki</td><td>Continuously updated memory and natural-language Q and A</td></tr><tr><td>Policy</td><td>Plain-English rules compiled to deterministic guards</td></tr><tr><td>Agent</td><td>Internal and external agents proposing actions in scope</td></tr><tr><td>Audit</td><td>Per-tenant Merkle tree anchored on Base L2</td></tr></tbody></table>

Reads are grounded in evidence, writes emit audit events, and any financial action has to pass a deterministic pre-execution gate before it leaves the system. Nothing executes outside that gate.

## What Brain Is Not

Brain is not a bank, a custodian, an accounting tool, an agent marketplace, or a generic assistant. Funds and custody belong to the account holder. Brain reads, reasons, governs, and proves; it does not own the assets it operates on.

## A First Integration

```typescript
import { Brain } from "@brainfinance/sdk";

const brain = new Brain({ apiKey: process.env.BRAIN_API_KEY!, environment: "sandbox" });

// Ask a grounded question about a tenant's money.
const answer = await brain.ask("acme", "What's our cash position right now?");

// Propose a payment. Brain runs policy and the pre-execution gate before any settlement.
const action = await brain.pay("acme", { invoiceId: "inv_8231" });

// Get a verifiable record of what just happened.
const proof = await brain.proof(action.intent.id!);
```

That covers most of what a typical integration touches; no on-chain knowledge is required to use any of it.

> **Staging / controlled pilot.** Autonomous execution and on-chain proof are wired end-to-end, but Brain runs on **Base Sepolia** behind smart contracts **pending an external audit**; a `brain_sk_live_` key uses the same code path yet does not move real money on mainnet today. See the [Readiness Summary](/architecture/readiness-summary).

<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>Quickstart</strong></td><td>Five minutes from <code>npm install</code> to a working integration.</td><td><a href="/pages/PbRFcMj0jEeGN8bAgAQD">/pages/PbRFcMj0jEeGN8bAgAQD</a></td><td></td></tr><tr><td><strong>Build</strong></td><td>Task-shaped guides. The patterns most apps need in their first hour.</td><td><a href="/pages/BJk23JjE7PeUfpXzaGcs">/pages/BJk23JjE7PeUfpXzaGcs</a></td><td></td></tr><tr><td><strong>Concepts</strong></td><td>The mental model. Memory, policy, agents, proof.</td><td><a href="/pages/5dhCMyKXDue9NEBWqOTf">/pages/5dhCMyKXDue9NEBWqOTf</a></td><td></td></tr><tr><td><strong>Protocol</strong></td><td>The deep stack. Six layers, smart contracts, on-chain anchoring.</td><td><a href="/pages/m9CZyvHRkykhnfA4mZcY">/pages/m9CZyvHRkykhnfA4mZcY</a></td><td></td></tr></tbody></table>

## Who Brain Is For

| You're building                    | You use Brain to                                                                 |
| ---------------------------------- | -------------------------------------------------------------------------------- |
| A B2B finance product              | Skip the integrations layer. Read normalized financial truth across every source |
| An autonomous agent for a business | Give it grounded memory, scoped permissions, and a provable audit trail          |
| A fintech embedding into an ERP    | Ship policy-gated actions on top of existing customer data, no schema rewrites   |
| A treasury or operations dashboard | Query the tenant's full money picture in natural language or structured calls    |
| An external agent marketplace      | Plug into Brain's MCP surface. Same primitives, same audit semantics             |

## What You Can Build

| In an afternoon               | What it looks like                                                                  |
| ----------------------------- | ----------------------------------------------------------------------------------- |
| A finance copilot             | Ask natural-language questions about a tenant's money; get answers with citations   |
| A spending agent              | Let an autonomous agent pay invoices under a limit; anything bigger goes to a human |
| An ops dashboard              | Read transactions, balances, obligations, and counterparties from one feed          |
| An external agent integration | Plug into any MCP-compatible runtime; full read and propose surface                 |
| A compliance trail            | Every read, every decision, every action, exportable as a tamper-evident log        |

## What Brain Handles for You

| You don't write                          | Because Brain handles                                                        |
| ---------------------------------------- | ---------------------------------------------------------------------------- |
| Bank, ERP, and on-chain integrations     | Source ingestion across Plaid, NetSuite, Alchemy, Stripe, and more           |
| A memory layer for your agent            | A continuously updated record per tenant, queryable in natural language      |
| A permissioning DSL                      | Plain-English policies compiled to deterministic rules, signed by the tenant |
| A safe execution path                    | A deterministic gate that checks every payment before it leaves              |
| An audit trail your customers can verify | A Merkle-anchored history on Base L2                                         |

## Integration Surfaces

Brain exposes four surfaces. Pick whichever matches your stack. They share the same data, the same policy, and the same audit log.

| Surface         | Best for                                                  | Reference                                    |
| --------------- | --------------------------------------------------------- | -------------------------------------------- |
| TypeScript SDK  | Web apps, agent runtimes, internal tools                  | [SDK quickstart](/introduction/quickstart)   |
| HTTP API        | Any language, server-side integrations, custom workflows  | [API reference](/api-reference/overview)     |
| MCP server      | Third-party agents over the Model Context Protocol        | [MCP server](/mcp-server/overview)           |
| Smart contracts | On-chain settlement, programmable accounts, scope attests | [Smart contracts](/smart-contracts/overview) |

## A First Request

```bash
npm install @brainfinance/sdk
```

```typescript
import { Brain } from "@brainfinance/sdk";

const brain = new Brain({ apiKey: process.env.BRAIN_API_KEY!, environment: "sandbox" });

const accounts = await brain.accounts.list({ limit: 10 });
console.log(accounts.accounts);
```

The full SDK surface (`brain.accounts`, `brain.transactions`, `brain.payments`, `brain.audit`, `brain.policy`, `brain.agents`, `brain.wiki`, and more) is documented in the [API reference](/api-reference/overview). For raw HTTP access, see [Authentication](/api-reference/authentication).

## Next Steps

* Build it: [Quickstart](/introduction/quickstart), then the [task-shaped guides](/build/overview).
* Understand it: [Concepts](/concepts/overview), then [Protocol](/protocol/overview).
* Integrate an external agent: [MCP server](/mcp-server/overview).
* Verify a payment on-chain: [Smart contracts](/smart-contracts/overview) and [BrainAuditAnchor](/smart-contracts/brainauditanchor).
* Review the safety model: [SECURITY.md](https://github.com/braindotfi/brain-core/tree/main/SECURITY.md). The §6 gate, layer boundaries, audit verification, and threat model.


# Quickstart

Five minutes from npm install to a working integration.

By the end of this page, you'll have a working integration that reads a tenant's financial state in natural language, proposes a payment, and pulls a verifiable receipt for what happened. Five minutes.

{% stepper %}
{% step %}

### Install

```bash
npm install @brainfinance/sdk
```

{% endstep %}

{% step %}

### Get a Key

Sign up at [console.brain.fi](https://console.brain.fi), create a tenant, and copy your sandbox API key (`brain_sk_test_...`).

```bash
# .env
BRAIN_API_KEY=brain_sk_test_...
```

{% hint style="info" %}
Sandbox uses test credentials and Base Sepolia for on-chain anchoring; no real money moves. The Console lives at `console.brain.fi`; sandbox API requests go to `https://staging-api.brain.fi/v1`, the same host the SDK uses for both `sandbox` and `staging`. Production API requests go to `https://api.brain.fi/v1`. See [API base URLs](/api-reference/overview#base-urls).
{% endhint %}

{% hint style="warning" %}
**Production keys (`brain_sk_live_...`) use the identical code path, but Brain is in staging / controlled pilot today.** Settlement rails run on **Base Sepolia** behind smart contracts **pending an external audit**, so a live key does not yet move real money on mainnet. See [Readiness Summary](/architecture/readiness-summary) before treating `brain_sk_live_` as production-ready.
{% endhint %}
{% endstep %}

{% step %}

### Build

```typescript
import { Brain, PolicyApprovalRequiredError } from "@brainfinance/sdk";

const brain = new Brain({ apiKey: process.env.BRAIN_API_KEY!, environment: "sandbox" });

// Read sandbox ledger data.
const accounts = await brain.accounts.list({ limit: 10 });
console.log(accounts.accounts);

// Ask the tenant's financial brain a question.
const answer = await brain.ask("acme", "What did we spend on AWS last month?");
console.log(answer.text);
console.log(answer.citations);

// Propose a payment.
let paymentId: string | undefined;
try {
  const result = await brain.pay("acme", {
    action_type: "ach_outbound",
    source_account_id: "acct_demo_ap",
    destination_counterparty_id: "cp_demo_vendor",
    amount: "125.00",
    currency: "USD",
    evidence_ids: ["raw_demo_invoice"],
    idempotencyKey: "quickstart-demo-001",
  });
  paymentId = result.intent.id;
} catch (error) {
  if (!(error instanceof PolicyApprovalRequiredError)) throw error;
  paymentId = error.intent.id;
  if (paymentId) {
    await brain.approve(paymentId);
    await brain.payments.execute(paymentId);
  }
}

// Pull a verifiable receipt.
const proof = await brain.proof(paymentId!);
console.log(proof.anchorTx); // on-chain anchor on Base Sepolia
console.log(proof.merklePath); // verifiable without trusting Brain
```

That's it. You just touched all five capabilities of Brain through one client.
{% endstep %}

{% step %}

### What You Just Built

| Line                     | What Brain did under the hood                                                                                 |
| ------------------------ | ------------------------------------------------------------------------------------------------------------- |
| `brain.accounts.list`    | Read normalized ledger accounts through the SDK                                                               |
| `brain.ask`              | Routed your question to a memory graph, retrieved relevant facts with citations, answered in natural language |
| `brain.pay`              | Created a PaymentIntent and evaluated it against the tenant's signed policy                                   |
| `brain.approve`          | Recorded an authenticated member approval when policy required it                                             |
| `brain.payments.execute` | Enqueued the approved intent for the worker-owned execution path                                              |
| `brain.proof`            | Pulled a Merkle proof from a tamper-evident log anchored on Base L2                                           |

You'll meet each of these underneath as you go deeper. For now, they're just five methods on one client.
{% endstep %}
{% endstepper %}

### Where to Go 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>Build</strong></td><td>Task-shaped guides. Read a tenant's full financial picture, give an agent a spending limit, audit every action.</td><td><a href="/pages/BJk23JjE7PeUfpXzaGcs">/pages/BJk23JjE7PeUfpXzaGcs</a></td><td></td></tr><tr><td><strong>Concepts</strong></td><td>The mental model in five minutes.</td><td><a href="/pages/5dhCMyKXDue9NEBWqOTf">/pages/5dhCMyKXDue9NEBWqOTf</a></td><td></td></tr><tr><td><strong>Protocol</strong></td><td>The deep stack: six layers, smart contracts, on-chain anchoring.</td><td><a href="/pages/m9CZyvHRkykhnfA4mZcY">/pages/m9CZyvHRkykhnfA4mZcY</a></td><td></td></tr></tbody></table>

### Stuck?

Error codes are lowercase `snake_case` (see the [full registry](/resources/errors)).

| Problem            | Fix                                                                                                                                                             |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auth_invalid_key` | Check `.env`. Sandbox keys start with `brain_sk_test_`, production with `brain_sk_live_`.                                                                       |
| `tenant_not_found` | Create a tenant in the Console first. Tenant IDs are case-sensitive.                                                                                            |
| `rate_limited`     | You hit your tier's per-minute limit. Honour the `Retry-After` header and retry. See [rate limits](/api-reference/overview#rate-limits) for the per-tier table. |

[**Full error reference**](/resources/errors)


# The Wiki Layer vs. RAG and Graph

### What It Is

The Wiki layer is the contextual knowledge surface in Brain's stack. It sits between the Ledger (canonical state) and the Agent (execution), and gives agents the meaning they need to act intelligently on financial data.

Think of it as Wikipedia for an autonomous financial system, except every article cites a primary source in the Ledger or Raw layer.

### What It Does

The Wiki layer handles three jobs that legacy memory systems split across separate tools:

1. **Entity resolution.** "Acme Corp," "ACME Corporation," and the LEI on the wire confirmation are the same thing. Wiki knows that.
2. **Relationship modeling.** Counterparty exposures, fund flows, ownership structures, vendor hierarchies. Stored explicitly, queryable.
3. **Narrative context.** Why a transaction happened, what a counterparty does, what risk class an asset falls into. Stored as semantic content, retrievable in natural language.

Every fact in Wiki points back to verifiable evidence in Raw or Ledger. If the source moves, Wiki updates. If sources conflict, Ledger wins.

### Comparison with RAG and Graph

Most memory systems use one of two tools.

**RAG** (Retrieval Augmented Generation) stores text chunks as vector embeddings and retrieves what looks similar at query time. It is fast, easy to set up, and useful for unstructured Q\&A. It has no entity identity, no relationships, and no notion of truth. Two strings that mean the same thing are unrelated to it. RAG tells you what it has read, not what it is.

**Graph** systems (knowledge graphs, property graphs) store entities and relationships explicitly. They support multi-hop traversal and structural reasoning. Strong on relationship questions. Weak on unstructured semantic content. Brittle when reality does not fit the schema. A standalone graph floats free of any system of record.

**Wiki** does both, anchored.

<table><thead><tr><th width="350">Capability</th><th>Wiki</th><th>RAG</th><th>Graph</th></tr></thead><tbody><tr><td>Semantic retrieval</td><td><strong>Yes</strong></td><td>Yes</td><td>Limited</td></tr><tr><td>Entity resolution</td><td><strong>Native</strong></td><td>No</td><td>Manual</td></tr><tr><td>Relationship traversal</td><td><strong>Yes</strong></td><td>No</td><td>Yes</td></tr><tr><td>Anchored to system of record</td><td><strong>Yes</strong></td><td>No</td><td>No</td></tr><tr><td>Provenance per fact</td><td><strong>Yes</strong></td><td>No</td><td>No</td></tr><tr><td>Conflict arbitration</td><td><strong>Ledger wins</strong></td><td>None</td><td>None</td></tr><tr><td>Suitable for autonomous money movement</td><td><strong>Yes</strong></td><td>No</td><td>No</td></tr></tbody></table>

### Why This Matters

When an agent is reasoning about money, "probably true" is not enough. A treasury agent that thinks the company has $4.2M in operating cash needs to be right, not approximately right. A lending agent applying a credit policy needs to know exactly which counterparty it is dealing with, not a fuzzy match.

RAG and graphs are useful tools, but they are retrieval systems, not systems of record. They tell agents what was said. Wiki tells agents what is.

The shorthand:

* RAG sounds right
* Graph connects right
* Wiki *is* right

### How Wiki Fits in Brain

Wiki is one of six layers in the Brain stack:

* **Raw** ingests signals
* **Ledger** maintains canonical state
* **Wiki** provides semantic context, grounded in Ledger
* **Policy** defines what agents are allowed to do
* **Agent** executes within policy, using Wiki to understand context
* **Audit** records every action with cryptographic provenance

Wiki is what makes Brain agents knowledgeable. Ledger is what makes them correct. Policy is what makes them safe. Audit is what makes them accountable. Together, they are what makes autonomous financial action trustworthy.


# Overview

Task-shaped guides. The patterns most apps need in their first hour.

Each guide on this page solves one task end-to-end. Pick the one that matches what you're trying to ship; come back for the rest as you grow.

<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>🚪 Sign Up and Onboard</strong></td><td>Self-provision a sandbox tenant; log in as a human or point an agent at it.</td><td><a href="/pages/Nt6Djhan47XRxLlsq7iM">/pages/Nt6Djhan47XRxLlsq7iM</a></td><td></td></tr><tr><td><strong>📊 Read a Financial Picture</strong></td><td>Pull balances, transactions, obligations, and counterparties for a tenant.</td><td><a href="/pages/srQkJDeBIoVx1YJ6sLg6">/pages/srQkJDeBIoVx1YJ6sLg6</a></td><td></td></tr><tr><td><strong>💸 Pay an Invoice Safely</strong></td><td>Propose a payment, route to approval if needed, execute, get a receipt.</td><td><a href="/pages/EvXLQlWSFcn9RYDpmBqe">/pages/EvXLQlWSFcn9RYDpmBqe</a></td><td></td></tr><tr><td><strong>🛡 Give an Agent a Spending Limit</strong></td><td>Define a policy in plain English. Brain enforces it on every proposed action.</td><td><a href="/pages/Wnu4l8fn2PmQ5iXwBzAx">/pages/Wnu4l8fn2PmQ5iXwBzAx</a></td><td></td></tr><tr><td><strong>📜 Audit Every Action</strong></td><td>Pull a verifiable trail of what your agent (or user) did.</td><td><a href="/pages/Fypem4aaYkeJlDtrdFXd">/pages/Fypem4aaYkeJlDtrdFXd</a></td><td></td></tr><tr><td><strong>🔌 Let an External Agent In</strong></td><td>Authorize an MCP-compatible agent to read and propose on a tenant's behalf.</td><td><a href="/pages/QlSPCaWGQFEIUdhwQS8v">/pages/QlSPCaWGQFEIUdhwQS8v</a></td><td></td></tr><tr><td><strong>🧩 Use Brain Agent Skills</strong></td><td>Install task-specific recipes for Brain's MCP proposal surface.</td><td><a href="/pages/tamHWgSuhBgqD5sKK0PD">/pages/tamHWgSuhBgqD5sKK0PD</a></td><td></td></tr></tbody></table>

### What Every Guide Assumes

| Assumption                    | How to satisfy                                            |
| ----------------------------- | --------------------------------------------------------- |
| You finished the Quickstart   | The SDK is installed, your API key works, a tenant exists |
| You're in sandbox             | Production works the same way; sandbox is just safer      |
| You're calling from a backend | Server keys never go in client-side code                  |

```typescript
import { Brain } from "@brainfinance/sdk";

export const brain = new Brain({ apiKey: process.env.BRAIN_API_KEY });
```

### What You'll Keep Coming Back To

| Pattern                          | Where                                                                      |
| -------------------------------- | -------------------------------------------------------------------------- |
| The SDK methods you already know | Each guide reuses `brain.ask`, `brain.pay`, `brain.approve`, `brain.proof` |
| Idempotency keys                 | Required on every mutating call; retries are free                          |
| Trace IDs                        | Returned on every response; paste into the Console for the full timeline   |
| Webhooks                         | Subscribe once, get notified on the events you care about                  |


# Sign Up and Onboard

Go from zero to an authenticated sandbox tenant. Human login or a wallet-based agent.

Self-serve onboarding provisions a **sandbox tenant** you can read and *propose* against immediately. Real money stays behind the promotion + external-audit gates, so onboarding is safe to explore end-to-end.

{% hint style="info" %}
Self-serve signup is gated by the `BRAIN_SELF_SERVE_SIGNUP` flag and lands every new tenant in **sandbox** (RFC 0002). Two principals share one tenant: a **human owner** (email/password or a linked wallet) for management + reads + approvals, and **agents** (wallet + on-chain scope) for the M2M tool surface. The human owner never gets `payment_intent:propose` / `*:execute`. Money movement is an agent + §6-gate concern.
{% endhint %}

### 1. Sign up

```bash
curl -sX POST "$BRAIN/v1/signup" -H 'content-type: application/json' \
  -d '{"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
```

Password is min 12 chars (stored as a scrypt hash). A duplicate email returns `409 signup_email_taken`. In production, the API sends the verification token through the configured ESP client and fails at boot if self-serve signup is enabled without ESP credentials.

### 2. Verify your email

```bash
curl -sX POST "$BRAIN/v1/auth/verify-email" -H 'content-type: application/json' \
  -d '{"tenant_id":"tnt_…","token":"<verification_token>"}'
# → 200 { "verified": true, "status": "active" }
```

Single-use, short-TTL token scoped to your tenant.

### 3. Log in for an owner token

```bash
curl -sX POST "$BRAIN/v1/auth/login" -H 'content-type: application/json' \
  -d '{"email":"founder@example.com","password":"a-strong-passphrase-12+"}'
# → 200 { "access_token":"eyJ…", "expires_in":900,
#         "principal": { "type":"user", "scopes":["ledger:read","wiki:read",
#           "raw:read","raw:write","policy:read","policy:write","audit:read","execution:read","payment_intent:approve"] } }
```

Use it as a bearer token. It's tenant-scoped (RLS) and 15-minute-lived; log in again to refresh.

```bash
curl -s "$BRAIN/v1/ledger/accounts" -H "authorization: Bearer $ACCESS_TOKEN"
```

### 4. Bring in a wallet or an agent

* **Link your own wallet** (so you can also sign in with it): `POST /v1/tenants/{tenant_id}/wallets` with your owner token. A wallet-based sign-in then mints the same owner token via SIWX.
* **Point an agent at Brain**: register the agent (it lands `pending_onchain`, becomes `active` once its `BrainMCPAgentRegistry` scope attestation confirms), then the agent signs in with SIWX and calls the MCP surface. Read, contribute, and **propose** (never execute; every settlement passes the §6 gate).

### 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>🔌 Let an External Agent In</strong></td><td>Authorize an MCP agent to read and propose.</td><td><a href="/pages/QlSPCaWGQFEIUdhwQS8v">/pages/QlSPCaWGQFEIUdhwQS8v</a></td><td></td></tr><tr><td><strong>💸 Pay an Invoice Safely</strong></td><td>Propose → approve → execute → receipt.</td><td><a href="/pages/EvXLQlWSFcn9RYDpmBqe">/pages/EvXLQlWSFcn9RYDpmBqe</a></td><td></td></tr><tr><td><strong>🔑 Authentication</strong></td><td>The full credential + token reference.</td><td><a href="/pages/0WFaizrTZls13BjYF116">/pages/0WFaizrTZls13BjYF116</a></td><td></td></tr></tbody></table>


# Read a Financial Picture

Pull balances, transactions, obligations, and counterparties for a tenant.

Goal: get a structured view of a tenant's full financial state, ready to render in a dashboard or feed to an LLM.

### In One Call

```typescript
const picture = await brain.snapshot("acme");

picture.accounts;        // [{ id, name, currency, currentBalance, ... }]
picture.transactions;    // recent, paginated
picture.obligations;     // upcoming, due, overdue
picture.counterparties;  // top counterparties by activity
picture.cashFlow;        // 30-day inflow/outflow summary
```

`brain.snapshot` is a convenience wrapper. It runs a handful of underlying calls in parallel and stitches the response together. For full control, call them yourself.

### In Five Calls

```typescript
const [accounts, transactions, obligations, counterparties, cashFlow] = await Promise.all([
  brain.accounts.list("acme"),
  brain.transactions.list("acme", { from: "2025-09-01", limit: 100 }),
  brain.obligations.list("acme", { status: ["upcoming", "due", "overdue"] }),
  brain.counterparties.list("acme", { sortBy: "activity", limit: 20 }),
  brain.cashFlow.summarize({ tenantId: "acme", since: "2025-09-01", until: "2025-09-30" }),
]);
```

### Filtering Transactions

```typescript
const txns = await brain.transactions.list("acme", {
  from: "2025-09-01",
  to:   "2025-09-30",
  direction:       "outflow",       // inflow | outflow | transfer | adjustment
  counterpartyId:  "cp_aws",
  minAmount:       100,
  status:          ["posted", "cleared"],
  limit:           50,
});

txns.data.forEach((t) => console.log(t.date, t.amount, t.description));
console.log(txns.nextCursor);
```

| Filter                   | Type     | Notes                                                            |
| ------------------------ | -------- | ---------------------------------------------------------------- |
| `from`, `to`             | ISO date | Inclusive                                                        |
| `direction`              | enum     | One or many                                                      |
| `counterpartyId`         | string   | Filter to one counterparty                                       |
| `accountId`              | string   | Filter to one account                                            |
| `minAmount`, `maxAmount` | decimal  | Currency-agnostic                                                |
| `currency`               | ISO 4217 | When mixing currencies                                           |
| `status`                 | enum\[]  | `pending`, `posted`, `cleared`, `failed`, `reversed`, `disputed` |

### Asking Questions Instead of Querying

Sometimes you don't know what to filter on. Ask in natural language.

```typescript
const answer = await brain.ask("acme", "Which counterparties did we pay the most in Q3?");
console.log(answer.text);
console.log(answer.citations);  // ledger references back to specific transactions
```

The answer comes with citations to the specific transactions it cites. You can render those in your UI as clickable proof.

### Paginating

All list endpoints return a `nextCursor`. Pass it on the next call.

```typescript
let cursor: string | undefined;
do {
  const page = await brain.transactions.list("acme", { from: "2025-01-01", cursor, limit: 200 });
  for (const t of page.data) {
    // process
  }
  cursor = page.nextCursor;
} while (cursor);
```

### Getting Notified of Changes

Instead of polling, use webhooks. Set the endpoint in the Console under Settings → Webhooks.

| Event                     | Payload                                 |
| ------------------------- | --------------------------------------- |
| `transaction.created`     | The new transaction                     |
| `transaction.updated`     | Status, amount, or counterparty changed |
| `account.balance_changed` | New balance for an account              |
| `obligation.due_soon`     | An obligation is N days from due        |

### 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>💸 Pay an Invoice</strong></td><td>Take action on what you just read.</td><td><a href="/pages/EvXLQlWSFcn9RYDpmBqe">/pages/EvXLQlWSFcn9RYDpmBqe</a></td><td></td></tr><tr><td><strong>🛡 Spending Limits</strong></td><td>Let an agent read and act, with guardrails.</td><td><a href="/pages/Wnu4l8fn2PmQ5iXwBzAx">/pages/Wnu4l8fn2PmQ5iXwBzAx</a></td><td></td></tr></tbody></table>


# Pay an Invoice Safely

Propose a payment, route to approval if needed, execute, get a receipt.

Goal: pay an invoice with a single SDK call. If it's within the tenant's policy, it goes through. If not, it routes to a human approver. Either way, you get a receipt you can show a customer.

### The Simplest Case

```typescript
const action = await brain.pay("acme", { invoiceId: "inv_8231" });

console.log(action.status);
// "auto"           → already executed
// "needs_approval" → waiting for a human
// "rejected"       → policy said no
```

{% hint style="info" %}
`action.status` uses the SDK aliases `auto | needs_approval | rejected`. The HTTP PaymentIntent lifecycle uses `approved | pending_approval | rejected | executed | …` for the same states (`auto` ⇒ `approved`/`executed`). See the [mapping table](/api-reference/payment-intents-api#status-lifecycle).
{% endhint %}

### Handling All Three Outcomes

```typescript
const action = await brain.pay("acme", { invoiceId: "inv_8231" });

switch (action.status) {
  case "auto":
    // Already done. Brain executed and recorded the receipt.
    console.log("paid:", action.receipt.txHash ?? action.receipt.railReceipt);
    break;

  case "needs_approval":
    // Surface to your approval UI. Approvers receive the action.
    console.log("waiting on:", action.approvers);
    break;

  case "rejected":
    console.log("blocked:", action.reason);
    break;
}
```

### Approving from Your App

```typescript
// In your approval UI, signed by the approver's key.
await brain.approve(actionId);
```

`approve` records the typed signature. Once all required approvers have signed, the intent becomes `approved` and Brain's internal settlement path runs the §6 gate and dispatches it; you do not call a separate execute step, and the approver's signature is not itself a settlement call. The action's status moves from `needs_approval` to `auto`.

For multi-approver policies, every required approver calls `brain.approve`. Brain holds the action in `needs_approval` until the last one lands.

### Rejecting from Your App

```typescript
await brain.reject(actionId, {
  reason: "Vendor under review",
});
```

Rejection is final. The action moves to `rejected` and emits a webhook your app can react to.

### Getting the Receipt

Every executed action has a verifiable receipt.

```typescript
const proof = await brain.proof(actionId);

proof.txHash;       // on-chain tx for on-chain rails (Base)
proof.railReceipt;  // bank receipt for ACH/wire
proof.merklePath;   // Merkle path to the on-chain anchor
proof.anchorTx;     // anchor transaction on Base L2
```

If you ever need to prove to a customer that a payment happened, this is the thing to send them. They can verify it without a Brain account.

### Paying Without an Invoice

Sometimes you're paying something that isn't a structured invoice yet (a vendor name and an amount, say). Pass the destination directly.

```typescript
const action = await brain.pay("acme", {
  to:        { counterpartyId: "cp_acme_legal" },
  amount:    "12500.00",
  currency:  "USD",
  memo:      "Q3 retainer",
});
```

Brain still runs every check it would run for an invoice payment.

### Idempotency

Always pass an idempotency key. Retries with the same key return the existing action instead of creating a duplicate.

```typescript
const action = await brain.pay("acme", {
  invoiceId:      "inv_8231",
  idempotencyKey: "pay_inv_8231_2025_09",
});
```

If your service crashes mid-call and your retry handler fires, you'll get the same action back. No duplicate payments.

### Webhooks for Long-Running Flows

Most ACH and wire payments don't settle instantly. Subscribe to the action's lifecycle.

Outbound webhooks use the `payment_intent.*` event names (the same `event_type` values the [Webhooks API](/api-reference/webhooks-api) lists), not an `action.*` namespace:

| `event_type`                   | When                                                            |
| ------------------------------ | --------------------------------------------------------------- |
| `payment_intent.created`       | Just after `brain.pay` returns (intent proposed)                |
| `payment_intent.approved`      | All required approvers have signed (or Policy returned `allow`) |
| `payment_intent.rejected`      | Policy or an approver rejected                                  |
| `payment_intent.execute.after` | The §6 gate ran and the intent was dispatched to its rail       |

{% hint style="info" %}
Settlement is asynchronous and there is **no** dedicated `payment_intent.settled` / `payment_intent.failed` outbound event today. `payment_intent.execute.after` fires when the intent is dispatched; final rail settlement (or failure) is observed via the rail-specific provider webhook and confirmed through [`GET /v1/proof/{action_id}`](/api-reference/proof-api) / replay-investigation.
{% endhint %}

```typescript
// Webhook handler
app.post("/webhooks/brain", verifyBrainSig, (req, res) => {
  const event = req.body;
  switch (event.event_type) {
    case "payment_intent.execute.after":
      markInvoiceDispatched(event.data.invoiceId);
      break;
  }
  res.sendStatus(200);
});
```

### What if My Action Fails?

Brain returns a structured failure code and never silently retries.

```json
{
  "status": "failed",
  "reason": "INSUFFICIENT_BALANCE",
  "details": {
    "required":  "61404.12 USD",
    "available": "58901.04 USD",
    "accountId": "acct_ops"
  }
}
```

You can re-propose with a different source account, a different amount, or wait until the balance covers it.

### 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>🛡 Spending Limits</strong></td><td>Define what counts as "needs approval" in plain English.</td><td><a href="/pages/Wnu4l8fn2PmQ5iXwBzAx">/pages/Wnu4l8fn2PmQ5iXwBzAx</a></td><td></td></tr><tr><td><strong>📜 Audit Trail</strong></td><td>Pull the full record of what happened and why.</td><td><a href="/pages/Fypem4aaYkeJlDtrdFXd">/pages/Fypem4aaYkeJlDtrdFXd</a></td><td></td></tr></tbody></table>


# Give an Agent a Spending Limit

Define a policy in plain English. Brain enforces it on every proposed action.

Goal: write a sentence in English describing what an agent (or human user) can do with a tenant's money. Brain compiles it to a deterministic rule, signs it with the tenant's key, and enforces it on every proposed action.

### The Simplest Policy

```typescript
const policy = await brain.policy.compose("acme", {
  text:
    "Allow invoice payments under $5,000 to approved vendors. " +
    "Require CFO approval above $5,000. " +
    "Block payments to new counterparties without review.",
});

await brain.policy.sign(policy.id);
```

That's the whole flow. From this point on, every `brain.pay` call evaluates against this policy.

{% hint style="warning" %}
**Plain-English authoring is the intended experience, but not yet wired.** Today, policies are authored as **structured JSON DSL**, not prose; there is no natural-language compile step on either the SDK or the HTTP API. The real SDK call is `brain.policy.compose(tenantId, dsl)`: it **validates the DSL** and returns the EIP-712 signing payload, which you then submit via `brain.policy.sign(...)` (also exposed as `activate`), which takes signatures, not a policy id. Non-SDK callers POST the same DSL to `/policy/{tenant_id}/compose`. See the [Policy API](/api-reference/policy-api#compose-a-candidate-policy) for the JSON shape. Treat the `{ text: "…" }` form below as illustrative of intent until NL authoring ships.
{% endhint %}

### Reviewing What Got Compiled

The compiler returns the structured rules and a human-readable explanation. Always review before activating.

```typescript
console.log(policy.explanation);
// This policy will:
//  - Auto-approve payments under $5,000 to vendors marked as approved
//  - Escalate payments at or above $5,000 to anyone with the CFO role
//  - Reject all payments to counterparties not yet on the approved list

console.log(policy.rules);
// [
//   { if: "amount < 5000 && counterparty.known", then: "auto" },
//   { if: "amount >= 5000 && counterparty.known", then: "needs_approval", approvers: ["role:cfo"] },
//   { if: "!counterparty.known", then: "rejected", reason: "new_counterparty_review_required" }
// ]
```

If the explanation matches your intent, activate. If not, edit the text and recompile.

### Trying It Before You Ship It

Dry-run a hypothetical action against the active policy.

```typescript
const decision = await brain.policy.evaluate("acme", {
  type:           "pay_invoice",
  amount:         7800,
  currency:       "USD",
  counterpartyId: "cp_vendor_x",
});

console.log(decision.outcome);     // "auto" | "needs_approval" | "rejected"
console.log(decision.matchedRule); // which rule fired
console.log(decision.approvers);   // populated if needs_approval
```

`evaluate` doesn't create a payment intent. It just shows you what would happen. Useful for testing edge cases before activating.

{% hint style="info" %}
`decision.outcome` returns the SDK aliases `auto | needs_approval | rejected`. Over HTTP/MCP the same decision is the canonical `allow | confirm | reject`, and the rule's `then` (`execute`) field uses `auto | confirm | reject`. The three vocabularies map 1:1; see [Policy → decision vocabulary across surfaces](/api-reference/policy-api#decision-vocabulary-across-surfaces).
{% endhint %}

### Approvers

Approvers are referenced by role or user.

```typescript
"Allow invoice payments under $5,000.
 Require CFO approval above $5,000.
 Require both CFO and CEO approval above $50,000."
```

| Reference               | Matches                               |
| ----------------------- | ------------------------------------- |
| `role:cfo`              | Anyone in your team with the CFO role |
| `role:cfo + role:ceo`   | Both must sign                        |
| `user:user_cfo`         | A specific user                       |
| `any:role:cfo,role:ceo` | Either CFO or CEO                     |

### Approving Counterparties

Many policies key off "approved vendors." A counterparty's trust standing is the server-controlled `verified_status` field (`unverified`, `self_attested`, `document_verified`, `sanctions_cleared`). It is not a value you set directly through the SDK: manual counterparty edits are identity-only, and trust fields are managed server-side through verification and the Console.

Once a counterparty is verified, payments to it fall under the "approved vendor" branch of the policy.

### Multiple Environments

Policies are per-tenant, per-environment. Sandbox and production each have their own active policy. You'll typically:

| Environment    | Policy approach                                                   |
| -------------- | ----------------------------------------------------------------- |
| **Sandbox**    | Loose (high limits, few required approvers) for testing           |
| **Production** | Tight (low limits, multiple approvers, narrower vendor allowlist) |

### Updating a Policy

Policies are versioned. New text creates a new version that supersedes the old one.

```typescript
const v2 = await brain.policy.compose("acme", {
  text: "..."  // new policy text
});

await brain.policy.sign(v2.id);
```

The old version is automatically deactivated. Past actions remain bound to the version that was active when they were proposed; you can always see which version evaluated which action by reading the action's metadata.

### What Policy Can Express

| Concept                           | Example                                        |
| --------------------------------- | ---------------------------------------------- |
| **Amount thresholds**             | "above $5,000", "between $1,000 and $10,000"   |
| **Counterparty status**           | "approved vendors", "new counterparties"       |
| **Counterparty type**             | "to employees", "to tax authorities"           |
| **Account balance preconditions** | "if the account balance is at least $50,000"   |
| **Time windows**                  | "between 9am and 5pm Pacific", "weekdays only" |
| **Approval requirements**         | "require approval from", "with sign-off by"    |
| **Outright denial**               | "block", "do not allow", "reject"              |

### What Policy Can't Express in Plain English (Yet)

Edge cases that need precise semantics. For these, you can author rules directly. See Policy in the Protocol section for the rule grammar.

### 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>💸 Pay an Invoice</strong></td><td>Watch the policy you just wrote enforce itself.</td><td><a href="/pages/EvXLQlWSFcn9RYDpmBqe">/pages/EvXLQlWSFcn9RYDpmBqe</a></td><td></td></tr><tr><td><strong>📜 Audit Trail</strong></td><td>Every policy decision lands in the audit log.</td><td><a href="/pages/Fypem4aaYkeJlDtrdFXd">/pages/Fypem4aaYkeJlDtrdFXd</a></td><td></td></tr></tbody></table>


# Audit Every Action

Pull a verifiable trail of what your agent (or user) did.

Goal: pull a complete, tamper-evident record of every meaningful event for a tenant. Useful for compliance review, customer disputes, internal reporting, and proving to auditors that the right thing happened.

Brain's API and off-chain audit service are production available. The public anchor contract is deployed on Base Sepolia only, is unaudited, and is not a Base mainnet deployment.

### Reading the Trail

```typescript
const events = await brain.audit.list("acme", {
  from:  "2025-09-01",
  to:    "2025-09-30",
  type:  "action.executed",  // optional filter
});

events.data.forEach((e) => {
  console.log(e.timestamp, e.type, e.actor, e.summary);
});
```

| Type                  | When                                  |
| --------------------- | ------------------------------------- |
| `source.connected`    | A source connected for the tenant     |
| `transaction.created` | A new transaction landed              |
| `wiki.query`          | A natural-language question was asked |
| `policy.evaluated`    | A policy decision was rendered        |
| `action.proposed`     | An agent proposed an action           |
| `action.approved`     | A human signed approval               |
| `action.executed`     | An action settled on its rail         |
| `audit.anchored`      | A Merkle root was anchored on Base    |

### Verifying a Specific Action

For any action, you can pull a Merkle proof verifiable on-chain.

```typescript
const proof = await brain.proof(actionId);

proof.event;        // the event itself
proof.merklePath;   // sibling hashes from leaf to root
proof.anchorRoot;   // the Merkle root anchored on Base
proof.anchorTx;     // the transaction that anchored it
proof.anchorBlock;  // the Base block number
```

You can hand this to a counterparty or auditor. They can verify it without trusting Brain.

```solidity
// Public verifier on Base Sepolia
bool published = brainAuditAnchor.isPublished(tenantIdHash, proof.anchorRoot);
bool included = brainAuditAnchor.verifyInclusion(
  proof.anchorRoot,
  eventLeaf,
  merklePath
);
```

### Pulling the Trace for One Action

Trace IDs link every event tied to one action.

```typescript
const trace = await brain.trace(actionId);

console.log(trace.events);
// [
//   { type: "action.proposed",    timestamp: "..." },
//   { type: "policy.evaluated",   decision: "needs_approval" },
//   { type: "action.approved",    actor: "user_cfo" },
//   { type: "action.executed",    rail: "ach", txHash: null },
//   { type: "action.settled",     receipt: "..." },
//   { type: "audit.anchored",     merkleRoot: "0x...", txHash: "0x..." }
// ]
```

You can paste a trace ID into the Console to see the same view rendered visually.

### Exporting for Compliance Review

For SOC 2, ISO 27001, or any structured review, export the log as a file.

```typescript
const job = await brain.audit.export("acme", {
  format: "ndjson",  // or "csv"
  from:   "2025-01-01",
  to:     "2025-12-31",
});

console.log(job.jobId);  // track this export job
```

The export contains every event in the range plus the Merkle proofs needed to verify any of them after the fact.

### Streaming Events Live

Use webhooks to receive audit events as they happen, then ship them to your SIEM, Datadog, Splunk, or wherever you centralize logs.

### Filtering by Actor

Useful for "what did agent X do today?"

```typescript
const today = await brain.audit.list("acme", {
  actor: "agent:payments-v1",
  from:  new Date(Date.now() - 86400_000).toISOString(),
});
```

Or "what did user Y do?"

```typescript
const trail = await brain.audit.list("acme", {
  actor: "user:user_cfo",
  from:  "2025-01-01",
});
```

### What You Don't Have to Worry About

| Concern               | Why Brain handles it                                                                |
| --------------------- | ----------------------------------------------------------------------------------- |
| **Tamper resistance** | Every event is hashed and chained; each tenant-root pair is published once          |
| **Publisher control** | The current Base Sepolia publisher is one EOA; rotation is a two-step handoff       |
| **Reorg safety**      | Pending anchors are retried; off-chain status remains authoritative until confirmed |
| **Privacy**           | Only Merkle roots and hashed tenant IDs are on-chain; no payload data leaks         |

### 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>🔌 External Agent</strong></td><td>Authorize an MCP-compatible agent and audit its actions the same way.</td><td><a href="/pages/QlSPCaWGQFEIUdhwQS8v">/pages/QlSPCaWGQFEIUdhwQS8v</a></td><td></td></tr><tr><td><strong>📦 Audit and Proof</strong></td><td>How the audit trail works underneath.</td><td><a href="/pages/Fypem4aaYkeJlDtrdFXd">/pages/Fypem4aaYkeJlDtrdFXd</a></td><td></td></tr></tbody></table>


# Let an External Agent In

Authorize an MCP-compatible agent to read and propose on a tenant's behalf.

Goal: authorize an external agent (one you didn't write) to read a tenant's financial state and propose actions on the tenant's behalf, with the same policy and audit guarantees as anything you'd build yourself.

External agents speak [MCP](https://modelcontextprotocol.io). Brain ships an MCP server. The integration is mostly authorization, not code.

### The Flow

```
1. Agent owner registers their agent with Brain.
2. Tenant grants the agent specific scopes (read, propose, etc.).
3. Agent connects to the MCP endpoint (POST /v1/agents/mcp) with a JWT.
4. Brain enforces scope on every call.
5. Every read and propose lands in the tenant's audit log.
```

### Step 1: Register the Agent

Agent owners register once. Tenants do not see this step.

```typescript
const agent = await brain.agents.register({
  address:        "0xAgentAddress",
  capabilities:   ["read", "propose_payment", "propose_action"],
  // Planned (RFC 0001), NOT yet anchored on-chain; accepted by the SDK but
  // dropped before the on-chain write:
  identityRoot:   "0x...",                          // ERC-8004 identity root (planned, RFC 0001)
  mcpEndpoint:    "https://my-agent.example.com/mcp",
});

console.log(agent.id);       // ag_8231
console.log(agent.txHash);   // BrainMCPAgentRegistry registration on Base
```

{% hint style="warning" %}
**What actually lands on-chain.** The deployed `BrainMCPAgentRegistry` struct stores only `agentId`, `agentAddress`, `tenantId`, `scopeHash`, and `behaviorHash`. `identityRoot`, `mcpEndpoint`, and `capabilities[]` are the **planned** ERC-8004 target (RFC 0001) and are not anchored today. Your `capabilities` are not written as a list; the SDK compiles them (together with the scope grant in Step 2) into the single `scopeHash` the contract stores, and the agent's JWT `scope_hash` claim must equal it. Under the hood this is a tenant-signed registration via `POST /v1/execution/agents/register`. See [BrainMCPAgentRegistry](/smart-contracts/brainmcpagentregistry).
{% endhint %}

### Step 2: Grant the Agent Scope

The tenant authorizes the agent for specific capabilities, on this tenant only.

```typescript
const grant = await brain.agents.grantScope("acme", agent.id, {
  scopes: [
    "ledger:read",
    "wiki:read",
    "payment_intent:propose",
  ],
  validFrom: Date.now(),
  validTo:   Date.now() + 30 * 86400_000,  // 30 days
});
```

The tenant signs an EIP-712 message under the hood; the SDK handles it. The grant's hash is anchored on Base.

| Scope                    | Allows                                                                   |
| ------------------------ | ------------------------------------------------------------------------ |
| `ledger:read`            | Read accounts, transactions, obligations, counterparties                 |
| `wiki:read`              | Ask natural-language questions; get cited answers                        |
| `raw:write`              | Push artifacts (transcripts, documents) into the tenant's evidence layer |
| `payment_intent:propose` | Propose payments (cannot execute)                                        |
| `execution:propose`      | Propose non-financial actions (reconciliation matches, anomaly flags)    |

{% hint style="warning" %}
External agents only ever **propose**; they never **execute**. Once an action is approved (Policy returned `allow`, or all required human approvals are in), Brain's internal settlement path runs the §6 gate and dispatches it. The proposing agent never moves the money itself, and a human approval supplies a signature, not a settlement call. That separation is the safety guarantee that makes external agents safe to authorize.
{% endhint %}

#### One permission, three vocabularies

The same grant is spelled three ways depending on the surface. They map 1:1:

| SDK `register` capability | SDK `grantScope` scope     | HTTP `allowed_actions` / `action_types_proposable` |
| ------------------------- | -------------------------- | -------------------------------------------------- |
| `read`                    | `ledger:read`, `wiki:read` | `read_wiki` (+ `read_ledger`, `read_audit`)        |
| `propose_payment`         | `payment_intent:propose`   | `action_types_proposable: ["outbound_payment", …]` |
| `propose_action`          | `execution:propose`        | `propose_action`                                   |
| (contribute evidence)     | `raw:write`                | `write_raw`                                        |

The SDK `register` capabilities are the coarse intent; `grantScope` scopes are the canonical `{layer}:{verb}` strings the gate checks; the [HTTP `POST /v1/execution/agents/register`](/api-reference/agents-api#register-an-external-agent) body uses `allowed_actions` + `action_types_proposable`. Pick the vocabulary for your surface; Brain stores them all as one `scopeHash`.

### Step 3: the Agent Connects

The MCP surface is a JSON-RPC endpoint on the same API host. There is no separate MCP hostname. The agent owner points their MCP runtime at:

```
POST https://api.brain.fi/v1/agents/mcp           (production)
POST https://api.sandbox.brain.fi/v1/agents/mcp   (sandbox)
```

The runtime authenticates with a JWT signed by the agent's registered key. The first call discovers the tools the agent has scope for.

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list"
}
```

A tenant who granted only `ledger:read` and `wiki:read` will see exactly those tools and no others.

### Step 4: the Agent Works

From the agent's side, calls look like this.

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "wiki.question",
    "arguments": {
      "tenant_id": "acme",
      "question":  "What invoices are overdue?"
    }
  }
}
```

The same Policy gating, the same Wiki memory, the same audit emission as anything you'd call from your own backend. Identical guarantees.

### Step 5: You Watch

Every external agent action lands in the tenant's audit log.

```typescript
const events = await brain.audit.list("acme", {
  actor: `agent:${agent.id}`,
  from:  "2025-09-01",
});

events.data.forEach((e) => console.log(e.type, e.timestamp, e.summary));
```

The Console shows agent activity in real time under **Agents → Activity**.

### Revoking an Agent

Revocation is immediate.

```typescript
await brain.agents.revoke("acme", agent.id);
```

Within at most 60 seconds (the on-chain scope cache window), the agent's calls fail. Already-stored evidence and prior actions remain (the audit log is immutable). Future calls are rejected.

### What This Enables

| Pattern                  | Example                                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------------------ |
| **Specialist agents**    | A vendor-management agent authorized to read invoices and counterparties only                          |
| **Compliance bots**      | A bot authorized to read audit events and flag anomalies                                               |
| **Cross-product agents** | An agent that contributes evidence (transcripts, contracts) to multiple tenants under their own scopes |
| **Marketplaces**         | Tenants discover, authorize, and revoke agents from a marketplace without writing code                 |

### What This Does Not Enable

| Pattern                                    | Why not                                                              |
| ------------------------------------------ | -------------------------------------------------------------------- |
| **External agents that execute**           | Execution is internal-only by design                                 |
| **Agents that read across tenants**        | Scope is per-tenant; cross-tenant requires explicit, separate grants |
| **Agents that bypass policy**              | All proposals run through the same Policy evaluator                  |
| **Agents that read each other's evidence** | Tenant isolation extends to evidence contributed by agents           |

### 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>🔌 MCP Server</strong></td><td>The full reference for the MCP surface.</td><td><a href="/pages/zjEFSPkZADwcvDIrQ4kS">/pages/zjEFSPkZADwcvDIrQ4kS</a></td><td></td></tr><tr><td><strong>📜 Audit Trail</strong></td><td>Watch what external agents do.</td><td><a href="/pages/Fypem4aaYkeJlDtrdFXd">/pages/Fypem4aaYkeJlDtrdFXd</a></td><td></td></tr></tbody></table>


# Install the Brain Finance Plugin

Install all 11 Brain Finance skills and the official MCP connection, as a Claude plugin or from any MCP-capable agent runtime.

The `brain-finance` plugin packages 11 portable `SKILL.md` recipes and the official Brain MCP connection in one installation. Each skill teaches an agent how to gather the evidence required for a finance task, call Brain's MCP tools, return the policy result, and stop at the proposal boundary.

The Claude plugin is the turnkey packaging, but the skills are provider-neutral. The portable core is the Brain MCP server, a standard MCP surface with OAuth 2.0 discovery, so any MCP-capable runtime can connect. See [Use with Other Agent Runtimes](#use-with-other-agent-runtimes) below.

The skills add no protocol behavior. Brain remains the source of financial data, policy decisions, approvals, and audit records.

## Available Skills

| Skill                  | Outcome                                    | Authority boundary                                    |
| ---------------------- | ------------------------------------------ | ----------------------------------------------------- |
| `brain-reconciliation` | Match statement and ledger activity        | Proposes matches                                      |
| `brain-subscription`   | Review recurring charges and price changes | Proposes findings                                     |
| `brain-vendor-risk`    | Review vendors and changed destinations    | Confirm/reject ceiling                                |
| `brain-collections`    | Prepare overdue-invoice follow-up          | Proposes a reviewed draft                             |
| `brain-fraud-anomaly`  | Flag suspicious transactions               | Notify-only; card freeze requires an explicit request |
| `brain-cash-forecast`  | Project cash and runway                    | Proposes a forecast                                   |
| `brain-dispute`        | Build a linked evidence packet             | Proposes the packet                                   |
| `brain-payment`        | Prepare an invoice payment                 | Proposes a payment intent                             |
| `brain-treasury`       | Prepare a sweep or account top-up          | Proposes a payment intent                             |
| `brain-revenue-intel`  | Surface churn and expansion signals        | Notify-only                                           |
| `brain-compliance`     | Review policy decisions and audit gaps     | Confirm/reject ceiling                                |

## Install

The skill repository is [`braindotfi/brain-skills`](https://github.com/braindotfi/brain-skills). In Claude Code, add Brain's public marketplace and install the single plugin:

```
/plugin marketplace add braindotfi/brain-skills
/plugin install brain-finance@brain-skills
```

The installation adds all 11 skills and configures the `brain` MCP server at `https://mcp.brain.fi`. Individual skills activate when the user's task matches their frontmatter descriptions. Credentials are not stored in the plugin; the host resolves the operator's Brain token at runtime.

The current package version is `0.1.0-beta.1`. Its manifests, skills, drift checks, isolated installation tests, and static security review are complete. The `https://mcp.brain.fi` endpoint is deployed and serves the OAuth 2.0 discovery contract described below. Full launch remains gated on the human Phase 0 proof, which exercises an authenticated read and proposal end to end.

## OAuth and Runtime Authentication

The plugin stores no credential. An unauthenticated MCP connection receives an HTTP `401` challenge whose `WWW-Authenticate: Bearer` header points to Brain's OAuth protected-resource metadata at:

```
https://mcp.brain.fi/.well-known/oauth-protected-resource
```

That metadata names Brain's authorization server (`https://auth.brain.fi`) and the scopes it understands. The host uses it to discover the authorization server, show the requested scopes, obtain user consent, and receive a runtime bearer token. Brain then verifies the token's tenant and scopes, including the on-chain `scope_hash`, before accepting a tool call.

The discovery contract is live and can be probed today. Phase 0 remains the launch gate for the authenticated path: it must prove consent, token issuance, an authorized read, and a confirm/reject proposal from each supported host.

## Install a Standalone Skill in Agensi

Agensi installs standalone skill archives and does not carry the Claude plugin's root `.mcp.json`. Build the archives from the reviewed source:

```bash
git clone https://github.com/braindotfi/brain-skills.git
cd brain-skills
npm run build:agensi
```

This produces 11 archives under `dist/agensi/`. The directory is generated and gitignored; ZIP files are not stored in the repository. Each built `SKILL.md` includes the prerequisite to connect `https://mcp.brain.fi` manually before use.

## Use with Other Agent Runtimes

The skills are not Claude-specific. Because the Brain MCP server is a standard MCP surface with OAuth 2.0 discovery, any MCP-capable runtime can register it and get the same policy-gated, propose-only tools. Build a provider-neutral bundle from the reviewed source:

```bash
git clone https://github.com/braindotfi/brain-skills.git
cd brain-skills
npm run build:portable
```

This writes `dist/portable/`, which contains `skills-manifest.json`, a machine-readable index of all 11 skills (id, description, trigger patterns, readable scopes, propose tool, and action types), plus each skill body as a provider-neutral instruction file. An orchestrator on any provider can read the manifest to route a request to the right skill.

Three invariants make this portable:

1. Point the runtime at `https://mcp.brain.fi` over MCP HTTP.
2. Never embed a credential. The host supplies the runtime bearer token, and Brain resolves tenant and scopes from it through the OAuth discovery flow.
3. Keep human approval between propose and execute. There is no execute, settle, or sign tool on the surface.

Copy-paste registration examples for OpenAI (Agents SDK and Responses API), Google Gemini, and Anthropic live in the repository under [`docs/providers/`](https://github.com/braindotfi/brain-skills/tree/main/docs/providers). These are MCP-compatibility guides; each integration should still be exercised against a sandbox tenant before production use.

## Verify the Package

The public repository contains the source and package checks:

```bash
git clone https://github.com/braindotfi/brain-skills.git
cd brain-skills
npm test
```

The test suite validates the plugin and marketplace manifests, checks all 11 skills against Brain's generated public specification, verifies that every MCP reference copy is byte-identical, enforces money-mover and frontmatter safety invariants, builds all 11 Agensi archives, and performs an isolated Claude marketplace installation.

The drift check warns when the generated specification is more than 30 days old and recommends regenerating it; the build no longer fails on age alone, since spec correctness is enforced by the field-level comparison. Changes to Brain's internal agent definitions trigger a private `brain-core` workflow that regenerates the public-safe specification and opens or updates a reviewable pull request in `brain-skills`.

The repository also includes:

* `SECURITY.md` and a file-grounded eight-point static review;
* injection-rejection examples for untrusted documents, instructions, and payment destinations;
* `scripts/verify-phase0.mjs` and a two-host human verification runbook;
* listing drafts that remain blocked until Phase 0 passes.

## Runtime Contract

Every skill follows the same sequence:

1. Connect to `https://mcp.brain.fi` using MCP over HTTP.
2. Authenticate with the operator's runtime-supplied Brain token.
3. Read only the scopes declared by the selected agent.
4. Gather the agent's required evidence.
5. Respect the agent's minimum-confidence floor and authority boundary.
6. Call `agent.action.propose`, or `payment_intent.propose` for Payment and Treasury. Duplicate proposal protection is server-side; the agent does not pass an idempotency key.
7. Return the proposal id, policy decision, unresolved evidence, and next review step.

The proposing host does not sign, dispatch, or move funds. Payment and Treasury have no default action, so a financial proposal requires an explicit request or a matched event with complete evidence.

## Agent-Specific Safety Boundaries

High risk does not imply one shared default-action rule:

* Vendor Risk and Compliance have a confirm/reject ceiling.
* Fraud and Anomaly legitimately defaults to `notify`; notification changes nothing. Its consequential `freeze_card` action is explicit-request-only and never selected from an anomaly trigger.
* Payment and Treasury categorically omit a default action because they are the two money-moving skills.

These fields are generated from Brain's internal-agent definitions into a public-safe specification. The skill repository's CI compares every `brain-meta.json` file with that specification and rejects drift.

## Phase 0 Launch Gate

Phase 0 is not an automated CI claim. A human must use a dedicated sandbox tenant to prove:

1. installation in Claude Code and standalone-skill installation in Agensi;
2. OAuth metadata discovery, scope review, and user consent;
3. an authorized `ledger.accounts.list` call;
4. a `payment_intent.propose` result ending in `pending_approval` or `rejected`;
5. absence of execute, settle, or sign tools; and
6. no balance or settlement change.

The exact checklist and evidence requirements live in [`docs/phase0-runbook.md`](https://github.com/braindotfi/brain-skills/blob/main/docs/phase0-runbook.md).

## Updating the Skills

The private source of truth remains the internal-agent catalog. The sync path is:

```
brain-core internal-agent definitions
  -> tools/skills-spec/generate.ts
  -> brain-skills/spec/brain-agents.json
  -> brain-skills/scripts/check-drift.mjs
```

When an agent definition changes, regenerate the specification, update the affected skill copy, and run the drift check before publishing. The automated cross-repository workflow requires a human-provisioned `BRAIN_SKILLS_PUSH_TOKEN`; the token is referenced by name and is never embedded in source or generated output.

## Related

* [Let an External Agent In](/build/let-an-external-agent-in)
* [Internal Agents](/concepts/internal-agents)
* [MCP Server](/mcp-server/overview)
* [MCP Tools](/mcp-server/tools)


# Overview

The mental model in five minutes.

Brain is one API for autonomous financial operations. Underneath, it does four things, in this order, every time:

```
Remember   →   Decide   →   Execute   →   Prove
```

| Word         | What Brain does                                                                                             | What you call it                       |
| ------------ | ----------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| **Remember** | Pulls in evidence from banks, ERPs, processors, on-chain wallets, and structures it into a queryable record | `brain.ask`, `brain.transactions.list` |
| **Decide**   | Evaluates every proposed action against rules the tenant signed                                             | runs automatically inside `brain.pay`  |
| **Execute**  | Dispatches the action through the right rail (ACH, ERP write, on-chain)                                     | `brain.pay`, `brain.approve`           |
| **Prove**    | Records every step in a tamper-evident log anchored on Base L2                                              | `brain.proof`, `brain.audit.list`      |

Everything else in this documentation is depth on those four steps.

### Why This Matters

Most fintech infrastructure stops at execution. The provider moves the money and confirms it landed. That leaves the integrating application to handle context, rules, and audit on its own. Brain is built around the fact that agents need all four steps, in order, on every action.

| Without remember           | Without decide            | Without execute        | Without prove                     |
| -------------------------- | ------------------------- | ---------------------- | --------------------------------- |
| Agent acts on partial data | Agent acts outside policy | Agent can't move money | Agent's actions can't be verified |

### The Four Ideas in Detail

<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>🧠 Memory</strong></td><td>What Brain knows about a tenant, where it came from, and how to query it.</td><td><a href="/pages/lj8rT9koqxMc6YXPpcDL">/pages/lj8rT9koqxMc6YXPpcDL</a></td><td></td></tr><tr><td><strong>🛡 Policy</strong></td><td>The rules a tenant signed. How decisions are made.</td><td><a href="/pages/pKaCvQpKvk0xKd6TcXzR">/pages/pKaCvQpKvk0xKd6TcXzR</a></td><td></td></tr><tr><td><strong>🤖 Agents</strong></td><td>Who can read, propose, and act. Internal and external.</td><td><a href="/pages/SKNJp6HT7EckCBOhNHMl">/pages/SKNJp6HT7EckCBOhNHMl</a></td><td></td></tr><tr><td><strong>📜 Proof</strong></td><td>Why every claim Brain makes is verifiable.</td><td><a href="/pages/Fg2F0IzPndDnqtabdkwF">/pages/Fg2F0IzPndDnqtabdkwF</a></td><td></td></tr></tbody></table>

### Tenants

Everything in Brain happens **for a tenant**. A tenant is a customer of yours: a business, a workspace, a user. Brain isolates tenants at the storage, key, and policy layer.

```typescript
await brain.ask("acme", "...");        // for tenant "acme"
await brain.pay("acme", { ... });      // also "acme"
await brain.audit.list("acme", { ... }); // also "acme"
```

Cross-tenant access is impossible by construction. You'll never see one tenant's data accidentally surface in another tenant's response.

### Provenance

Every fact Brain returns carries citations.

```typescript
const answer = await brain.ask("acme", "What did we spend on AWS last month?");

answer.text; // a natural-language answer
answer.citations; // pointers back to the specific transactions, invoices, or evidence that produced the answer
```

You never have to take Brain's word for anything. Every claim links back to source evidence.

### Idempotency

Every mutating call accepts (and most require) an idempotency key.

```typescript
await brain.pay("acme", {
  invoiceId: "inv_8231",
  idempotencyKey: "pay_inv_8231_2025_09",
});
```

Retries with the same key return the existing action, so a network blip never produces a duplicate payment.

### Where the Depth Lives

When you're ready, the protocol underneath has more to offer. Each of those concepts maps to a specific layer.

| Concept | Layer (deep dive)                 |
| ------- | --------------------------------- |
| Memory  | Raw, Ledger, and Wiki             |
| Policy  | Policy and the pre-execution gate |
| Agents  | The Agent layer                   |
| Proof   | Audit and on-chain anchoring      |

You can build with Brain without reading any of those sections. They are there when you want the deeper view.


# Memory

What Brain knows about a tenant, where it came from, and how to query it.

Brain holds, per tenant, a continuously updated record of every financial fact it has seen: transactions, balances, accounts, counterparties, obligations, invoices, contracts, on-chain transfers. You can query it as structured data or in natural language.

### Two Ways to Read It

```typescript
// Structured.
const txns = await brain.transactions.list("acme", { from: "2025-09-01" });

// Natural language.
const answer = await brain.ask("acme", "What did we spend on AWS last month?");
```

Both reach the same underlying record. Structured queries are precise and predictable. Natural-language questions are forgiving and discoverable. Use structured for code paths you'll hit often; use natural language for the questions you wouldn't have thought to filter for.

### What Goes In

Brain ingests from any source the tenant authorizes:

| Category                 | Examples                                                         |
| ------------------------ | ---------------------------------------------------------------- |
| **Banks and processors** | Plaid, direct bank APIs, Stripe, Adyen                           |
| **On-chain**             | Wallets via Alchemy, contract event streams                      |
| **ERPs**                 | NetSuite, SAP, Dynamics                                          |
| **Accounting**           | QuickBooks, Xero                                                 |
| **Payroll**              | Gusto, Rippling, ADP                                             |
| **Documents**            | Email-attached invoices, CSV/PDF uploads                         |
| **Agent contributions**  | Transcripts, signed contracts, observations from external agents |

You connect a source once. Brain handles the rest: pulling, parsing, normalizing, indexing, keeping it current.

### Citations on Every Claim

Every answer Brain gives carries citations back to source evidence.

```typescript
const answer = await brain.ask("acme", "Which vendor did we pay the most last quarter?");

answer.text;
// "Amazon Web Services, $182,431 across 14 invoices."

answer.citations;
// [
//   { type: "transaction", id: "tx_4127" },
//   { type: "transaction", id: "tx_4128" },
//   { type: "invoice",     id: "inv_8231" },
//   ...
// ]
```

You can render those citations in your UI as clickable proof. Open one and Brain returns the underlying transaction, invoice, or document.

### What "Continuously Updated" Means

| Trigger                         | Brain's response                                          |
| ------------------------------- | --------------------------------------------------------- |
| New transaction lands at a bank | Webhook arrives; record updates within seconds            |
| Invoice paid                    | Obligation closes; counterparty's payment history updates |
| Counterparty merged             | Duplicate entities resolve; relationships rewrite         |
| New month begins                | Rolling summaries regenerate                              |

There's no batch job you wait for. The memory is current.

### What "Tenant-Isolated" Means

Each tenant has its own logical record. Cross-tenant access is impossible by construction:

| Boundary          | How                                                                                                                                           |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **Storage**       | Per-tenant database partitions and Azure Blob prefixes                                                                                        |
| **Encryption**    | Source credentials use a single AES-256-GCM key loaded from Azure Key Vault in production or `BRAIN_SOURCE_CREDENTIAL_KEY` outside production |
| **Authorization** | Every API call carries a tenant; cross-tenant reads return 404                                                                                |

You'll never accidentally surface one customer's data in another customer's response.

### Memory Compounds

The longer Brain runs for a tenant, the better it gets:

| Time horizon     | What improves                                                 |
| ---------------- | ------------------------------------------------------------- |
| **First days**   | Counterparty profiles emerge; basic narrative anchors form    |
| **First months** | Rolling baselines mature; anomaly detection becomes possible  |
| **First year**   | Year-over-year comparisons unlock; vendor history is deep     |
| **Multi-year**   | Cross-period narratives are durable; switching costs are real |

### Where This Lives in the Protocol

If you want to look under the hood, memory is built from three of Brain's six layers:

| Layer      | Job                                              |
| ---------- | ------------------------------------------------ |
| **Raw**    | Lossless ingestion of the original evidence      |
| **Ledger** | Deterministic structuring into immutable records |
| **Wiki**   | Continuously regenerated human-readable memory   |

[**→ Protocol: Raw and Ledger**](/protocol/raw-and-ledger)

### Related

| Concept                                | Page   |
| -------------------------------------- | ------ |
| The rules that govern action on memory | Policy |
| Who can read memory                    | Agents |
| Proving Brain's claims                 | Proof  |


# Policy

The rules a tenant signed. How decisions are made.

Every action that touches a tenant's money runs through Policy. Policy is the rules a tenant has signed, expressed in plain English, compiled to deterministic checks, and evaluated on every proposed action.

### Three Possible Outcomes

| Outcome          | Meaning                                                                                         |
| ---------------- | ----------------------------------------------------------------------------------------------- |
| `auto`           | The action satisfies policy and may proceed only if the rail's hard floor also permits autonomy |
| `needs_approval` | The action is allowed but requires a human signature first                                      |
| `rejected`       | The action is not allowed; structured reason returned                                           |

There is no fourth outcome. There is no override. There is no bypass.

Policy `auto` is not the same as "money moves immediately" for every rail. A policy allow on `onchain_transfer`, `escrow_release`, or `wire` still requires a recorded human approval before dispatch. `x402_settle` can run autonomously only when the matched signed policy rule sets both `onchain_settlement_permitted: true` and a covering `x402_autonomous_max_amount`. ACH and card can run autonomously only when the matched signed policy rule carries a covering `ach_autonomous_max_amount` or `card_autonomous_max_amount`. Missing, malformed, wrong-currency, or over-cap values fail closed to human approval.

### Plain-English in, Deterministic Out

A tenant writes:

> Allow invoice payments under $5,000 to approved vendors. Require CFO approval above $5,000. Block payments to new counterparties without review.

Brain compiles to:

```json
[
  { "if": "amount < 5000 and counterparty.known", "then": "auto" },
  {
    "if": "amount >= 5000 and counterparty.known",
    "then": "needs_approval",
    "approvers": ["role:cfo"]
  },
  { "if": "!counterparty.known", "then": "rejected", "reason": "new_counterparty_review_required" }
]
```

The tenant signs the **compiled** form. The compiler also returns a human-readable explanation so the tenant can verify the rules match their intent before signing.

### Versioning

Policies are versioned. A new version supersedes the old one. Past actions remain bound to whichever version evaluated them, which means the audit log is reproducible: anyone can replay any past decision against the policy that was active at the time.

### Where the Rules Live

Two layers, by design:

| Layer                            | Catches                                                                  |
| -------------------------------- | ------------------------------------------------------------------------ |
| **Off-chain Policy engine**      | Most violations, fast feedback, dynamic conditions                       |
| **On-chain `BrainSmartAccount`** | Anything the off-chain layer missed; protects against backend compromise |

The on-chain layer is the belt and braces. Even if Brain's backend were fully compromised, an attacker still couldn't push through a payment that doesn't carry a valid, non-expired, scope-bound policy verdict.

[**→ Smart contracts: BrainSmartAccount**](/smart-contracts/brainsmartaccount)

### The Pre-Execution Gate

After Policy says `auto` (or after a human approves a `needs_approval`), one more check runs before money leaves: a deterministic gate (13 numbered checks + 10 hardening additions = 23 entries) that reads the **current** Ledger state. Account balance, counterparty status, idempotency, on-chain limits, obligation direction (payable vs receivable), audit-chain health.

Policy is the standing rule. The gate is the flight check. Both must pass.

[**→ Protocol: The pre-execution gate**](/protocol/the-pre-execution-gate)

### Why ESCALATE Is the Default

Any action that doesn't match a rule is **escalated for approval**, not auto-allowed and not silently rejected. This is intentional: new scenarios fail-safe, in front of a human, instead of silently going in either direction.

### What Policy Can Express

| Concept             | Example                                                                        |
| ------------------- | ------------------------------------------------------------------------------ |
| **Amounts**         | `under $5,000`, `between $1k and $50k`                                         |
| **Counterparties**  | `approved vendors`, `new counterparties`, `to employees`, `to tax authorities` |
| **Account state**   | `if balance is at least $50k`                                                  |
| **Time windows**    | `weekdays 9am-5pm Pacific`                                                     |
| **Approvals**       | `require approval from CFO and CEO`                                            |
| **Outright denial** | `block`, `do not allow`                                                        |

For edge cases that don't fit plain English, you can author rules directly in the structured grammar.

### Privacy

Only the policy hash goes on-chain. The text and compiled rules stay encrypted in the tenant's partition. Counterparties verifying a policy decision check that the hash referenced in the verdict matches a hash registered on-chain. They don't need (and don't get) the rules themselves.

### Related

| Concept                            | Page             |
| ---------------------------------- | ---------------- |
| What Policy reads                  | Memory           |
| Who is subject to Policy           | Agents           |
| The audit record of every decision | Proof            |
| Deep dive                          | Protocol: Policy |


# Agents

Who can read, propose, and act. Internal and external.

In Brain, an **agent** is any non-human caller that proposes or executes actions on a tenant's behalf. Agents and humans share the same authorization model. The only thing that differs is the credential.

| Caller                                    | Credential                                      |
| ----------------------------------------- | ----------------------------------------------- |
| **Human**                                 | Email + password, or a linked wallet (SIWX)     |
| **Internal agent** (your backend)         | Server API key (the `brain_sk_…` service token) |
| **External agent** (third-party software) | JWT, anchored to an on-chain registration       |

All three hit the same endpoints, run through Policy, and land in the Audit log.

### Internal vs External

You can use Brain in two ways: **build agents on top of it** or **let other people's agents in**.

| Pattern      | What it looks like                                                                          |
| ------------ | ------------------------------------------------------------------------------------------- |
| **Internal** | Your backend uses the SDK. Your code is the agent.                                          |
| **External** | Someone else's MCP-compatible agent connects to Brain. The tenant authorizes it explicitly. |

Most apps start with internal agents. External agents become useful when:

* Your tenant wants to use a specialist agent (a vendor-management bot, a treasury agent) you didn't build
* You're building a marketplace where tenants pick agents
* You're integrating a third-party assistant that should see a tenant's financial state

### How Agents Act

Whether internal or external, the lifecycle is the same:

```
1. Read context (memory, citations)
2. Propose an action
3. Brain runs Policy
4. If policy allows, the rail-specific hard floor decides whether it executes or routes to a human
5. Audit anchors what happened
```

An agent proposes; Brain decides. Agents do not bypass Policy, and policy allow is still subject to the money-rail floor. `onchain_transfer`, `escrow_release`, and `wire` require recorded human approval before dispatch. `x402_settle`, ACH, and card can run autonomously only under signed policy caps that cover the action.

### What External Agents Can Do

Tenant-granted scopes determine what an external agent sees and can do.

| Scope                    | Allows                                                                   |
| ------------------------ | ------------------------------------------------------------------------ |
| `ledger:read`            | Read transactions, balances, counterparties, obligations                 |
| `wiki:read`              | Ask natural-language questions; get cited answers                        |
| `raw:write`              | Push artifacts (transcripts, contracts) into the tenant's evidence layer |
| `payment_intent:propose` | Propose payments (cannot execute)                                        |
| `execution:propose`      | Propose non-financial actions                                            |

A tenant can grant any subset. Unused scopes don't appear in the agent's available tools.

{% hint style="warning" %}
External agents only ever **propose**; they never **execute**. Once an action is eligible (Policy returned `allow` and the rail permits autonomy, or all required human approvals are in), Brain's internal settlement path runs the §6 gate and dispatches it. The proposing agent never moves the money itself, and a human approval is a recorded member approval, not a settlement call. That separation is the safety guarantee that makes external agents safe to authorize.
{% endhint %}

### How External Agents Stay Accountable

Three properties combine to make external agents safe:

| Property       | Mechanism                                                                                    |
| -------------- | -------------------------------------------------------------------------------------------- |
| **Identity**   | Agent registered in `BrainMCPAgentRegistry` on Base; signs every JWT with its registered key |
| **Scope**      | Tenant signs an EIP-712 message granting specific scopes; the hash is anchored on-chain      |
| **Revocation** | Tenant can revoke at any time; new calls fail within 60 seconds                              |

If an agent goes rogue, you turn it off. The audit log shows exactly what it did, when, and under whose authorization.

### How Internal Agents Stay Accountable

Same audit log, same Policy gating. Your server keys are scoped (you can issue narrow keys per service), and every call carries the key fingerprint. Compromised keys can be revoked, and the trail of what they did before revocation is recoverable.

### What "MCP-Compatible" Means

[MCP](https://modelcontextprotocol.io) is the open standard for connecting agents to tools and data sources. Brain runs an MCP server at the canonical host `https://mcp.brain.fi` (which maps onto the internal `POST /v1/agents/mcp` route). Any agent built on an MCP-compatible runtime can connect.

You don't have to know any of this if you're only building internal agents. The MCP server matters when you want to **be a destination** for third-party agents.

[**→ MCP server**](/mcp-server/overview)

### Related

| Concept                            | Page             |
| ---------------------------------- | ---------------- |
| What agents read                   | Memory           |
| The rules that gate every action   | Policy           |
| The audit record of agent activity | Proof            |
| Deep dive                          | Protocol: Agents |


# Internal Agents

Brain-shipped agents are first-class participants, not a parallel system.

Brain ships a small set of its own agents (for example, collections, treasury, and reconciliation). These **internal agents** are not a separate mechanism. They register in the same registry, pass the same validation, and propose through the same path as any third-party agent. The only thing that distinguishes them is a metadata field and who operates the execution key.

## Three Kinds of Caller

| Kind            | Who builds it              | Provenance | Credential                         |
| --------------- | -------------------------- | ---------- | ---------------------------------- |
| **Internal**    | Brain ships it             | `internal` | Brain-operated execution key       |
| **First-party** | The customer's own backend | n/a        | Server API key                     |
| **External**    | A third party              | `external` | JWT anchored to an on-chain record |

"Internal" and "external" are values of the agent's `provenance` metadata (stored as the agent record's `kind`). "First-party" describes a customer backend calling Brain with its own API key; it is a usage pattern, not a registry entry.

## Same Registry, Same Validation

An internal agent is registered in `BrainMCPAgentRegistry` exactly like an external one: an `agentId`, an execution address, a per-tenant `scopeHash`, and a tenant-signed authorization. When an internal agent settles on-chain, it executes under the same `BrainSmartAccount` session-key model as any agent. The owner first `grantSessionKey`s the agent's execution address with a `policyVersion` bound at grant time and on-chain spend caps (per-tx and per-period). Each settlement then calls `executeViaSessionKey`, which enforces:

1. the session key is granted, not paused, and within its validity window,
2. the supplied nonce matches the per-holder replay nonce,
3. the call stays within the per-tx and per-period spend caps, and
4. the key's `policyVersion` matches what `BrainPolicyRegistry` returns.

The owner can `pauseSessionKey`/`unpauseSessionKey` or `revokeSessionKey` at any time. There is no `BrainNativeAgent` and no bypass. Capabilities are identified by `keccak256(name)` and fold into the agent's `scopeHash`, the same as for external agents.

## The Shared Pattern

Every internal agent is described by an **agent definition**: its capabilities, the events and intent patterns it responds to, the data it may read, its risk level, its minimum confidence, the evidence it requires, and its default authority. A handler turns a triggered action into a proposal. The agent never executes; it proposes through `POST /v1/agents/run`, which runs Policy and the deterministic pre-execution gate.

## Routing

A multi-agent router selects an agent for an incoming event or intent. It filters candidates by capability and by the tenant's scope grants, scores them by trigger match, intent match, evidence completeness, reputation, and cost, and returns the best agent plus fallbacks. The selection is itself an audit event, so a tenant can later verify why a particular agent was chosen. Routing only selects; the selected agent still proposes through the gated path.

## Upload-Driven Triggers

When an uploaded document is interpreted and projected into Ledger, the canonical projector emits one `ledger.upload.projected` event for the artifact. The event carries the tenant id, raw artifact id, and a summary of created records: transactions, receivables, obligations, accounts, and counterparties.

The API worker handles that event server-side and fans out only to the relevant internal agents:

| Projection summary                   | Agents considered          |
| ------------------------------------ | -------------------------- |
| Receivables created                  | Collections                |
| Transactions created                 | Cash Forecasting, Treasury |
| Counterparties created               | Vendor Risk                |
| Transactions plus tenant receivables | Reconciliation             |

This path is propose-only. It uses service identity, routes through the existing `AgentRunService`, persists `agent_runs`, creates ordinary proposals or informational agent actions, and emits agent audit events. API-key scopes are not involved because upload-triggered runs are internal server-side work. Each run is deduped by tenant, raw artifact id, and agent, so replaying the same projection does not create duplicate proposals.

## The Decision

The proposal decision stays `ALLOW`, `ESCALATE`, or `DENY`. Internal agents add three fields to that response without changing it: `confidence`, `evidence_score`, and an `execution_mode` of `execute`, `propose`, `confirm`, `notify_only`, or `reject`. Low confidence or missing required evidence yields `notify_only`: surface to a human, take no action. Existing callers who read `decision` are unaffected.

## The Business Agent Library

Brain ships a library of business-category internal agents. Every one follows the shared pattern above: a `keccak256` capability, a definition, a handler that only proposes, and a `policy.template.json` a tenant can adopt. None of them moves money outside `POST /v1/agents/run` and the pre-execution gate.

| Agent                    | Capability             | Risk   | Typical mode             |
| ------------------------ | ---------------------- | ------ | ------------------------ |
| **Collections**          | `collections_followup` | medium | propose                  |
| **Treasury**             | `treasury_sweep`       | medium | propose / confirm        |
| **Payment**              | `payment_propose`      | medium | confirm (financial)      |
| **Vendor Risk**          | `vendor_risk`          | high   | confirm / reject (block) |
| **Cash Forecasting**     | `cash_forecast`        | low    | notify\_only / propose   |
| **Dispute**              | `dispute_evidence`     | medium | propose                  |
| **Compliance**           | `compliance_monitor`   | high   | notify\_only / confirm   |
| **Revenue Intelligence** | `revenue_intel`        | low    | notify\_only             |

High-risk boundaries are agent-specific. Vendor Risk and Compliance have a confirm/reject ceiling. Fraud and Anomaly is notify-only by default; its consequential `freeze_card` action is explicit-request-only and never selected from a trigger.

## The Consumer Agent Library

Brain also ships consumer-category agents for individuals. They follow the same pattern, but their `policy.template.json` defaults are more conservative than the business templates: smaller per-action caps and `notify_only` as the default authority for any medium- or high-risk agent.

| Agent                 | Capability          | Risk   | Typical mode                 |
| --------------------- | ------------------- | ------ | ---------------------------- |
| **Personal Budget**   | `personal_budget`   | low    | propose                      |
| **Bill Management**   | `bill_management`   | medium | notify\_only                 |
| **Savings**           | `savings_sweep`     | low    | propose                      |
| **Debt Optimization** | `debt_optimization` | medium | notify\_only                 |
| **Tax Prep**          | `tax_prep`          | low    | propose                      |
| **Travel Finance**    | `travel_finance`    | low    | propose                      |
| **Financial Health**  | `financial_health`  | low    | notify\_only                 |
| **Purchase Advisor**  | `purchase_advisor`  | medium | notify\_only (intent-driven) |

Three internal agents are **agnostic** and serve business and consumer tenants alike: **Subscription** (`subscription_review`), **Reconciliation** (`reconciliation_review`), and **Fraud & Anomaly** (`fraud_anomaly`). The Subscription agent is shared, not duplicated: it ships a stricter `policy.consumer.template.json` for consumer tenants rather than a separate consumer agent.

## Category-Aware Routing

Some triggers are shared across categories: `cash.balance_high` matches both **Treasury** (business) and **Savings** (consumer); `bill.due_soon` matches both **Payment** (business) and **Bill Management** (consumer). The router resolves the tenant's category (business or consumer) and prefers the category-matching agent, so a business tenant routes `cash.balance_high` to Treasury and a consumer tenant routes it to Savings.

Category mismatch is a **scoring downgrade, not a hard reject**: a mismatched agent is penalized but can still win when it is the best (or only) match. So an explicit user intent ("help me save") can override the default category preference. Agnostic agents carry no penalty. When no tenant category is resolved, routing is category-blind and behaves exactly as in the earlier phases.

## Intent Classification

A request that carries a free-form intent (rather than a domain event) is scored against each agent's declared `intent_patterns`. Two classifier strategies share one interface, selected by the `AGENT_INTENT_CLASSIFIER` flag:

| Strategy      | Flag value        | How it matches                                                                                                                                      |
| ------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Rules**     | `rules` (default) | Token overlap with patterns. Deterministic, no dependencies; misses paraphrases.                                                                    |
| **Embedding** | `embedding`       | Cosine similarity between intent and pattern embeddings. Matches paraphrases; pattern embeddings are cached and reindexed when the catalog changes. |

The embedding strategy keeps the **rules classifier as a live fallback**: when an intent scores below the similarity threshold, or the embedding service is unavailable, the router falls back to token overlap. The two strategies are interchangeable behind the same interface, so routing and selection scoring are unchanged. Only the source of the intent-match score differs. With the flag off (the default), behavior is identical to the earlier phases.

## Autonomous Execution (Agent Autonomy)

The library is hardened for production autonomous execution, with money-movement off by default:

* **Shadow mode + graduated promotion.** Every agent is shadowed by default. A financial proposal terminates as `shadow_completed` and moves no money. Going live is a deliberate, per-agent promotion gated by strict caps (signed spend envelopes + `approval_required_above`) and an allowlisted rail. The five money-movers (Treasury, Payment, Bill Management, Savings, Debt Optimization) are promoted one at a time.
* **Action resolution.** Within a selected agent, the action is resolved explicit → event-map → intent-map → opt-in default; unresolved actions persist as `missing_action`, never a silent default. Money-movers have no default action. Vendor Risk and Compliance also omit one; Fraud and Anomaly may default only to its non-consequential `notify` action.
* **Behavior pinning.** Each agent registers a `behaviorHash`; the gate (check 1.5) rejects a runtime model/prompt/tool drift. Promotion to a new behavior needs tenant re-attestation.
* **High-risk agents** (Vendor Risk, Compliance) emit auditable **findings** before any block/confirm, with a tenant-root override-and-document path.
* **Counterparty-facing agents** (Collections, Dispute, Subscription) send only **tenant-approved message templates** from the signed policy doc. No free-form prose to customers/vendors.
* **Observability.** Every run persists a structured reason and trace; `GET /v1/agents/runs/{id}/why` returns the full reason + gate trace + rail receipt.

See the API reference for the `/v1/agents/run`, `/why`, and kill-switch endpoints.

## Related

| Topic                          | Page                                                            |
| ------------------------------ | --------------------------------------------------------------- |
| The shared authorization model | [Agents](/concepts/agents)                                      |
| How agent actions are gated    | [Policy](/concepts/policy)                                      |
| The agent layer in depth       | [Protocol: Agents](/protocol/agents)                            |
| The on-chain identity contract | [BrainMCPAgentRegistry](/smart-contracts/brainmcpagentregistry) |
| Portable agent skill recipes   | [Use Brain Agent Skills](/build/use-brain-agent-skills)         |


# Proof

Why every claim Brain makes is verifiable.

Every meaningful event Brain records is hashed, chained, and periodically anchored on Base Sepolia. Brain's API and off-chain services are production available. The on-chain contract is unaudited, Base Sepolia only, and not deployed on Base mainnet. A counterparty, auditor, or end user can verify that a specific event happened, at a specific time, with a specific decision, **without trusting Brain**.

### Two Layers of Proof

| Layer              | What it proves                                                                        |
| ------------------ | ------------------------------------------------------------------------------------- |
| **Citations**      | The data behind any answer or decision (transactions, invoices, evidence)             |
| **Merkle anchors** | That the event itself happened, in the order Brain says, with the metadata Brain says |

Citations make claims traceable inside Brain. Anchors make Brain's claims independently verifiable outside Brain.

### What Gets Logged

Every material state change emits an audit event:

| Type                  | When                                  |
| --------------------- | ------------------------------------- |
| `source.connected`    | A source connected for the tenant     |
| `transaction.created` | A new transaction landed              |
| `wiki.query`          | A natural-language question was asked |
| `policy.evaluated`    | A policy decision was rendered        |
| `action.proposed`     | An agent proposed an action           |
| `action.approved`     | A human signed approval               |
| `action.executed`     | An action settled on its rail         |
| `audit.anchored`      | A Merkle root was anchored on Base    |

Read endpoints (Wiki queries, Ledger reads) also land in the log. Anyone reviewing the trail can see exactly what was read, by whom, when.

### Tamper-Evidence

Each event is hashed deterministically. Each event references the previous event's hash. The result is a per-tenant hash chain.

```
event_n.prev_hash = hash(event_{n-1})
```

To rewrite history, you'd have to regenerate every subsequent hash. And you'd still have to fool the Merkle anchor on Base.

### On-Chain Anchors

Brain batches audit events into a Merkle tree per tenant and submits roots on the configured publisher cycle. The default interval is one hour, but an event is anchored only when its audit status records a confirmed on-chain transaction. There is no severity-accelerated anchoring path. Once published, a tenant-root pair cannot be published again.

```typescript
const proof = await brain.proof(actionId);

proof.merklePath; // sibling hashes from leaf to root
proof.anchorRoot; // the Merkle root anchored on Base
proof.anchorTx; // the transaction that anchored it
```

A counterparty verifies on-chain by checking `BrainAuditAnchor.isPublished(tenantId, root)` and calling `BrainAuditAnchor.verifyInclusion(root, leaf, proof)`. `latestAnchor(tenantId)` returns the most recently published root for a tenant. They do not need a Brain account, an API key, or access to the underlying data.

### What's on-Chain vs Off-Chain

| On-chain                       | Off-chain                           |
| ------------------------------ | ----------------------------------- |
| Hashed `tenant_id`             | Tenant's actual id                  |
| Merkle roots                   | Individual events                   |
| Published roots and block data | Event content, citations, decisions |
| Publisher transaction address  | Audit event signatures              |

The on-chain footprint is intentionally minimal. The hash commits to history without revealing anything.

### Privacy Properties

| Concern                             | How Brain handles                                                          |
| ----------------------------------- | -------------------------------------------------------------------------- |
| Counterparty learns tenant identity | Tenant ID is hashed before storage                                         |
| Counterparty learns event content   | Events are off-chain; only hashes anchor                                   |
| Anchor publisher compromise         | Only the configured publisher can write; root uniqueness prevents replay   |
| Reorg drops an anchor               | Pending anchors are retried; off-chain status is canonical until confirmed |

### Why "Anchored on-Chain" Matters

Most audit logs in fintech are SOC 2 documents and SQL exports. They prove that the vendor cared. They don't prove that the events happened as described.

An on-chain anchor is the difference between **trust** and **verify**. Even if Brain disappeared tomorrow, the on-chain record would still be queryable on Base, and any party with a Merkle proof could prove what happened.

### Where This Lives in the Protocol

The proof story is the Audit layer (Layer 6) plus the six deployed protocol contracts:

| Contract                  | Job                                                                  |
| ------------------------- | -------------------------------------------------------------------- |
| `BrainAuditAnchor`        | Anchors Merkle roots per tenant                                      |
| `BrainPolicyRegistry`     | Anchors policy version hashes per tenant                             |
| `BrainSmartAccount`       | Directly called session-key account enforcing scope, caps, and nonce |
| `BrainMCPAgentRegistry`   | Anchors external-agent scope and behavior hashes                     |
| `BrainEscrow`             | Testnet conditional escrow locks                                     |
| `BrainReputationRegistry` | Publishes reputation roots; scoring remains placeholder              |

[**→ Smart contracts overview**](/smart-contracts/overview)

### Related

| Concept                             | Page                      |
| ----------------------------------- | ------------------------- |
| The data that backs every answer    | Memory                    |
| The decisions captured in the trail | Policy                    |
| Who acts and gets logged            | Agents                    |
| Deep dive                           | Protocol: Audit and Proof |


# Overview

You don't need to read this section to build with Brain. It's here for the moments when you do: compliance review, custom policy design, on-chain audit, agent autonomy work, or just because you want to understand what's happening underneath.

### The Six-Layer Stack

```
Raw → Ledger → Wiki → Policy → Agent → Audit
```

Information flows up. Control flows down. Each tenant has its own logical instance of every layer, with hard isolation at the database, key, and policy boundaries.

| Layer         | Owns                                        | Concept page             |
| ------------- | ------------------------------------------- | ------------------------ |
| **1. Raw**    | Source evidence, immutable                  | Raw and Ledger           |
| **2. Ledger** | Machine-readable financial truth            | Raw and Ledger           |
| **3. Wiki**   | Human-readable financial memory             | The Wiki                 |
| **4. Policy** | Deterministic permission and approval logic | Policy and Permissioning |
| **5. Agent**  | Proposal and orchestration                  | Agents                   |
| **6. Audit**  | Immutable proof of what happened and why    | Audit and Proof          |

[**→ Six-layer overview**](/protocol/the-six-layer-stack)

### What's in This Section

<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>The Six-Layer Stack</strong></td><td>The whole protocol on one page.</td><td><a href="/pages/P1sKkvfj7oBfLCRzdXv4">/pages/P1sKkvfj7oBfLCRzdXv4</a></td><td></td></tr><tr><td><strong>Raw and Ledger</strong></td><td>How evidence becomes deterministic structure.</td><td><a href="/pages/pPTXzUZ6cZ8LCgvMmMRO">/pages/pPTXzUZ6cZ8LCgvMmMRO</a></td><td></td></tr><tr><td><strong>The Wiki</strong></td><td>The continuously regenerated memory layer.</td><td><a href="/pages/IDbmiD3RRs6QlgR8UT8z">/pages/IDbmiD3RRs6QlgR8UT8z</a></td><td></td></tr><tr><td><strong>Policy and Permissioning</strong></td><td>Plain-English rules to deterministic guards.</td><td><a href="/pages/GSe2ntE9CLqxoQAiQuzG">/pages/GSe2ntE9CLqxoQAiQuzG</a></td><td></td></tr><tr><td><strong>Agents</strong></td><td>Internal and external agents in the protocol.</td><td><a href="/pages/t9xBO8sSjFElp8ecB6i5">/pages/t9xBO8sSjFElp8ecB6i5</a></td><td></td></tr><tr><td><strong>Payment Intents</strong></td><td>The Ledger entity that represents a proposed action.</td><td><a href="/pages/frJ0ygywHJq5WCmjwuXH">/pages/frJ0ygywHJq5WCmjwuXH</a></td><td></td></tr><tr><td><strong>The Pre-Execution Gate</strong></td><td>The 13 numbered checks + 10 hardening additions (23 total) before any payment.</td><td><a href="/pages/GcCCOqv3BXHFtEpuypqD">/pages/GcCCOqv3BXHFtEpuypqD</a></td><td></td></tr><tr><td><strong>Audit and Proof</strong></td><td>Tamper-evident history anchored on Base L2.</td><td><a href="/pages/PIgNXssgtEUZDLnC4b4d">/pages/PIgNXssgtEUZDLnC4b4d</a></td><td></td></tr><tr><td><strong>Agent Contributions</strong></td><td>How external agents contribute evidence safely.</td><td><a href="/pages/rXMm84kpjeJ8SEHuOWBP">/pages/rXMm84kpjeJ8SEHuOWBP</a></td><td></td></tr></tbody></table>

### Architecture Deep Dives

<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>System Overview</strong></td><td>The architecture top-down.</td><td><a href="/pages/0faef97530c1f0291979e9bc4239668ed769f3a9">/pages/0faef97530c1f0291979e9bc4239668ed769f3a9</a></td><td></td></tr><tr><td><strong>Data Flow</strong></td><td>How a single source-of-truth event ripples up the stack.</td><td><a href="/pages/f13dc46dcb9951f17a7148e3db84880b35c5a671">/pages/f13dc46dcb9951f17a7148e3db84880b35c5a671</a></td><td></td></tr><tr><td><strong>Write Paths</strong></td><td>The two controlled exceptions to bottom-up flow.</td><td><a href="/pages/ahIJrDMqEknY2O7lB9az">/pages/ahIJrDMqEknY2O7lB9az</a></td><td></td></tr><tr><td><strong>Tenant Isolation</strong></td><td>Per-tenant boundaries, end to end.</td><td><a href="/pages/6653bed462d23f79b20417161f16b62ffd97ed9c">/pages/6653bed462d23f79b20417161f16b62ffd97ed9c</a></td><td></td></tr><tr><td><strong>Security and Compliance</strong></td><td>Crypto, keys, sanctions, SOC 2 trajectory.</td><td><a href="/pages/02a6fa38c72a26d3f8a40cbaad4ac854d7353a1e">/pages/02a6fa38c72a26d3f8a40cbaad4ac854d7353a1e</a></td><td></td></tr><tr><td><strong>Risks and Mitigations</strong></td><td>Where things can go wrong, and what catches them.</td><td><a href="/pages/BBebQzrRNDafNKFzHG4I">/pages/BBebQzrRNDafNKFzHG4I</a></td><td></td></tr></tbody></table>

### Where the Protocol Meets the Chain

The on-chain surface is intentionally small. Most logic stays off-chain. Four smart contracts on Base L2 anchor the parts that have to be public and tamper-evident.

| Contract                | Anchors                                                                                                   | Page                  |
| ----------------------- | --------------------------------------------------------------------------------------------------------- | --------------------- |
| `BrainAuditAnchor`      | Audit Merkle roots per tenant                                                                             | BrainAuditAnchor      |
| `BrainPolicyRegistry`   | Policy version hashes per tenant                                                                          | BrainPolicyRegistry   |
| `BrainSmartAccount`     | Session-key account enforcing scope, spend caps, and the bound `policyVersion` via `executeViaSessionKey` | BrainSmartAccount     |
| `BrainMCPAgentRegistry` | Agent identity, capabilities, scope grants                                                                | BrainMCPAgentRegistry |

### Where to Start

| If you want to understand...              | Start here                  |
| ----------------------------------------- | --------------------------- |
| The whole stack                           | Six-Layer Stack             |
| Why memory and policy are separate layers | Raw and Ledger              |
| How decisions stay safe                   | Pre-Execution Gate          |
| Why this is verifiable                    | Audit and Proof             |
| What external agents can and can't do     | Agents, Agent Contributions |


# The Six-Layer Stack

Brain is a layered protocol. Information flows up; control flows down.

```
Raw → Ledger → Wiki → Policy → Agent → Audit
```

Each tenant has its own logical instance of every layer, with hard isolation at the database, KMS, and policy boundaries. Off-chain state lives in Postgres with pgvector and Azure Blob-backed raw artifacts. On-chain commitments and smart-account execution live on Base L2.

### The Six Layers at a Glance

<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>1️⃣ Raw</strong></td><td>Lossless ingestion of evidence from any authorized source.</td><td><a href="/pages/pPTXzUZ6cZ8LCgvMmMRO">/pages/pPTXzUZ6cZ8LCgvMmMRO</a></td><td></td></tr><tr><td><strong>2️⃣ Ledger</strong></td><td>Deterministic structuring into immutable records with provenance.</td><td><a href="/pages/pPTXzUZ6cZ8LCgvMmMRO">/pages/pPTXzUZ6cZ8LCgvMmMRO</a></td><td></td></tr><tr><td><strong>3️⃣ Wiki</strong></td><td>Continuously updated memory graph per tenant.</td><td><a href="/pages/IDbmiD3RRs6QlgR8UT8z">/pages/IDbmiD3RRs6QlgR8UT8z</a></td><td></td></tr><tr><td><strong>4️⃣ Policy</strong></td><td>Plain-English rules compiled to deterministic guards.</td><td><a href="/pages/GSe2ntE9CLqxoQAiQuzG">/pages/GSe2ntE9CLqxoQAiQuzG</a></td><td></td></tr><tr><td><strong>5️⃣ Agent</strong></td><td>Internal and external agents executing within policy.</td><td><a href="/pages/t9xBO8sSjFElp8ecB6i5">/pages/t9xBO8sSjFElp8ecB6i5</a></td><td></td></tr><tr><td><strong>6️⃣ Audit</strong></td><td>Per-tenant Merkle tree anchored on Base L2.</td><td><a href="/pages/PIgNXssgtEUZDLnC4b4d">/pages/PIgNXssgtEUZDLnC4b4d</a></td><td></td></tr></tbody></table>

### Why Six Layers, in This Order

The stack is not a stylistic choice. Each layer enforces a property the layer above it requires.

| Layer      | What It Enforces                               | Why It Matters                                                             |
| ---------- | ---------------------------------------------- | -------------------------------------------------------------------------- |
| **Raw**    | Lossless, replayable storage                   | Higher layers can be rebuilt deterministically if extraction logic changes |
| **Ledger** | Deterministic structure with provenance        | Reasoning never reinterprets raw documents on the fly                      |
| **Wiki**   | Continuously refreshed memory linked to Ledger | Answers compound over time; citations are always traceable                 |
| **Policy** | Tenant-signed deterministic rules              | No agent action runs unchecked                                             |
| **Agent**  | Scoped, attestable execution                   | Internal and external agents share one verified substrate                  |
| **Audit**  | Hash-chained, Merkle-anchored events           | History cannot be silently rewritten                                       |

{% hint style="success" %}
This is the same separation that exists in any serious system between the database and the cache: structure first, reasoning second, memory bound to citations.
{% endhint %}

### Information Flow

Information flows **up**. Each layer enriches the one below.

```
Source webhook
   ↓
[ Raw ]            artifact stored, content-addressed by SHA-256
   ↓
[ Ledger ]         deterministic extractor produces structured records
   ↓
[ Wiki ]           entity resolution, narrative summarization, embeddings
   ↓
[ Policy ]         action evaluated against active policy version
   ↓
[ Agent ]          execution dispatched to off-chain rail or on-chain account
   ↓
[ Audit ]          every step hashed, Merkle root anchored on Base
```

### Control Flow

Control flows **down**. Higher layers gate lower ones.

| Layer Above | Gates                    | Layer Below                    |
| ----------- | ------------------------ | ------------------------------ |
| **Audit**   | requires hash links from | every other layer              |
| **Agent**   | requires verdict from    | Policy                         |
| **Policy**  | reads from               | Ledger only (never Wiki)       |
| **Wiki**    | rebuilds from            | Ledger                         |
| **Ledger**  | replays from             | Raw                            |
| **Raw**     | sources from             | the tenant's connected systems |

### Off-Chain and on-Chain Split

Most logic is off-chain by design. On-chain contracts exist to anchor state, register identity, enforce session-key scope/limits, and route agent execution.

| Tier                   | What Lives Here                                                                                                         |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Off-chain**          | Raw artifacts (Azure Blob), Ledger records (Postgres), Wiki graph (Postgres + pgvector), Policy compiler, Agent runtime |
| **On-chain (Base L2)** | `BrainAuditAnchor`, `BrainPolicyRegistry`, `BrainSmartAccount`, `BrainMCPAgentRegistry`                                 |

[**→ Smart contract reference**](/smart-contracts/overview)

### 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>📥 Raw and Ledger</strong></td><td>How evidence becomes deterministic structure.</td><td><a href="/pages/pPTXzUZ6cZ8LCgvMmMRO">/pages/pPTXzUZ6cZ8LCgvMmMRO</a></td><td></td></tr><tr><td><strong>🧠 The Wiki</strong></td><td>The memory graph per tenant.</td><td><a href="/pages/IDbmiD3RRs6QlgR8UT8z">/pages/IDbmiD3RRs6QlgR8UT8z</a></td><td></td></tr><tr><td><strong>📋 Policy and Permissioning</strong></td><td>Plain-English rules to deterministic guards.</td><td><a href="/pages/GSe2ntE9CLqxoQAiQuzG">/pages/GSe2ntE9CLqxoQAiQuzG</a></td><td></td></tr></tbody></table>


# Raw and Ledger

The bottom two layers of the stack do one job together: turn messy financial evidence into deterministic, structured records with provenance back to source.

### Raw Layer

The Raw Layer ingests financial evidence verbatim from authorized sources.

#### Sources

| Category                      | Examples                                              |
| ----------------------------- | ----------------------------------------------------- |
| **Banks and processors**      | Plaid, direct bank APIs, Stripe, Adyen                |
| **Custodians and brokerages** | Brokerage feeds, custodian APIs                       |
| **On-chain**                  | Wallets via Alchemy, contract event streams           |
| **ERPs**                      | NetSuite, SAP, Dynamics                               |
| **Accounting platforms**      | QuickBooks, Xero                                      |
| **Payroll**                   | Major payroll providers                               |
| **Documents**                 | Email-attached invoices and receipts, CSV/PDF uploads |

#### Storage Rules

Artifacts are content-addressed by SHA-256 and stored under tenant-prefixed Azure Blob paths. Source credentials, not every raw artifact, are encrypted at the application boundary with the global AES-256-GCM source-credential key from `shared/src/crypto/credential-key-provider.ts`.

| Property       | Value                                                               |
| -------------- | ------------------------------------------------------------------- |
| **Identifier** | `sha256:<hex>` over canonical bytes                                 |
| **Encryption** | Source credentials use AES-256-GCM with a global key today          |
| **Storage**    | Azure Blob with versioning, tenant prefixes, and lifecycle policies |
| **Retention**  | Per-tenant, configurable per source                                 |

{% hint style="info" %}
**Nothing is interpreted at this layer.** The Raw Layer's only job is to be a lossless, replayable record. If the extraction logic changes, every higher layer can be rebuilt deterministically from Raw.
{% endhint %}

### Ledger Layer

The Ledger Layer normalizes raw evidence into standard linkable objects.

#### Record Types

| Type                       | Purpose                                     |
| -------------------------- | ------------------------------------------- |
| `transactions`             | Money movements between accounts            |
| `balances`                 | Point-in-time and rolling balances          |
| `accounts`                 | Tenant-side and counterparty accounts       |
| `counterparties`           | Vendors, customers, employees               |
| `invoices`                 | Billed amounts, due dates, line items       |
| `obligations`              | Subscriptions, recurring charges, contracts |
| `cash_flows`               | Aggregations and forecasts                  |
| `assets` and `liabilities` | Holdings and debts                          |
| `permissions`              | Authorizations affecting the Ledger         |
| `events`                   | Lifecycle events tied to records            |

#### Post-Projection Agent Routing

For uploaded financial documents, projection into Ledger also emits a single artifact-level `ledger.upload.projected` event. The event is not per row. It summarizes what the artifact created, including transaction, receivable, obligation, account, and counterparty counts.

The API worker routes that event to the internal-agent fleet through the normal agent run machinery. Collections can respond to receivables, Cash Forecasting and Treasury can respond to transactions, Vendor Risk can respond to new counterparties, and Reconciliation can respond when uploaded transactions and tenant receivables coexist. The run remains propose-only: it may create reviewable proposals or informational agent actions, but it does not execute payments or bypass Policy.

Each upload-triggered run is idempotent by tenant, raw artifact id, and agent. Reprojecting the same artifact can refresh Ledger state, but it does not spawn duplicate agent proposals.

#### Provenance on Every Record

Every Ledger record carries:

| Field               | What It Contains                             |
| ------------------- | -------------------------------------------- |
| `raw_refs`          | The Raw artifact hashes that produced it     |
| `extractor_version` | The deterministic extractor that produced it |
| `confidence`        | A calibrated score from 0 to 1               |
| `supersedes`        | Optional pointer to the record this corrects |

#### Immutability

Records are immutable and append-only. Corrections are written as superseding records that reference what they correct.

```
record_v1: { id: "tx_001", amount: 1234.56, supersedes: null }
record_v2: { id: "tx_002", amount: 1234.65, supersedes: "tx_001" }
```

The history is preserved. Any reader can replay the chain to see how the value evolved.

### Why Ledger Sits Between Raw and Wiki

LLMs are excellent at language and pattern recognition and unreliable at arithmetic, deduplication, and reconciliation.

| Concern                                   | Right Place                        |
| ----------------------------------------- | ---------------------------------- |
| Arithmetic, deduplication, reconciliation | Ledger (deterministic)             |
| Fluent reasoning, narrative answers       | Wiki (LLM-driven, citation-backed) |

The Wiki is the place for fluent reasoning, not the source of financial truth. Ledger enforces a deterministic, machine-verifiable structure first, so Wiki always reasons over verified facts rather than reinterpreting raw documents on every query.

{% hint style="success" %}
This is the same separation that exists in any serious system between the database and the cache. Brain's Ledger is the database; the Wiki is the reasoning surface that points back to it.
{% endhint %}

### 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>The Wiki</strong></td><td>The memory graph that reasons over the Ledger.</td><td><a href="/pages/IDbmiD3RRs6QlgR8UT8z">/pages/IDbmiD3RRs6QlgR8UT8z</a></td><td></td></tr><tr><td><strong>Audit and Proof</strong></td><td>How every Ledger change gets a verifiable history.</td><td><a href="/pages/PIgNXssgtEUZDLnC4b4d">/pages/PIgNXssgtEUZDLnC4b4d</a></td><td></td></tr><tr><td><strong>Sources API</strong></td><td>Connect a source through the API.</td><td><a href="/pages/EQ4MJytikUzDXJfFnfp5">/pages/EQ4MJytikUzDXJfFnfp5</a></td><td></td></tr></tbody></table>


# The Wiki

The Wiki is a **continuously updated structured memory per tenant**. Not a vector store with documents in it. A graph of entities, relationships, narratives, and rolling summaries, linked back to Ledger and Raw.

{% hint style="info" %}
The Wiki is what makes Brain compound. The longer it runs for a tenant, the deeper the memory and the lower the marginal cost per query.
{% endhint %}

### What Lives in the Wiki

| Element               | Examples                                                                  |
| --------------------- | ------------------------------------------------------------------------- |
| **Entities**          | Counterparties, accounts, products, contracts, employees                  |
| **Relationships**     | "Vendor X invoices Cost Center Y", "Account A funds Subsidiary B"         |
| **Narratives**        | "Q3 receivables held flat versus Q2 despite revenue growth, driven by..." |
| **Rolling summaries** | Week-over-week, month-over-month, quarter-over-quarter snapshots          |
| **Embeddings**        | pgvector embeddings indexed for semantic retrieval                        |

### What the Wiki Answers

The Wiki is built to answer the kinds of questions only memory can answer.

| Example Question                            | Why Memory Is Required                 |
| ------------------------------------------- | -------------------------------------- |
| "Who is this counterparty?"                 | Requires accumulated entity knowledge  |
| "What is our normal monthly burn?"          | Requires rolling baselines             |
| "Have we paid this vendor before?"          | Requires historical lookups            |
| "What changed in receivables this quarter?" | Requires diff against prior periods    |
| "Is this subscription one we still use?"    | Requires usage and recurrence tracking |

### Citations on Every Answer

Every answer carries citations into the Ledger and Raw. Any claim is traceable back to source evidence.

```typescript
const answer = await brain.wiki.question({
  tenantId: "acme",
  question: "What did we spend on AWS last quarter, by environment?",
});

// answer.text         → fluent natural-language response
// answer.citations[]  → [{ ledger_id, raw_refs: [...] }, ...]
// answer.audit_event_id → the audit event under which this query was logged
```

{% hint style="success" %}
You never have to trust the Wiki blindly. Every claim links back to the Ledger records and Raw artifacts that produced it.
{% endhint %}

### How the Wiki Updates

The Wiki updates **incrementally** as new Ledger records arrive.

| Trigger                   | Wiki Action                                                              |
| ------------------------- | ------------------------------------------------------------------------ |
| New transaction in Ledger | Update counterparty profile, refresh rolling balance, re-embed narrative |
| Counterparty merge        | Resolve duplicate entities, rewrite relationship edges                   |
| Invoice paid              | Close the matching obligation; update vendor history                     |
| Period boundary           | Generate rolling summary; index for retrieval                            |

### Why Not Just a Vector Store

Vector stores retrieve documents. The Wiki retrieves a graph of verified entities with citations.

| Vector store                        | Wiki                                    |
| ----------------------------------- | --------------------------------------- |
| Returns chunks of documents         | Returns entities and relationships      |
| No native citations to source       | Every node links to Ledger and Raw      |
| Updates by re-embedding             | Updates incrementally as Ledger changes |
| No notion of correction             | Supersession propagates from Ledger     |
| Reasoning hallucinated on retrieval | Reasoning bounded by structured facts   |

### Compounding Effect

Brain's Wiki gets cheaper to query and richer to read the longer it runs.

| Time Horizon     | What Compounds                                                   |
| ---------------- | ---------------------------------------------------------------- |
| **First weeks**  | Entity resolution stabilizes; counterparty profiles emerge       |
| **First months** | Rolling baselines mature; anomaly detection becomes possible     |
| **First year**   | Year-over-year comparisons unlock; vendor history is deep        |
| **Multi-year**   | Cross-period narratives become durable; switching costs are high |

### 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>📥 Raw and Ledger</strong></td><td>The verified substrate underneath the Wiki.</td><td><a href="/pages/pPTXzUZ6cZ8LCgvMmMRO">/pages/pPTXzUZ6cZ8LCgvMmMRO</a></td><td></td></tr><tr><td><strong>📋 Policy</strong></td><td>How Wiki context informs policy decisions.</td><td><a href="/pages/GSe2ntE9CLqxoQAiQuzG">/pages/GSe2ntE9CLqxoQAiQuzG</a></td><td></td></tr><tr><td><strong>🛠️ Wiki SDK</strong></td><td>Programmatic queries.</td><td><a href="/pages/m25lyIz3DPEyKYuR5mU1">/pages/m25lyIz3DPEyKYuR5mU1</a></td><td></td></tr></tbody></table>


# Policy and Permissioning

Tenants describe policy in **plain English**. The Policy compiler converts each policy into a deterministic guard expression that is evaluated for every proposed action. Policies are versioned and signed by the tenant via EIP-712, with hashes anchored on-chain through `BrainPolicyRegistry`.

### Plain English in, Deterministic Guard Out

You write the policy in natural language. Brain compiles it. You sign the compiled form, not the prose.

```
Allow invoice payments under $5,000 to approved vendors,
require approval above $5,000,
and block payments to new counterparties without review.
```

Compiles to:

```json
{
  "subject": { "agent_capability": "pay_invoice" },
  "resource": { "counterparty.status": ["approved"] },
  "rules": [
    { "if": "amount < 5000 && counterparty.known", "then": "allow" },
    { "if": "amount >= 5000 && counterparty.known", "then": "confirm", "approvers": ["role:cfo"] },
    { "if": "!counterparty.known", "then": "reject", "reason": "new_counterparty_review_required" }
  ]
}
```

{% hint style="warning" %}
The compiler emits both the deterministic compiled policy **and** a human-readable explanation of what it will do. **Tenants sign the compiled form, not the prose.** This eliminates ambiguity at the moment of signing.
{% endhint %}

### The Five Elements of a Policy

Every policy has five elements.

| Element        | What It Defines                                                              |
| -------------- | ---------------------------------------------------------------------------- |
| **Subjects**   | Which agents, capabilities, or roles the policy applies to                   |
| **Resources**  | Which accounts, counterparties, asset classes, or jurisdictions are in scope |
| **Actions**    | What is permitted: read, propose, execute, approve                           |
| **Conditions** | Thresholds, time windows, frequency caps, required approvers                 |
| **Outcomes**   | `allow`, `reject`, or `confirm`                                              |

### The Three Outcomes

Every policy evaluation produces exactly one of three outcomes.

<table data-view="cards"><thead><tr><th></th><th></th></tr></thead><tbody><tr><td><strong>✅ allow</strong></td><td>The action proceeds. A signed policy verdict is attached to the resulting session-key call via `executeViaSessionKey` or rail call.</td></tr><tr><td><strong>⚠️ confirm</strong></td><td>Human approval is required before the action can execute. The verdict names the required approvers (e.g. <code>role:cfo</code>).</td></tr><tr><td><strong>❌ reject</strong></td><td>The action is blocked. The verdict carries a structured reason (e.g. <code>new_counterparty_review_required</code>).</td></tr></tbody></table>

{% hint style="info" %}
**`confirm` is the default for unmatched conditions.** If the policy compiler cannot determine a clear `allow` or `reject` for a proposed action, the safe default is to require human review. Failure modes are explicit, not silent.
{% endhint %}

### Worked Example: the $7,800 Invoice

A walkthrough of the policy from the top of this page, applied to a real proposal:

| Step | What Happens                                                                                                                       |
| ---- | ---------------------------------------------------------------------------------------------------------------------------------- |
| 1    | Agent proposes: pay $7,800 invoice to Vendor X                                                                                     |
| 2    | Policy Layer evaluates against version `v3` of the tenant policy                                                                   |
| 3    | Counterparty Vendor X: known, status = approved                                                                                    |
| 4    | Amount $7,800: above $5,000 threshold                                                                                              |
| 5    | Outcome: `confirm`, approvers = `[role:cfo]`                                                                                       |
| 6    | CFO receives the request with Wiki context (vendor history, prior payments) and Ledger references (invoice, PO)                    |
| 7    | CFO approves. EIP-712 approval signature recorded                                                                                  |
| 8    | Action moves to executable. `BrainSmartAccount.executeViaSessionKey` dispatches the on-chain call OR a bank API call is dispatched |
| 9    | Audit Layer records: proposal, policy decision, approver identity, execution receipt, settlement confirmation, all linked by hash  |

### Versioning, Signing, and Anchoring

Every policy version has a lifecycle.

```
draft → compile → review → sign (EIP-712) → anchor on-chain → active
```

| Phase       | What Happens                                                        |
| ----------- | ------------------------------------------------------------------- |
| **Draft**   | Plain-English text written in the Console or via API                |
| **Compile** | Compiler produces deterministic JSON + a human-readable explanation |
| **Review**  | Tenant reviews the compiled form                                    |
| **Sign**    | Tenant signs the canonical hash via EIP-712 `PolicyRegistration`    |
| **Anchor**  | Hash is registered in `BrainPolicyRegistry` on Base L2              |
| **Active**  | The policy version is active until superseded or revoked            |

The signed structure:

```
PolicyRegistration(
  bytes32 tenantId,
  uint64  version,
  bytes32 policyHash,
  uint64  notBefore,
  uint64  notAfter,
  uint256 nonce
)
```

[**→ Smart contract reference**](/smart-contracts/overview)

### How Policy Enforcement Is Layered

Policy is enforced **twice** by design.

| Level                             | What It Catches                                                                                                                                                                                                                                                         |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Off-chain** Brain Policy Engine | Most evaluations, fast feedback, dynamic conditions, rich error messages                                                                                                                                                                                                |
| **On-chain** `BrainSmartAccount`  | The session key is bound to the active `policyVersion` at grant time and its scope + spend caps are enforced inside `executeViaSessionKey`. Any action outside the granted key's bounds is rejected at the account level, regardless of what the off-chain engine said. |

{% hint style="success" %}
Belt and braces. Even if the off-chain engine were compromised, the on-chain account would still reject any call outside the granted session key's policyVersion-bound scope and spend caps.
{% endhint %}

Each session key is bound to the active `policyVersion` at grant time, and every `executeViaSessionKey` call carries a single-use replay nonce, so a call cannot be replayed against a different action.

### 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>🤖 Agents</strong></td><td>How agents propose actions and receive scope grants.</td><td><a href="/pages/t9xBO8sSjFElp8ecB6i5">/pages/t9xBO8sSjFElp8ecB6i5</a></td><td></td></tr><tr><td><strong>📜 Audit and Proof</strong></td><td>How every policy decision is captured.</td><td><a href="/pages/PIgNXssgtEUZDLnC4b4d">/pages/PIgNXssgtEUZDLnC4b4d</a></td><td></td></tr><tr><td><strong>📜 BrainPolicyRegistry</strong></td><td>The on-chain anchor.</td><td><a href="/pages/Qk74oUUATzrLis1xhGcb">/pages/Qk74oUUATzrLis1xhGcb</a></td><td></td></tr></tbody></table>


# Agents

The Agent Layer coordinates **internal and external agents**. Brain ships a small set of internal agents (payments, reconciliation, reporting). The layer is also an open registry. External agents authenticate via SIWX, advertise capabilities, and execute through `BrainSmartAccount`.

{% hint style="info" %}
Brain does not need to build every agent. **It is the substrate they share.** External agents listed on Brain do not need to ship their own ledger, memory, policy engine, or audit pipeline.
{% endhint %}

{% hint style="info" %}
**Internal agents are first-class participants of this same layer.** A Brain-shipped agent registers in the same `BrainMCPAgentRegistry`, carries the same per-tenant scope grant, and executes under the same `BrainSmartAccount` session-key model as any external agent. The only difference is a `provenance: "internal"` metadata field and that Brain operates the execution key. There is no separate native-agent path. See [Internal agents](/concepts/internal-agents).
{% endhint %}

### Agents Are First-Class Entities

Each agent has four attributes registered on-chain or referenced from on-chain.

| Attribute              | What It Is                                                                                                                                            |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Identity**           | A `BrainMCPAgentRegistry` record on Base, keyed by an agent address (`agentId`/`tenantId`/`scopeHash`/`behaviorHash`)                                 |
| **Capability set**     | Declared at registration: `pay_invoice`, `rebalance_treasury`, `file_vat_return`, etc                                                                 |
| **Reputation history** | A per-agent reputation pointer / Merkle root in `BrainReputationRegistry` (RFC 0001, **UNAUDITED testnet**); read by Policy as a threshold input only |
| **Scope grants**       | Per-tenant EIP-712 attestations granting specific actions, limits, and durations                                                                      |

### Discovery and Routing

Tenants and other agents query the registry by capability and reputation.

```
Tenant: "I need to pay an invoice"
   ↓
Brain queries BrainMCPAgentRegistry
   ↓
Returns agents with capability = pay_invoice
   ↓
Brain selects based on:
   - capability match
   - policy compatibility
   - cost
   - historical performance (on-chain reputation via BrainReputationRegistry. RFC 0001, testnet)
   ↓
Selection itself is an audited event
```

[**→ Smart contract reference**](/smart-contracts/overview)

#### Category-aware selection

Some triggers match agents of different categories. `cash.balance_high`, for example, matches a business **Treasury** agent and a consumer **Savings** agent. The router resolves the tenant's category (business or consumer) and adds it to scoring: a candidate whose category matches the tenant is preferred, so a business tenant routes to Treasury and a consumer tenant routes to Savings.

A category mismatch is a **scoring downgrade, not a hard reject**. A mismatched agent can still be selected when it is the best or only match, so an explicit user intent overrides the default category preference. Agnostic agents (which serve both categories) take no penalty, and when no tenant category is resolved the router is category-blind.

#### Intent matching

When a request carries a free-form intent rather than a domain event, the router scores it against each agent's declared `intent_patterns`. Two classifier strategies share one interface, selected by the `AGENT_INTENT_CLASSIFIER` flag:

* **`rules`** (default). A deterministic token-overlap classifier. Fast and dependency-free, but it only matches phrasings that share words with a pattern.
* **`embedding`**. Embeds the intent and the patterns and scores by cosine similarity, so paraphrases match ("chase late-paying customers" routes to Collections even though it shares no tokens with "follow up on overdue invoice"). Pattern embeddings are cached and reindexed when the catalog changes.

The embedding classifier keeps the rules classifier as a **live fallback**: when an intent scores below the similarity threshold (or the embedding service is unavailable) the router falls back to token overlap. Selection scoring is unchanged. Only the source of the intent-match score differs.

### Three Execution Paths

Approved actions execute through one of three paths.

<table data-view="cards"><thead><tr><th></th><th></th><th data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>🏦 Off-chain Rail</strong></td><td>A bank API or processor SDK called server-side by Brain on behalf of the tenant.</td><td></td></tr><tr><td><strong>⛓️ On-chain Via Smart Account</strong></td><td>The agent's granted session key calls <code>BrainSmartAccount.executeViaSessionKey</code>, enforced on-chain against its spend caps, bound <code>policyVersion</code>, and replay nonce.</td><td></td></tr><tr><td><strong>🤝 Agent-to-Agent</strong></td><td>An agent invokes another agent's capability through Brain. Both calls are policy-checked. Both are audited.</td><td></td></tr></tbody></table>

### Settlement: When Agents Are Paid

Where an external agent is paid for its work, Brain coordinates settlement so that the operator never redirects funds.

| Pattern                    | Mechanism                         | Use Case                                                                 |
| -------------------------- | --------------------------------- | ------------------------------------------------------------------------ |
| **Escrowed jobs**          | `BrainEscrow` (UNAUDITED testnet) | Multi-step work where USDC releases incrementally as milestones complete |
| **HTTP-native settlement** | x402                              | Per-call pay-per-use (an agent paying for an API call or tool call)      |

For x402 the tenant's smart account / EOA pays and the agent's address receives. For conditional work, the immutable `BrainEscrow` contract escrows USDC against a hashed job commitment and releases/refunds incrementally. The arbiter can only ever pay the designated payee or refund the designated payer, never redirect. Brain records and proves the flow.

**→ Escrow and x402 reference**

### SIWX Authentication

External agents authenticate using SIWX (Sign-In With X), based on EIP-4361 over Base.

```
1. Brain issues a structured SIWX challenge to the agent
2. Agent signs the challenge with its registered execution key
3. Brain verifies the signature, recovers the agent address
4. Brain looks up the address in BrainMCPAgentRegistry
5. Brain checks the agent's scope grants for the requesting tenant
6. Brain issues a session token with scoped capabilities
```

The session token gates every subsequent API or MCP call. Scopes that have not been granted by the tenant are simply invisible to the agent.

### EIP-712 ScopeAttestation

A scope grant is a tenant-signed authorization for a specific agent to perform a specific capability under specific limits. The EIP-712 type:

```
ScopeAttestation(
  bytes32 tenantId,
  address agent,
  bytes32 capability,        // e.g. keccak256("pay_invoice")
  uint128 maxAmount,
  bytes32 resourceScope,     // e.g. counterparty allowlist root
  uint64  notBefore,
  uint64  notAfter,
  uint256 nonce
)
```

This signed attestation is enforced off-chain (its hash is anchored as the agent's `scopeHash` in `BrainMCPAgentRegistry`). On-chain, the tenant translates the grant into a `BrainSmartAccount` session key via `grantSessionKey`, which binds the `policyVersion` and per-tx / per-period spend caps at grant time; `executeViaSessionKey` then enforces those caps, the bound `policyVersion`, and a per-holder replay nonce on every call.

### What Brain Provides vs What the Agent Provides

| Concern                             | Brain Provides                                                                         | Agent Provides |
| ----------------------------------- | -------------------------------------------------------------------------------------- | :------------: |
| **Verified financial context**      | ✅ Wiki + Ledger + citations                                                            |                |
| **Policy enforcement**              | ✅ Off-chain + on-chain                                                                 |                |
| **Identity and scope**              | ✅ `BrainMCPAgentRegistry`; reputation in `BrainReputationRegistry` (RFC 0001, testnet) |                |
| **Audit trail**                     | ✅ Hash chain + Merkle anchor                                                           |                |
| **Settlement infrastructure**       | ✅ Smart account, `BrainEscrow` (testnet), x402                                         |                |
| **Domain logic for the capability** |                                                                                        |        ✅       |
| **The actual work**                 |                                                                                        |        ✅       |

{% hint style="success" %}
This is why the Agent Layer is open. The substrate is general-purpose. The capabilities are pluggable.
{% endhint %}

### 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>📐 Policy</strong></td><td>How agent actions are gated.</td><td><a href="/pages/GSe2ntE9CLqxoQAiQuzG">/pages/GSe2ntE9CLqxoQAiQuzG</a></td><td></td></tr><tr><td><strong>📜 BrainSmartAccount</strong></td><td>The session-key smart account that enforces spend caps and policy binding on every call.</td><td><a href="/pages/2xFXIKlbOlKKY8V47AgE">/pages/2xFXIKlbOlKKY8V47AgE</a></td><td></td></tr><tr><td><strong>📜 BrainMCPAgentRegistry</strong></td><td>The agent identity contract.</td><td><a href="/pages/7cGQBqLnTUZjyofcuHlm">/pages/7cGQBqLnTUZjyofcuHlm</a></td><td></td></tr></tbody></table>


# Payment Intents

A **PaymentIntent** is an agent-proposed financial action that lives as a row in the Ledger. It is the only path to financial execution in Brain. There is no shortcut.

| Property             | Value                                                                                                  |
| -------------------- | ------------------------------------------------------------------------------------------------------ |
| **Layer**            | Ledger row, lifecycle owned by Agent layer                                                             |
| **Created by**       | Internal or external agents                                                                            |
| **Executes through** | Provider rails (ACH via Plaid Transfer, NetSuite SuiteTalk, BrainSmartAccount on-chain)                |
| **Gates**            | Policy decision plus the pre-execution gate (13 numbered checks + 10 hardening additions = 23 entries) |

{% hint style="info" %}
PaymentIntents are the **second of two controlled write paths** into the Ledger. The first is Raw extraction. PaymentIntents are the only Ledger write that doesn't originate from a Raw artifact, by design.
{% endhint %}

### Why PaymentIntents Are a Ledger Entity

A proposed payment is itself a financial fact. Treating it as a row in the Ledger has three consequences:

| Property                         | Effect                                                                                                        |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Queryable like any other row** | The Wiki, Policy, and other agents can read PaymentIntents the same way they read transactions or obligations |
| **Provenance carries through**   | Every state transition becomes an audit event linked to the row                                               |
| **Policy reads it directly**     | Policy evaluators read PaymentIntent fields and the live Ledger together; no shadow data model                |

### The `ledger_payment_intents` Row

```sql
ledger_payment_intents (
  id,
  owner_id,
  created_by_agent_id,
  action_type,            -- ach_outbound | ach_inbound | wire | onchain_transfer | erp_writeback | card_payment | x402_settle | escrow_release | other
  source_account_id,
  destination_counterparty_id,
  amount,
  currency,
  obligation_id,          -- optional
  invoice_id,             -- optional
  status,                 -- proposed | pending_approval | awaiting_second_approval | approved | paused | dispatching | rejected | executed | failed | cancelled
  policy_decision_id,
  approval_ids[],
  execution_receipt_ids[],
  evidence_ids[],
  created_at,
  updated_at
)
```

### Lifecycle

```
proposed
  │
  │ Policy evaluates against live Ledger state
  │
  ├──► auto-allow ─────► approved
  │
  ├──► confirm ────────► pending_approval
  │                       │
  │                       │ approver signs EIP-712
  │                       │
  │                       ├──► distinct second approver required ──► awaiting_second_approval
  │                       │                                           │
  │                       │                                           │ distinct approver signs
  │                       │                                           │
  │                       │                                           └──► approved
  │                       │
  │                       └──► approved
  │
  └──► reject ─────────► rejected

approved
  │
  │ 13 numbered checks (+10 hardening additions)
  │
  ├──► gate passes ────► dispatching
  │                       │
  │                       ├──► success ────► executed
  │                       │
  │                       └──► rail failure ► failed
  │
  └──► gate fails ─────► aborts before outbox handoff
```

[**→ The Pre-Execution Gate**](/protocol/the-pre-execution-gate)

### Rails

A gate-passed intent is handed to a durable outbox in the same database transaction that moves the intent from `approved` to `dispatching`. For ledger-account payments, that transaction also locks the source account, locks the latest balance snapshot, rechecks available balance net of active reservations, creates a balance reservation, and stores its `reservation_id` on the outbox row. The outbox worker later consumes the reservation when the intent reaches `executed`, or releases it when a deterministic rail rejection moves the intent to `failed`.

Two rails are real:

| Rail           | Implementation                           | Settlement                                                                                                        |
| -------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `bank_ach`     | Plaid Transfer (authorization → create)  | **Async**. `dispatch` returns `pending`; a Plaid `TRANSFER_EVENTS_UPDATE` webhook settles or fails the outbox row |
| `onchain_base` | `BrainSmartAccount.executeViaSessionKey` | On-chain receipt; the rail threads the per-holder session-key nonce and signs via Azure Key Vault                 |

Both are idempotency-keyed by the outbox row so a crash-retry never moves money twice. `erp_writeback` (NetSuite) remains a fail-closed stub. The M2M settlement action types `x402_settle` / `escrow_release` map to the `x402_base` / `escrow_base` rails (USDC on Base / `BrainEscrow`). They do not create ledger balance reservations because their spend is enforced by the settlement wallet or locked escrow state rather than by an off-chain ledger account hold. The live SDK wiring (Plaid / viem+KMS) and the sandbox/anvil round-trips are a follow-up; see `services/execution/README.md`.

For on-chain money movement, policy `allow` is not always enough to dispatch. `onchain_transfer` and `escrow_release` require at least one recorded human approval before execution. `x402_settle` may stay autonomous only when the signed policy rule permits on-chain settlement and sets an `x402_autonomous_max_amount` cap that covers the amount.

### State Transitions

| From                       | To                         | Trigger                                                                  |
| -------------------------- | -------------------------- | ------------------------------------------------------------------------ |
| `proposed`                 | `pending_approval`         | Policy returned `confirm`; approvers required                            |
| `proposed`                 | `approved`                 | Policy returned `auto`; no human in the loop                             |
| `proposed`                 | `rejected`                 | Policy returned `reject`                                                 |
| `pending_approval`         | `awaiting_second_approval` | First approver signed; a distinct second approver is required            |
| `pending_approval`         | `approved`                 | All required approvers signed                                            |
| `pending_approval`         | `rejected`                 | Approver explicitly rejected                                             |
| `pending_approval`         | `cancelled`                | Tenant cancelled before approval                                         |
| `awaiting_second_approval` | `approved`                 | Distinct second approver signed                                          |
| `awaiting_second_approval` | `rejected`                 | Approver explicitly rejected                                             |
| `approved`                 | `dispatching`              | Gate passed; outbox row and any balance reservation committed atomically |
| `approved`                 | `paused`                   | Tenant or halt-category kill-switch paused execution                     |
| `paused`                   | `approved`                 | Resume re-ran and passed the live gate                                   |
| `dispatching`              | `executed`                 | Outbox worker received and validated a successful rail receipt           |
| `dispatching`              | `failed`                   | Deterministic rail rejection where the worker can prove no money moved   |

Every transition emits an audit event. The full history of any PaymentIntent is reconstructable from `audit_events` ordered by `created_at`.

### How Agents Create Them

Internal agents call `PaymentIntentService.create()`. External agents call the MCP `payment_intent.propose` tool. **Both paths go through the same service method**, so policy evaluation, validation, and audit emission are identical.

```typescript
// Internal agent (TypeScript)
const intent = await paymentIntentService.create({
  ownerId: "acme",
  createdByAgentId: "ag_payment_v1",
  actionType: "ach_outbound",
  sourceAccountId: "acct_ops",
  destinationCounterpartyId: "cp_aws",
  amount: "61404.12",
  currency: "USD",
  obligationId: "ob_aws_2025_09",
  idempotencyKey: "pi_2025_09_aws_001",
});

console.log(intent.status); // "proposed" → resolved by Policy
console.log(intent.policyDecisionId); // the PolicyDecision row to inspect
```

### API Surface

PaymentIntents are a Ledger entity but their lifecycle endpoints live in the Agent group.

| Method | Endpoint                           | Purpose                                          |
| ------ | ---------------------------------- | ------------------------------------------------ |
| `POST` | `/v1/payment-intents`              | Agent proposes; returns `proposed` PaymentIntent |
| `GET`  | `/v1/payment-intents/{id}`         | Detail with PolicyDecision and audit trail       |
| `POST` | `/v1/payment-intents/{id}/approve` | Human approval for `confirm` intents             |
| `POST` | `/v1/payment-intents/{id}/reject`  | Reject                                           |
| `POST` | `/v1/payment-intents/{id}/execute` | Execute approved intent through rail             |

The MCP equivalent: `payment_intent.propose` for creation. **There is no `payment_intent.execute` on MCP**. Execution is reserved for internal Brain workers running under tenant policy.

### Reading Them Like Any Other Ledger Row

Because PaymentIntents are a real Ledger entity, the Wiki and other agents query them the same way they query transactions or obligations.

```http
GET /v1/ledger/payment-intents?status=pending_approval&owner=acme
```

Or in the MCP:

```json
{ "method": "resources/read", "params": { "uri": "brain://ledger/payment-intents/pi_a1b2c3" } }
```

A Wiki page about a vendor automatically includes their pending PaymentIntents in the **Recent Activity** section.

### Idempotency

Every PaymentIntent creation requires an `idempotencyKey`. Brain stores it in a per-tenant index; retries with the same key return the existing PaymentIntent. This protects against double-proposal under network errors or agent retries. A second, proposal-layer key dedups equivalent proposals from the same agent run (collision → `409 agent_proposal_duplicate`).

### Kill-Switch: The `paused` State

An `approved` PaymentIntent can be **paused** without a terminal transition: `approved ⇄ paused`, and `paused → cancelled`. `POST /v1/payment-intents/{id}/pause` holds it; `/resume` re-runs the **live** §6 gate before re-entering `approved` (defending against Ledger state drift while paused). `POST /v1/agents/{agent_id}/halt` atomically pauses all of an agent's in-flight intents and quarantines the agent. The rail dispatcher re-reads state immediately before submission and aborts cleanly if the intent was paused.

### 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>🚪 Pre-execution Gate</strong></td><td>The deterministic gate every payment must pass (13 numbered checks + 10 hardening additions = 23 entries).</td><td><a href="/pages/GcCCOqv3BXHFtEpuypqD">/pages/GcCCOqv3BXHFtEpuypqD</a></td><td></td></tr><tr><td><strong>🤖 Agents</strong></td><td>How internal and external agents propose actions.</td><td><a href="/pages/t9xBO8sSjFElp8ecB6i5">/pages/t9xBO8sSjFElp8ecB6i5</a></td><td></td></tr><tr><td><strong>📋 Policy and Permissioning</strong></td><td>How Policy evaluates PaymentIntents.</td><td><a href="/pages/GSe2ntE9CLqxoQAiQuzG">/pages/GSe2ntE9CLqxoQAiQuzG</a></td><td></td></tr></tbody></table>


# The Pre-Execution Gate

Before any PaymentIntent can execute, it must pass a **deterministic pre-execution gate**: **13 numbered checks plus 10 hardening additions (checks 1.5, 3.5, 5.5, 6.5, 6.6, 6.7, 7.5, 8.5, 9.5, 11.5) = 23 entries total**; the canonical happy path is the 13 numbered checks (several additions record `not_applicable` for non-M2M flows). The gate is the only path to financial execution. The gate is non-overridable.

| Property       | Value                                                        |
| -------------- | ------------------------------------------------------------ |
| **Runs at**    | The boundary before `approved -> dispatching`                |
| **Reads from** | The live Ledger (current balance, counterparty status, etc.) |
| **Emits**      | An audit event before each step and after each pass/fail     |

### Why a Gate

Policy returns `allow` based on the rules a tenant signed. But "the rules say yes" is not the same as "it is safe to execute right now." Between Policy `allow` and rail dispatch, dozens of conditions can change: a balance drops below the threshold, a counterparty flips to sanctioned, the policy version supersedes, an idempotency-key replay arrives.

The gate is the deterministic check that runs immediately before dispatch and reads from the **current** Ledger state, not the snapshot Policy evaluated against.

{% hint style="success" %}
Think of Policy as the **standing rule** and the gate as the **flight check**. Both must pass. Either one failing is a hard stop.
{% endhint %}

### The Core Steps

The gate runs the following classes of check, every payment, every time. Steps are deterministic and versioned with the protocol. These are the 13 numbered checks of the canonical happy path; 10 hardening additions are inserted at their correct positions (checks 1.5, 3.5, 5.5, 6.5, 6.6, 6.7, 7.5, 8.5, 9.5, 11.5. See **Hardening Additions** below) for 23 entries total. The M2M / x402 / escrow additions (3.5, 5.5, 6.5, 6.6, 8.5) record `not_applicable` for non-M2M flows so the canonical happy path is unchanged. Check 6.7 (obligation direction) is dormant when the intent carries no `obligation_id`.

| #  | Step (`check name`)                                                                                                                                         | Reads From                                                 |
| -- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| 1  | Agent identity verified (`agent_identity_verified`): the principal is an agent that owns this intent, and the agent record is active                        | agent record (`resolveAgent`)                              |
| 2  | Agent authorized (`agent_authorized`): the principal carries `payment_intent:execute` scope, or the agent may execute payments                              | principal scopes, agent scope                              |
| 3  | Action allowed (`action_allowed`): policy matched a rule for this action type and returned `allow` or `confirm`, never `reject`                             | `policy_decisions`                                         |
| 4  | Source account allowed (`source_account_allowed`): the source account exists and is active                                                                  | `ledger_accounts`                                          |
| 5  | Counterparty allowed (`counterparty_allowed`): the destination counterparty exists and is not sanctioned                                                    | `ledger_counterparties.risk_level`                         |
| 6  | Counterparty verified (`counterparty_verified`): when the policy threshold applies, the counterparty is `document_verified` or `sanctions_cleared`          | `ledger_counterparties.verified_status`                    |
| 7  | Amount within limit (`amount_within_limit`): the amount is at or below the policy `amount_upper_bound`, with currency match                                 | `policy_decisions` (`amount_upper_bound`)                  |
| 8  | Available balance sufficient (`available_balance_sufficient`): `available_balance - Σ(active reservations) ≥ amount`, with currency match                   | `ledger_accounts.available_balance`, `ledger_reservations` |
| 9  | Required evidence present (`required_evidence_present`): when policy requires evidence kinds, the intent carries evidence references                        | `policy_decisions`, intent `evidence_ids`                  |
| 10 | Approval requirement determined (`approval_requirement_determined`): the policy decision outcome (`allow` vs `confirm`) is recorded                         | `policy_decisions`                                         |
| 11 | Approval granted when required (`approval_granted_when_required`): required approver signatures are present, and the hard human-approval floor is satisfied | `approvals`                                                |
| 12 | Policy decision recorded (`policy_decision_recorded`): the PolicyDecision row is persisted and its id surfaced                                              | `policy_decisions`                                         |
| 13 | Audit-before emitted (`audit_before_emitted`): the `payment_intent.execute.before` event is written before any rail dispatch                                | `audit_events`                                             |

If **any** step fails, the PaymentIntent transitions to `failed` with a structured reason. No rail call is made.

### Hardening Additions

Ten further deterministic checks are inserted at their source-defined positions, bringing the complete trace to **23 entries**:

| Check                                       | What It Enforces                                                                                                                                         | Reads From                          |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
| **Agent behavior pinned** (1.5)             | The runtime agent `behaviorHash` equals the registered behavior hash; a silent model, prompt, or tool swap is a hard reject when required.               | `BrainMCPAgentRegistry`             |
| **On-chain settlement permitted** (3.5)     | The matched policy rule explicitly permits on-chain settlement for this tenant and payment class.                                                        | policy dimension                    |
| **Agent counterparty attested** (5.5)       | When the payee is an agent, it is registered and active in `BrainMCPAgentRegistry`.                                                                      | `BrainMCPAgentRegistry`             |
| **x402 payment context valid** (6.5)        | The x402 settlement context is USDC on Base and matches the resolved counterparty payee.                                                                 | intent settlement context           |
| **Escrow state bound** (6.6)                | For an escrow release, the on-chain `BrainEscrow` lock matches: still `Locked`, enough remaining to cover this release, same payee, same `jobTermsHash`. | `BrainEscrow.getEscrow` (testnet)   |
| **Obligation direction matches flow** (6.7) | When the intent cites an `obligation_id`, the linked obligation is not a receivable. An outflow targeting an obligation owed to us is a hard reject.     | `ledger_obligations.direction`      |
| **Ledger-state snapshot binding** (7.5)     | The Ledger snapshot Policy decided against is captured and re-validated immediately before dispatch; drift is a hard reject.                             | `computeLedgerSnapshot` over Ledger |
| **Micropayment cap within window** (8.5)    | Per-agent rolling-window spend stays within the signed policy cap.                                                                                       | `executions`                        |
| **Evidence semantic validation** (9.5)      | The supporting evidence actually substantiates this action (amount, counterparty, obligation), not just that it exists.                                  | `raw_parsed`, `evidence_ids`        |
| **Duplicate-payment protection** (11.5)     | No prior execution with the same counterparty and amount inside the configured duplicate window, and no reused paid evidence or settled obligation.      | `executions`, Ledger evidence       |

These persist into the `gate_checks` snapshot on the audit-before event, so the full 23-entry trace is part of the verifiable Proof artifact.

Check 11 also enforces the hard human-approval floor for on-chain money movement. `onchain_transfer` and `escrow_release` require at least one recorded human approval even when policy returns `allow`. `x402_settle` can remain approval-free only when the matched signed policy rule sets `onchain_settlement_permitted: true` and `x402_autonomous_max_amount` with the same currency and a value greater than or equal to the intent amount. Otherwise the gate fails with `hard_human_approval_floor_required` until a human approval is recorded.

### Audit Emission

The gate emits two audit events per step.

| Event                                | When                                          |
| ------------------------------------ | --------------------------------------------- |
| `payment_intent.gate.step_started`   | Immediately before each step runs             |
| `payment_intent.gate.step_completed` | After the step passes (or fails, with reason) |

Plus two outer events:

| Event                                                        | When                                         |
| ------------------------------------------------------------ | -------------------------------------------- |
| `payment_intent.gate.started`                                | Before step 1                                |
| `payment_intent.gate.passed` or `payment_intent.gate.failed` | After the final step (or earlier on failure) |

The full step-by-step audit means a counterparty or auditor can reconstruct exactly what was checked, in what order, against what state.

### Why Deterministic

Every step is a pure function over Ledger state plus the PaymentIntent. Two independent runs against the same Ledger snapshot produce the same result. This is what lets the gate appear in the audit trail with high confidence: it is replayable.

| Anti-pattern                                                     | Forbidden Because                              |
| ---------------------------------------------------------------- | ---------------------------------------------- |
| LLM-driven decision in the gate                                  | Non-deterministic; not replayable              |
| Network call to an external service for a "yes/no"               | Adds non-determinism and latency to a hot path |
| Step that mutates Ledger state                                   | The gate must be observation-only              |
| Step that depends on wall-clock except for stale-data thresholds | Wall-clock dependence is opt-in and bounded    |

### What Happens on Failure

Failure is structured.

```json
{
  "payment_intent_id": "pi_a1b2c3",
  "status": "failed",
  "gate_failure": {
    "step": 8,
    "reason": "INSUFFICIENT_BALANCE",
    "expected_min": "61404.12 USD",
    "observed": "58901.04 USD",
    "ledger_row": "acct_ops"
  },
  "audit_event_id": "evt_..."
}
```

The agent that proposed the intent receives the failure code. It can re-propose with adjusted parameters (smaller amount, different source account); that's a new PaymentIntent, with a new id, new PolicyDecision, and a fresh gate run.

### Why No Override

A bypass path defeats the purpose. If anyone (tenant, operator, agent) can override the gate, then the audit story collapses ("the gate passed, except when it didn't"). The gate is **non-overridable** by design. To execute a payment that the gate currently rejects, the tenant must change the underlying state (top up the account, verify the counterparty, sign a new policy). The gate then passes naturally.

This is the same logic as airline pre-flight checklists: not because the captain doesn't know what they're doing, but because removing the checklist removes the proof that it was done.

### Dry-Run Mode (Agent Autonomy)

The gate accepts a `dryRun` flag. In dry-run it runs the **same** checks against the **same** Ledger state and returns the same envelope, but does **not** insert a `policy_decisions` row, write a reservation, or emit audit events. Agents call dry-run before building a full proposal. To short-circuit obvious rejects and to decide `confirm` vs `execute`. There is **one** evaluator: the same gate code runs live and dry-run, so the two can never drift. The live gate still runs at execute time.

### Behavior Pinning Check

Check 1.5 sits between identity and authorization: the runtime agent `behaviorHash` must equal the value registered on-chain in `BrainMCPAgentRegistry`. A mismatch (a silent model/prompt/tool swap) is a hard reject regardless of every other signal. It is verified only when a runtime hash is supplied (or when a tenant opts into mandatory pinning), so the canonical happy path remains the 13 numbered checks.

### Net of Reservations

Check #8 (balance) subtracts active balance reservations: `available_balance - Σ(active reservations) ≥ amount`. With several money-movers live, parallel proposers cannot double-spend the same balance.

The live execution path treats the gate as a preflight and then performs the authoritative reserve in the handoff transaction. It locks the source account, locks the latest balance snapshot, rechecks `available_balance - active reservations >= amount`, creates the reservation, moves a PaymentIntent from `approved` to `dispatching`, and enqueues the outbox row. The outbox row carries `reservation_id` across the async boundary. On a successful rail receipt, `completeExecution()` consumes the reservation inside the same transaction as `dispatching -> executed`; on a deterministic rail rejection, `failExecution()` releases it inside the same transaction as `dispatching -> failed`. `x402_settle` and `escrow_release` remain `not_applicable` for this check because their spend is enforced by on-chain wallet or escrow state, not by an off-chain ledger-account hold.

### 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>Payment Intents</strong></td><td>The Ledger entity the gate evaluates.</td><td><a href="/pages/frJ0ygywHJq5WCmjwuXH">/pages/frJ0ygywHJq5WCmjwuXH</a></td><td></td></tr><tr><td><strong>Policy and Permissioning</strong></td><td>The standing rule that runs alongside the flight check.</td><td><a href="/pages/GSe2ntE9CLqxoQAiQuzG">/pages/GSe2ntE9CLqxoQAiQuzG</a></td><td></td></tr><tr><td><strong>Audit and Proof</strong></td><td>Where the per-step audit events land.</td><td><a href="/pages/PIgNXssgtEUZDLnC4b4d">/pages/PIgNXssgtEUZDLnC4b4d</a></td><td></td></tr></tbody></table>


# M2M and x402

Machine-to-machine (M2M) commerce is the part of Brain where one agent pays another agent. Directly, on-chain, under the same policy, gate, and audit constraints as any other Brain payment. RFC 0001.

{% hint style="warning" %}
M2M settlement is **shadow-first**. The two new rails (`x402_base` and `escrow_base`) are **unregistered at boot and fail closed** until promoted; the underlying smart contracts (`BrainEscrow`, `BrainReputationRegistry`) are **unaudited testnet** reference implementations. No money moves through them until they're audit-batched and the rails are explicitly promoted in a tenant's configuration.
{% endhint %}

### What M2M Adds. Nothing About the Gate Changes

Brain didn't grow a parallel money path for agents. It grew **two new `action_type`s** that flow through the *same* `PaymentIntent → Policy → §6 gate → Audit` pipeline as everything else:

| `action_type`    | Rail          | Settlement                                                                         |
| ---------------- | ------------- | ---------------------------------------------------------------------------------- |
| `x402_settle`    | `x402_base`   | USDC on Base via the [x402](https://www.x402.org/) HTTP-native settlement standard |
| `escrow_release` | `escrow_base` | Release (full / partial / dispute-split) of a `BrainEscrow` lock                   |

The §6 pre-execution gate still runs. The audit chain still anchors. Policy still decides. M2M is **not** an opt-out of any of that.

### Two Settlement Patterns

| Pattern         | When                                                                                    |
| --------------- | --------------------------------------------------------------------------------------- |
| **x402**        | Atomic, single-shot machine settlement. "I've finished the work, pay me now in USDC."   |
| **BrainEscrow** | Multi-step engagements. Fund up-front, release on milestones, dispute splits if needed. |

x402 is the right primitive when the payee is verifiable in real time (a service responds, you settle). Escrow is right when the work is bounded in advance and there's a possibility of dispute or staged release.

### Agent Counterparties

The payee in an M2M payment is *another agent*, not a vendor. Brain models this as an **agent counterparty**. A counterparty whose `type` is `agent` (or `wallet` with an attestation linking it to a registered agent in [`BrainMCPAgentRegistry`](/smart-contracts/brainmcpagentregistry)). The §6 gate's check 5.5 verifies that an agent payee is registered + active before any M2M settlement runs.

### The 5 M2M §6 Checks

Five gate checks were added at non-canonical positions specifically for M2M. They are active when the `PaymentIntent` carries the relevant settlement or escrow context and the needed loader is configured. For non-settlement payments, they record `not_applicable` or do not add a row as defined by the shared gate. The full gate is the canonical 13 numbered checks plus 10 hardening additions.

| Check                                    | What It Enforces                                                                                                                                            |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **On-chain-settlement permitted** (3.5)  | The payment class is allowed to settle on-chain for this tenant (else it must route off-chain)                                                              |
| **Agent-counterparty attested** (5.5)    | When the payee is an agent, it is registered + active in `BrainMCPAgentRegistry`                                                                            |
| **x402 payment-context valid** (6.5)     | The x402 `paymentRequirements` (amount, asset = USDC, network = Base, recipient) match the intent                                                           |
| **Escrow-state bound** (6.6)             | For an escrow release, the on-chain `BrainEscrow` lock matches: still `Locked`, enough **remaining** to cover this release, same payee, same `jobTermsHash` |
| **Micropayment cap within window** (8.5) | Per-agent rolling-window spend stays within the policy envelope (mirrors the on-chain session-key window cap)                                               |

See [The Pre-Execution Gate](/protocol/the-pre-execution-gate) for the full 23-entry gate trace.

### Reputation as a Tightener, Never a Gate

Agent reputation lives in a separate, ERC-8004-style contract. [`BrainReputationRegistry`](/smart-contracts/brainreputationregistry) (RFC 0001, **UNAUDITED testnet**). Policy can read the per-agent reputation pointer and use it as a **tighten-only threshold input**. For example, "only auto-approve M2M settlements under $X to agents above reputation Y."

**Reputation is never a money gate and never a §6 precondition.** A high score doesn't unlock anything beyond what Policy already allows; a low score can only tighten an existing rule. This is the same principle that keeps LLM judgment out of the §6 gate: signal that can move is allowed in; signal that can fail closed in dangerous ways is kept out.

### What Ships Today vs Later

| Capability                                                               | Status                                                                           |
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| `x402_settle` / `escrow_release` action types on `POST /payment-intents` | Live in the spec; rails fail closed until promoted                               |
| The 5 M2M §6 gate checks (3.5, 5.5, 6.5, 6.6, 8.5)                       | Live but **dormant**. Each becomes active only when its on-chain loader is wired |
| `BrainEscrow` (custodial; partial release; dispute splits)               | **UNAUDITED reference implementation** on testnet                                |
| `BrainReputationRegistry` (ERC-8004-style pointer; RFC 0001)             | **UNAUDITED testnet**                                                            |
| Agent-counterparty schema, on-chain settlement reconciliation matcher    | Live                                                                             |
| Mainnet promotion of the new contracts                                   | **Requires external audit first**. Non-negotiable                                |

### Why M2M Belongs Inside the Same Gate

The temptation in M2M settlement is to skip the gate "because it's machine-to-machine and fast." Brain rejects that: the gate is the **only** path to financial execution. An agent paying another agent is no different from a treasury system paying a vendor. The same evidence requirements, the same balance check, the same audit-event chain. The §6 design extends; it never shortcuts.

This is what makes M2M commerce on Brain *auditable* in the same shape as everything else: every settlement produces a [Proof](/api-reference/proof-api) you can hand to a counterparty, an auditor, or a regulator.

### 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>Payment Intents</strong></td><td>The Ledger entity x402_settle and escrow_release flow through.</td><td><a href="/pages/frJ0ygywHJq5WCmjwuXH">/pages/frJ0ygywHJq5WCmjwuXH</a></td><td></td></tr><tr><td><strong>The Pre-Execution Gate</strong></td><td>The 23-entry gate trace, including the M2M checks.</td><td><a href="/pages/GcCCOqv3BXHFtEpuypqD">/pages/GcCCOqv3BXHFtEpuypqD</a></td><td></td></tr><tr><td><strong>Escrow and x402</strong></td><td>The on-chain contracts.</td><td><a href="/pages/pdclcSITK44863badHKU">/pages/pdclcSITK44863badHKU</a></td><td></td></tr><tr><td><strong>BrainReputationRegistry</strong></td><td>The reputation pointer contract.</td><td><a href="/pages/0TL0aQXn4l3eiEqu51uS">/pages/0TL0aQXn4l3eiEqu51uS</a></td><td></td></tr></tbody></table>


# Audit and Proof

Every event in Brain (ingestion, extraction, query, proposal, policy decision, approval, execution, settlement) emits an audit record into an append-only log. Records form a per-tenant **Merkle tree**. Tree roots are batched and anchored on-chain through `BrainAuditAnchor`.

Brain core and its HTTP APIs are available in production. The on-chain proof deployment is on Base Sepolia. It is unaudited and there is no Base mainnet deployment. A separate sandbox environment is available for integration work.

### Three Properties This Gives You

<table data-view="cards"><thead><tr><th></th><th></th></tr></thead><tbody><tr><td><strong>📜 Tenant-verifiable History</strong></td><td>A tenant can prove a specific decision occurred at a specific time, based on specific evidence, under a specific policy version.</td></tr><tr><td><strong>🤝 Counterparty-verifiable Proofs</strong></td><td>A counterparty can verify a payment was authorized without seeing the underlying data, by checking a Merkle proof against an anchored root.</td></tr><tr><td><strong>🔒 No Silent Rewrites</strong></td><td>Brain itself cannot silently rewrite history. Anchors commit the past state to a public chain.</td></tr></tbody></table>

### What Every Audit Event Commits To

Audit events are content-addressed. Each event commits to:

| Field             | Description                                            |
| ----------------- | ------------------------------------------------------ |
| `event_type`      | `proposal`, `policy.evaluated`, `action.executed`, etc |
| `tenant_id`       | Which tenant generated the event                       |
| `actor`           | Human user ID or agent address                         |
| `timestamp`       | When the event was recorded                            |
| `inputs_hash`     | Hash of Ledger / Wiki / Raw IDs the event depended on  |
| `policy_version`  | The policy version evaluated, if any                   |
| `decision`        | The outcome of the event (where applicable)            |
| `reason`          | Structured reason code (where applicable)              |
| `prev_event_hash` | Forms a per-tenant hash chain                          |

The `prev_event_hash` field means each event references the one before it, building a chain that breaks if anything is altered.

### The Hash Chain in Pictures

```
event_001     event_002     event_003     event_004
  hash=A   ←   hash=B   ←   hash=C   ←   hash=D
                prev=A        prev=B        prev=C
```

Tamper with `event_002` and `B` changes. `event_003` still references the old `B` via its `prev=B` pointer. The chain breaks. Detection is automatic.

### Merkle Batching and on-Chain Anchoring

Events are batched into a per-tenant Merkle tree. Roots are anchored to Base Sepolia through `BrainAuditAnchor`.

| Property                | Value                                                                                                                  |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Publisher cycle**     | Configured bounded cycles. The default interval is one hour, but anchor status is the source of truth for each record. |
| **Immediate anchoring** | No severity-accelerated path exists.                                                                                   |
| **Anchor target**       | `BrainAuditAnchor` on Base Sepolia.                                                                                    |
| **Anchor authority**    | The contract's `onlyPublisher` address. The current Base Sepolia publisher is a single EOA with two-step rotation.     |
| **Replay guard**        | A tenant-root pair is published once. `anchorBatch` skips already-published pairs so batch retries are safe.           |

[**→ BrainAuditAnchor smart contract**](/smart-contracts/brainauditanchor)

### Pulling a Proof

Counterparties verify Brain audit proofs by checking a Merkle proof against the on-chain anchored root.

```http
GET /v1/audit/{event_id}/proof

→ {
    "event":         { ... },
    "merkle_path":   ["0xabc...", "0xdef...", "..."],
    "anchored_root": "0x...",
    "base_tx_hash":  "0x...",
    "base_block":    8829110
  }
```

To verify, the counterparty:

1. Checks that the proof root is published for the tenant with `isPublished(tenantId, root)`. `latestAnchor(tenantId)` is useful when the proof is for that tenant's most recent root.
2. Reconstructs the leaf hash from the `event` data.
3. Calls `verifyInclusion(root, leaf, merklePath)`.

If both checks succeed, the event is provably part of the anchored history. **Brain is not a trusted intermediary in this verification. It is just a publisher.**

### Privacy

On-chain anchors must not leak tenant data.

| What's On-Chain              | What's Off-Chain                   |
| ---------------------------- | ---------------------------------- |
| Merkle roots                 | Event payloads (encrypted at rest) |
| Hashed `tenantId`            | Raw artifacts                      |
| Anchor transaction and block | Ledger records, Wiki entities      |
| Publisher address            | Policy text and compiled rules     |

Counterparties verifying a proof receive only the specific event(s) the tenant chooses to share, plus the Merkle path. Everything else stays private.

### Compliance Exports

The Audit Layer also exposes structured exports for compliance reviews.

| Standard               | Coverage                                           |
| ---------------------- | -------------------------------------------------- |
| **SOC 2 Type II**      | Full event log with provenance                     |
| **ISO 27001**          | Access logs, key management events, change records |
| **Financial controls** | Approval chains, segregation of duties evidence    |

A public verifier endpoint is also available for counterparties to verify proofs without a Brain account.

{% hint style="success" %}
**Audit compounds across counterparties.** As more counterparties accept Brain audit proofs, every party in the graph benefits from cheaper, faster verification.
{% endhint %}

### 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>📐 Policy</strong></td><td>How decisions feed the audit trail.</td><td><a href="/pages/GSe2ntE9CLqxoQAiQuzG">/pages/GSe2ntE9CLqxoQAiQuzG</a></td><td></td></tr><tr><td><strong>📜 BrainAuditAnchor</strong></td><td>The on-chain anchor contract.</td><td><a href="/pages/5njwTjZlypdSt55BbRDG">/pages/5njwTjZlypdSt55BbRDG</a></td><td></td></tr><tr><td><strong>🌐 Audit API</strong></td><td>Pull proofs programmatically.</td><td><a href="/pages/BqeKz3FmbRDmRILK0zaA">/pages/BqeKz3FmbRDmRILK0zaA</a></td><td></td></tr></tbody></table>


# Agent Contributions

External agents do not just **read** Brain. With the right scope, they can **contribute** to Brain by pushing artifacts (transcripts, documents, structured observations) into the Raw layer, with cryptographic attribution.

| Property                              | Value                                                      |
| ------------------------------------- | ---------------------------------------------------------- |
| **Scope required**                    | `raw:write` (granted via on-chain `BrainMCPAgentRegistry`) |
| **Tool**                              | `raw.contribute` (MCP)                                     |
| **Source type on artifact**           | `agent_contributed`                                        |
| **Provenance on derived Ledger rows** | `agent_contributed`                                        |
| **Confidence ceiling**                | `0.5` until tenant or human review lifts it                |

{% hint style="info" %}
This is one of Brain's category-defining moves. Most "agent platforms" let agents act. Brain lets agents **contribute back** to the financial substrate, with cryptographic attribution and clear governance.
{% endhint %}

### Why Agents Contribute

Most useful financial signals don't come from banks or ERPs. They come from conversations, emails, contracts, internal observations. An agent that sits in a customer's workflow accumulates context that Brain otherwise has no way to see.

Examples of what agents typically contribute:

| Artifact Type    | What It Captures                                                             |
| ---------------- | ---------------------------------------------------------------------------- |
| **Transcripts**  | Sales calls confirming a deal close, vendor negotiations, board discussions  |
| **Documents**    | Forwarded contracts, signed quotes, statements of work                       |
| **Observations** | "Vendor X confirmed via email that the September invoice was reduced by 15%" |

Without an agent contribution path, this evidence sits in the agent's head (or its short-term context). With one, it lands in Brain's Raw layer, gets fingerprinted and stored, and can be extracted into Ledger rows just like any other Raw artifact.

### How a Contribution Flows

```
External Agent
   │
   │ raw.contribute via MCP, with EIP-712 signature
   ▼
Raw Layer
   │
   │ Stored, content-addressed, attributed to agent
   │ source_type: "agent_contributed"
   │
   │ ──► Quarantine (first N from this agent)
   │     ──► tenant approves agent ──► proceeds
   │
   ▼
Extraction Pipeline
   │
   │ Standard parsers run; produce raw_parsed rows
   │
   ▼
Ledger Layer
   │
   │ Derived rows tagged provenance: "agent_contributed"
   │ confidence ≤ 0.5 until reviewed
```

### What Gets Stored

The Raw artifact carries everything an auditor would need.

| Field                             | Source                                                              |
| --------------------------------- | ------------------------------------------------------------------- |
| `sha256`                          | Content hash, computed by Brain                                     |
| `source_type`                     | `agent_contributed`                                                 |
| `source_ref.agent_id`             | The contributing agent's id                                         |
| `source_ref.signature`            | The agent's EIP-712 signature over content + tenant\_id + timestamp |
| `source_ref.onchain_registration` | The `BrainMCPAgentRegistry` record id                               |
| `blob_uri`                        | Pointer to the encrypted artifact in tenant-scoped Blob storage     |

### Quarantine and Trust Escalation

Brain does not auto-extract from agent contributions on the first N artifacts. By default, the first contributions from a newly registered agent land in **contribution hold**: they're stored, hashed, attributed, but not fed into the extraction pipeline.

| Phase                                              | Behavior                                        |
| -------------------------------------------------- | ----------------------------------------------- |
| **Contribution hold (default: first 5 artifacts)** | Stored and visible to the tenant; not extracted |
| **Tenant approves agent**                          | Future contributions auto-flow to extraction    |
| **Tenant revokes**                                 | Future contributions rejected at the MCP layer  |

This is the safety valve that keeps malicious or buggy agents from polluting the Ledger before the tenant has had a chance to look at what they're contributing.

The threshold is per-agent (`quarantine_threshold`, default 5): the first N contributions are held and the counter increments on each; once over the threshold, or once a human releases the agent via `POST /v1/agents/{agent_id}/contribution-hold/release`, contributions extract automatically. Release is owner-scoped and idempotent.

### Confidence Ceiling

Even after extraction, derived Ledger rows that trace back to an `agent_contributed` Raw artifact carry `provenance = agent_contributed` and have their `confidence` capped at **0.5**. This means:

| Effect                                                            | Detail                                                                        |
| ----------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Policy rules can require higher confidence for autonomous actions | Agent-contributed evidence by itself can never auto-approve a payment         |
| Wiki narratives can mark them as "unverified"                     | The narrative explicitly notes that the source is an agent, not a bank or ERP |
| Reconciliation matches treat them as soft evidence                | Stronger sources (bank, ERP) take precedence on conflicts                     |

OCR output follows the same rule at the extractor boundary: the document extraction agent returns OCR text with a `confidence_cap` of `0.5`, so downstream callers cannot accidentally treat scanned-image extraction as higher-trust evidence.

To lift the cap, a human or a higher-trust source has to corroborate. Once corroborated, the row carries both provenances and the cap lifts.

### Authorization

The `raw:write` scope is one of the five MCP capability scopes. It is granted by the tenant at agent registration time via an EIP-712 signature, and the hash of the canonical scope document is anchored in `BrainMCPAgentRegistry`. Without `raw:write`, calls to `raw.contribute` are rejected with JSON-RPC error `-32004` (scope insufficient).

[**→ MCP Authentication**](/mcp-server/mcp-authentication)

### Revocation

Revocation is the tenant calling `revokeAgent` on `BrainMCPAgentRegistry`. Within at most 60 seconds (the on-chain scope-cache window), all subsequent contribution calls are rejected. Already-stored Raw artifacts remain (Raw is immutable), but they no longer feed extraction unless the tenant explicitly re-approves.

### Audit

Every contribution emits both:

| Event                              | Layer     |
| ---------------------------------- | --------- |
| `agent.mcp.tool_called` (outer)    | MCP layer |
| `raw.artifact.contributed` (inner) | Raw layer |

The inner event includes the `sha256`, the contributing `agent_id`, the `tenant_id`, and the EIP-712 signature. A counterparty or auditor can verify the signature offline against the agent's on-chain registration.

### What Agents Must Not Contribute

The MCP server validates artifact types and rejects shapes that don't match the schema. The pipeline also rejects content that:

| Forbidden                                                                     | Reason                                                          |
| ----------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Already-canonical Ledger rows formatted as documents                          | Agents do not write to Ledger directly; only via Raw extraction |
| Non-financial content the tenant has not opted in to ingesting                | Out of scope                                                    |
| PII fields not allowed by the tenant's data-handling policy                   | Tenant policy boundary                                          |
| Signed payloads where the signature does not match the agent's registered key | Identity boundary                                               |

### 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>📥 Raw and Ledger</strong></td><td>The substrate contributions land in.</td><td><a href="/pages/pPTXzUZ6cZ8LCgvMmMRO">/pages/pPTXzUZ6cZ8LCgvMmMRO</a></td><td></td></tr><tr><td><strong>🛠️ MCP Tools</strong></td><td>The <code>raw.contribute</code> tool reference.</td><td><a href="/pages/LEWpOYJSpmIuTr20aNe8">/pages/LEWpOYJSpmIuTr20aNe8</a></td><td></td></tr><tr><td><strong>🪪 BrainMCPAgentRegistry</strong></td><td>Where scope is anchored.</td><td><a href="/pages/7cGQBqLnTUZjyofcuHlm">/pages/7cGQBqLnTUZjyofcuHlm</a></td><td></td></tr></tbody></table>


# System Overview

Brain is a layered protocol where information flows up and control flows down. Each tenant has its own logical instance of every layer, with hard isolation at the database, credential-encryption, and policy boundaries.

### At a Glance

```
┌────────────────────────────────────────────────────────────────┐
│                    Clients (humans, agents)                    │
│   Dashboard · Internal services · External MCP · Surfaces      │
└────────────────────────────────────────────────────────────────┘
                              ↓ Auth (email/password · SIWX)
┌────────────────────────────────────────────────────────────────┐
│                          Brain API                             │
│              REST · JSON-RPC · MCP server surface              │
└────────────────────────────────────────────────────────────────┘
                              ↓
┌────────────────────────────────────────────────────────────────┐
│                  The Six-Layer Protocol Stack                  │
│  Raw → Ledger → Wiki → Policy → Agent → Audit                  │
└────────────────────────────────────────────────────────────────┘
                ↓                              ↓
┌─────────────────────────────┐  ┌─────────────────────────────┐
│        Off-chain state      │  │    On-chain commitments     │
│  Postgres · pgvector · Azure Blob   │  │   Base L2 · Brain contracts │
└─────────────────────────────┘  └─────────────────────────────┘
                                              ↓
                              ┌────────────────────────────────┐
                              │ Execution rails                │
                              │  Bank APIs · Processors ·      │
                              │  Session-key smart account     │
                              │  (x402 planned. RFC 0001)     │
                              └────────────────────────────────┘
```

### What Lives Where

| Component                     | Location                                 | Notes                                                                                          |
| ----------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------- |
| **Raw Artifacts**             | Azure Blob                               | Content-addressed, tenant-prefixed object storage                                              |
| **Ledger Records**            | Postgres                                 | Immutable, append-only with supersedence                                                       |
| **Wiki Graph and Embeddings** | Postgres + pgvector                      | Updated incrementally                                                                          |
| **Policy Compiled Form**      | Postgres                                 | Hash-anchored on-chain                                                                         |
| **Audit Hash Chain**          | Postgres                                 | Merkle roots batched on-chain                                                                  |
| **Agent Identity**            | `BrainMCPAgentRegistry` (Base L2)        | Stores `agentId`/`tenantId`/`scopeHash`/`behaviorHash` (ERC-8004 reputation planned. RFC 0001) |
| **Smart Account State**       | `BrainSmartAccount` per tenant (Base L2) | Session-key account (scope, spend caps, bound `policyVersion`)                                 |
| **Policy Hashes**             | `BrainPolicyRegistry` (Base L2)          | EIP-712 signed by tenant                                                                       |
| **Audit Anchors**             | `BrainAuditAnchor` (Base L2)             | EIP-712 signed by Brain anchorer                                                               |

### On-Chain Surface Is Intentionally Small

Brain's on-chain surface is intentionally minimal. **Most logic lives off-chain.** On-chain contracts exist for six narrow purposes:

| On-Chain Purpose                                         | Contract                  |
| -------------------------------------------------------- | ------------------------- |
| **Anchor State**                                         | `BrainAuditAnchor`        |
| **Register Policy Hashes**                               | `BrainPolicyRegistry`     |
| **Register Agent Identity**                              | `BrainMCPAgentRegistry`   |
| **Enforce Session-Key Scope/Limits and Route Execution** | `BrainSmartAccount`       |
| **Custody Conditional Escrow Locks**                     | `BrainEscrow`             |
| **Publish Reputation Roots**                             | `BrainReputationRegistry` |

All six contracts are deployed on Base Sepolia today and written in Solidity 0.8.x, built and tested with Foundry. Mainnet deployment is blocked on external audit, bytecode verification, and operator attestation. The contracts are immutable: there is no upgrade path in the MVP, and any change ships as a separately audited redeploy.

[**→ Smart contract overview**](/smart-contracts/overview)

### Six Layers, One API

The same API surface serves humans, internal agents, and external agents. Auth differs; primitives don't.

| Layer      | Primary API Endpoints                                    |
| ---------- | -------------------------------------------------------- |
| **Raw**    | `POST /v1/sources`, `POST /v1/raw/ingest`                |
| **Ledger** | `GET /v1/ledger/transactions`, `GET /v1/ledger/balances` |
| **Wiki**   | `POST /v1/wiki/question`, `GET /v1/wiki/entity/{id}`     |
| **Policy** | `POST /v1/policy`, `POST /v1/policy/evaluate`            |
| **Agent**  | `POST /v1/agents`, `POST /v1/agents/{id}/propose`        |
| **Audit**  | `GET /v1/audit/{id}`, `GET /v1/audit/{id}/proof`         |

[**→ Full API reference**](/api-reference/overview)

### Approval Surfaces

Agent proposals can be delivered to Slack, Microsoft Teams, and email through `@brain/surfaces` and the standalone `services/surface-gateway` deployable. These surfaces are not execution rails. They render proposals, capture human decisions, and send every decision through the same Brain approval pipeline: expiry, tenant-scoped identity, policy re-check, terminal-decision idempotency, audit, then execution approval handoff. The gateway has its own DB role and does not receive Ledger or execution outbox privileges.

[**→ Surface approval adapters**](/architecture/surface-approval-adapters)

### Networks

| Network            | Role                                                                               |
| ------------------ | ---------------------------------------------------------------------------------- |
| **Base Sepolia**   | Current on-chain execution and proof environment for staging and controlled pilots |
| **Base Mainnet**   | Planned only after external audit, bytecode verification, and operator attestation |
| **External Rails** | Bank APIs, processors, custodians (off-chain)                                      |

### 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>Data Flow</strong></td><td>End-to-end walkthrough of an action.</td><td><a href="/pages/f13dc46dcb9951f17a7148e3db84880b35c5a671">/pages/f13dc46dcb9951f17a7148e3db84880b35c5a671</a></td><td></td></tr><tr><td><strong>Tenant Isolation</strong></td><td>How tenants are separated at every layer.</td><td><a href="/pages/6653bed462d23f79b20417161f16b62ffd97ed9c">/pages/6653bed462d23f79b20417161f16b62ffd97ed9c</a></td><td></td></tr><tr><td><strong>Security and Compliance</strong></td><td>Non-negotiable principles.</td><td><a href="/pages/02a6fa38c72a26d3f8a40cbaad4ac854d7353a1e">/pages/02a6fa38c72a26d3f8a40cbaad4ac854d7353a1e</a></td><td></td></tr></tbody></table>


# Data Flow

End-to-end: from a webhook landing in the Raw Layer to an action executing on a rail and proof anchoring on Base.

### The Full Flow

```
Source → Raw → Ledger → Wiki → Policy → Agent → Rail → Audit
```

| Step | What Happens                                                                                                            |
| ---- | ----------------------------------------------------------------------------------------------------------------------- |
| 1    | A webhook or scheduled pull lands in **Raw**                                                                            |
| 2    | Extractors normalise it into **Ledger** records                                                                         |
| 3    | **Wiki** updates incrementally (entity resolution, narrative summarisation, embedding refresh)                          |
| 4    | An agent proposes an action referencing **Ledger** context                                                              |
| 5    | **Policy** evaluates the proposal: allow, confirm, or reject                                                            |
| 6    | If approved, the **Agent** Layer executes through an external rail (bank API, payment processor, smart account on Base) |
| 7    | Every step writes an **Audit** event with cryptographic links back to preceding ones                                    |

### Step-by-Step Trace

Imagine a payments agent paying an invoice. Here is what every layer does.

#### Step 1: Raw Lands

```
External event:
  Plaid webhook arrives with new bank transactions.

Brain action:
  - Verify webhook signature.
  - Hash payload (SHA-256).
  - Store in Azure Blob at a tenant-prefixed, content-addressed path.
  - Emit audit event: source.received
```

#### Step 2: Ledger Structures

```
Extractor input:
  Raw artifact (Plaid transaction list).

Extractor output:
  N Ledger records (transactions), each carrying:
    raw_refs:            [sha256:abc...]
    extractor_version:   plaid-v2.1
    confidence:          0.97

Brain action:
  - Apply deterministic extractor.
  - Reconcile against existing records (deduplication).
  - Emit audit event: ledger.appended
```

#### Step 3: Wiki Updates

```
Wiki input:
  New Ledger records.

Wiki action:
  - Entity resolution: is this counterparty already known?
  - Update relationships in the graph.
  - Re-summarise affected narratives.
  - Refresh embeddings for any changed text.
  - Recompute rolling summaries if a period boundary was crossed.
  - Emit audit event: wiki.updated
```

#### Step 4: Agent Proposes

```
Agent input:
  Invoice ID, tenant context, Wiki citations.

Agent output:
  Proposal: { type: "pay_invoice", invoice_id: "inv_8231",
              amount: 7800, counterparty_id: "cp_x" }

Brain action:
  - Record proposal as audit event.
  - Forward to Policy Engine.
```

#### Step 5: Policy Evaluates

```
Policy input:
  Proposal + active policy version (v3) + Ledger state.

Policy evaluation:
  - Counterparty status: approved
  - Amount: $7,800 (above $5,000 threshold)
  - Outcome: confirm, approvers=[role:cfo]

Brain action:
  - Persist the policy decision and trace.
  - Notify required approvers.
  - Emit audit event: policy.evaluated
```

#### Step 6: Approval

```
Approver input:
  Proposal with Ledger references (invoice, PO,
  vendor history, prior payments).

Approver action:
  Approves through an authenticated member session or linked approval surface.

Brain action:
  - Record the member approval in the database.
  - Move action to executable.
  - Emit audit event: action.approved
```

#### Step 7: Execution

Two paths, depending on the rail:

**Off-chain rail**

```
Brain action:
  - Construct bank API request server-side.
  - Use tenant's stored bank credentials.
  - Submit transfer.
  - Capture rail receipt.
  - Emit audit event: action.executed
```

**On-chain via smart account**

```
Brain action:
  - Build the call: BrainSmartAccount.executeViaSessionKey(nonce, target, value, data)
      target:    recipient (e.g. USDC transfer)
      signed by: the granted session key (KMS-held)
  - On-chain executeViaSessionKey enforces:
      ✓ session key granted, not paused or revoked
      ✓ within the key's spend caps (per-tx, per-window)
      ✓ bound to the policyVersion set at grant time
      ✓ nonce not replayed
  - Tx executes on Base.
  - Emit audit event: action.executed
```

#### Step 8: Audit Anchors

```
Audit Layer:
  - Append event to per-tenant hash chain.
  - Continue building Merkle tree for the current batch.

Anchorer:
  - Hourly:
      Compute Merkle root.
      EIP-712 sign with anchorer key.
      Submit to BrainAuditAnchor on Base L2.
  - On-chain: RootAnchored event emitted.
```

### End-to-End Provenance

Every step links back to every previous step.

```
Settlement receipt
         └── Action executed
         └── Recorded member approval
               └── Policy verdict (v3)
                     └── Wiki citations
                           └── Ledger records
                                 └── Raw artifacts (Azure Blob SHA-256 hashes)
```

A single Merkle proof against an anchored root verifies the entire chain.

{% hint style="success" %}
There is no point in this flow where Brain holds funds. Money moves directly between the tenant's accounts and counterparties on the tenant's chosen rails.
{% endhint %}

### 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>Tenant Isolation</strong></td><td>How tenants are separated at every layer.</td><td><a href="/pages/6653bed462d23f79b20417161f16b62ffd97ed9c">/pages/6653bed462d23f79b20417161f16b62ffd97ed9c</a></td><td></td></tr><tr><td><strong>Security and Compliance</strong></td><td>Non-negotiable principles and compliance posture.</td><td><a href="/pages/02a6fa38c72a26d3f8a40cbaad4ac854d7353a1e">/pages/02a6fa38c72a26d3f8a40cbaad4ac854d7353a1e</a></td><td></td></tr><tr><td><strong>BrainSmartAccount</strong></td><td>The on-chain validator.</td><td><a href="/pages/2xFXIKlbOlKKY8V47AgE">/pages/2xFXIKlbOlKKY8V47AgE</a></td><td></td></tr></tbody></table>


# Write Paths

Brain's six layers form a one-way upward data flow. Information flows up; control flows down. Within that flow, only **two write paths are allowed to write upward into the authoritative state** without coming from below: agent contributions to Raw, and agent-proposed PaymentIntents into Ledger.

This page documents all write paths, where they originate, and what guarantees each one provides.

### The Six-Layer Write Surface

```
┌────────────────────────────────────────────────────┐
│  6. AUDIT       append-only                        │ ← every layer writes here
├────────────────────────────────────────────────────┤
│  5. AGENT       proposals, executions              │ ← agents propose
├────────────────────────────────────────────────────┤
│  4. POLICY      decisions                          │ ← policy evaluation writes
├────────────────────────────────────────────────────┤
│  3. WIKI        pages, snapshots, annotations*     │
├────────────────────────────────────────────────────┤
│  2. LEDGER      11 entities                        │
├────────────────────────────────────────────────────┤
│  1. RAW         artifacts, parsed                  │
└────────────────────────────────────────────────────┘
```

(\*) Wiki annotations write through Raw, not directly into Ledger.

### The Default Rule: Information Flows Upward

Each layer is derived from the layer below it. The Ledger is derived from Raw via deterministic extraction. The Wiki is regenerated from Ledger and Raw on demand. Policy reads from Ledger. Agents read from all of the above and write proposals.

This rule has one purpose: **everything authoritative is replayable**. If an extractor changes, the Ledger can be re-derived from Raw. If a Wiki generator changes, pages can be re-rendered from Ledger and Raw. Source immutability at Raw is what makes the whole protocol auditable.

### The Two Controlled Exceptions

Two write paths break the strict bottom-up rule. They are explicit, scoped, and audited.

#### Exception 1: Agent Contributions to Raw

External agents with `raw:write` scope can push artifacts into the Raw layer. Stored, content-addressed, attributed to the agent's on-chain registration.

| Property                           | Value                                               |
| ---------------------------------- | --------------------------------------------------- |
| **Originates from**                | External agent (off-protocol)                       |
| **Writes to**                      | Raw                                                 |
| **Required scope**                 | `raw:write` (on-chain in `BrainMCPAgentRegistry`)   |
| **Signature requirement**          | EIP-712 over content + tenant\_id + timestamp       |
| **Quarantine?**                    | Yes, for the first N contributions from a new agent |
| **Confidence cap on derived rows** | 0.5                                                 |

[**→ Agent Contributions**](/protocol/agent-contributions)

#### Exception 2: Agent-Proposed PaymentIntents into Ledger

Agents create PaymentIntent rows in the Ledger as proposals for financial actions. PaymentIntents are the only Ledger-write path that does not originate from a Raw extraction.

| Property            | Value                                                                            |
| ------------------- | -------------------------------------------------------------------------------- |
| **Originates from** | Internal or external agents                                                      |
| **Writes to**       | Ledger (`ledger_payment_intents`)                                                |
| **Required scope**  | `payment_intent:propose` (for external agents)                                   |
| **Service method**  | `PaymentIntentService.create()` (shared by HTTP and MCP)                         |
| **Lifecycle gates** | Policy → Approval → pre-execution gate (13 numbered + 10 hardening = 23 entries) |

[**→ Payment Intents**](/protocol/payment-intents)

### All Write Paths, by Layer

#### Raw

| Writer           | Path                                             | Notes                                           |
| ---------------- | ------------------------------------------------ | ----------------------------------------------- |
| Source adapters  | Webhook + ingestion endpoints                    | Plaid, NetSuite, Gmail, Alchemy, generic upload |
| Wiki annotations | `POST /v1/wiki/annotate` writes through Raw      | Annotations never write directly into Ledger    |
| External agents  | `raw.contribute` MCP tool with `raw:write` scope | Quarantine, then standard extraction            |
| Tombstoning      | `DELETE /v1/raw/{id}`                            | Writes a tombstone, never mutates the original  |

#### Ledger

| Writer                        | Path                                   | Notes                                   |
| ----------------------------- | -------------------------------------- | --------------------------------------- |
| Extraction pipeline           | Derived from Raw via parsers           | The default and dominant write path     |
| Reconciliation engine         | Writes `ledger_reconciliation_matches` | Triggered by `reconciliation-agent`     |
| Agent-proposed PaymentIntents | `PaymentIntentService.create()`        | The exception                           |
| Re-normalization              | `POST /v1/ledger/normalize`            | Idempotent re-extraction of an artifact |

#### Wiki

| Writer        | Path                                                 | Notes                                 |
| ------------- | ---------------------------------------------------- | ------------------------------------- |
| Page renderer | `wiki_pages` regenerated from Ledger + Raw           | On schedule and on demand             |
| Annotations   | `wiki_annotations` plus a corresponding Raw artifact | Annotations are human-authored memory |
| Snapshots     | `wiki_snapshots` updated when Ledger rows change     | Bitemporal pointers                   |

#### Policy

| Writer          | Path                                                | Notes                                     |
| --------------- | --------------------------------------------------- | ----------------------------------------- |
| Policy compose  | `POST /v1/policy/{tenant}/compose` produces a draft | Stays in `draft` state                    |
| Policy sign     | `POST /v1/policy/{tenant}/sign` activates a version | Plus on-chain registration for enterprise |
| Policy evaluate | Writes a `policy_decisions` row per evaluation      | The audit-trail anchor                    |

#### Agent

| Writer            | Path                                                               | Notes                 |
| ----------------- | ------------------------------------------------------------------ | --------------------- |
| Proposal          | `proposals` row from `agent.action.propose` or internal agent code | Non-financial actions |
| Execution attempt | `executions` row when a Proposal or PaymentIntent is dispatched    | Idempotency-keyed     |
| Approval          | `approvals` row from human signing approval                        | EIP-712               |

#### Audit

| Writer           | Path                                      | Notes                       |
| ---------------- | ----------------------------------------- | --------------------------- |
| Every layer      | `audit_events` append-only writes         | Every material state change |
| Anchor publisher | `audit_anchors` per published Merkle root | Hourly cadence              |

### What Is Not Allowed

| Forbidden                                                                        | Why                                                                              |
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Direct Ledger write that does not originate from Raw extraction or PaymentIntent | Breaks replayability                                                             |
| Wiki text used as a source of truth for balances, transactions, obligations      | Wiki is human-readable memory; Ledger is machine-readable truth                  |
| Policy reading Wiki for evaluation                                               | Policy reads Ledger only; Wiki is for narrative                                  |
| Wiki code writing to `ledger_*`                                                  | Enforced physically: the Wiki connects as the read-only `brain_wiki_reader` role |
| Audit row UPDATE or DELETE                                                       | Audit is append-only; no exceptions                                              |
| Anchor re-publication of the same Merkle root                                    | Idempotency at the on-chain layer                                                |

### Implications

| Property               | Consequence                                                                              |
| ---------------------- | ---------------------------------------------------------------------------------------- |
| **Replayability**      | Drop the Ledger and Wiki; rebuild deterministically from Raw plus extraction logic       |
| **Auditability**       | Every row carries provenance back to `raw_artifacts.id` plus `raw_parsed.id`             |
| **Tenant trust**       | Agent contributions are quarantined and capped at 0.5 confidence until reviewed          |
| **Counterparty trust** | Anyone with a Merkle proof can verify against the on-chain anchor without trusting Brain |

### 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>System Overview</strong></td><td>The full architecture top-down.</td><td><a href="/pages/0faef97530c1f0291979e9bc4239668ed769f3a9">/pages/0faef97530c1f0291979e9bc4239668ed769f3a9</a></td><td></td></tr><tr><td><strong>Data Flow</strong></td><td>How a single source-of-truth event ripples up.</td><td><a href="/pages/f13dc46dcb9951f17a7148e3db84880b35c5a671">/pages/f13dc46dcb9951f17a7148e3db84880b35c5a671</a></td><td></td></tr><tr><td><strong>Tenant Isolation</strong></td><td>How tenants are separated at every layer.</td><td><a href="/pages/6653bed462d23f79b20417161f16b62ffd97ed9c">/pages/6653bed462d23f79b20417161f16b62ffd97ed9c</a></td><td></td></tr></tbody></table>


# Surface Approval Adapters

Brain surface adapters deliver agent proposals to the places where operators already work: Slack, Microsoft Teams, and email. They are approval surfaces, not execution rails.

## Current Code

The implementation lives in `packages/surfaces`, core bindings live in `packages/core`, and the inbound webhook process lives in `services/surface-gateway`.

| Area                        | Location                                   | Responsibility                                                                                      |
| --------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| Proposal schema and hashing | `packages/surfaces/src/proposal`           | Canonical proposal validation and content hash generation                                           |
| Dispatch pipeline           | `packages/surfaces/src/core/dispatcher.ts` | Validate, hash once, deliver, and persist delivered refs through a callback                         |
| Approval pipeline           | `packages/surfaces/src/core/approval.ts`   | Expiry, identity, policy, idempotency, audit, approval signature, execution handoff, surface update |
| Surface renderers           | `packages/surfaces/src/surfaces`           | Slack Block Kit, Teams Adaptive Cards, and email templates                                          |
| Inbound helpers             | `packages/surfaces/src/http`               | Slack signature validation, email token validation, Teams verifier seam                             |
| Live clients                | `packages/surfaces/src/clients`            | Slack Web API, generic HTTP email provider, and Bot Framework Teams client                          |
| Core bindings               | `packages/core/src/bindings`               | Adapter layer from Brain services into surface ports                                                |
| Webhook deployable          | `services/surface-gateway`                 | Fastify routes, DB adapters, live client wiring, and process isolation                              |

`@brain/surfaces` does not import `@brain/core`. The root `check-surface-acyclic` script enforces the dependency direction.

## Runtime Flow

```
Agent finding
  -> Proposal factory
  -> Dispatcher
  -> Slack, Teams, or email adapter
  -> Human approve or hold action
  -> Inbound HTTP helper
  -> ApprovalService.handle
  -> @brain/core ports
  -> post-audit approval signature
  -> execution approval handoff
```

The dispatcher computes the proposal content hash once at emit time. That hash is the value later recorded in audit, so the audit record proves what was shown to the approver.

## Approval Safety Model

All surfaces share the same approval pipeline:

1. Reject expired proposals.
2. Resolve the surface identity to a tenant-scoped Brain actor.
3. Re-check authority at click time through policy.
4. Write audit before any quorum-changing approval signature.
5. Record the approval signature and read post-write quorum.
6. Claim the terminal decision only after approval quorum is met, or for rejection.
7. Enqueue execution only for approved proposals after audit, signature, quorum, and claim.
8. Update the original surface message on a best-effort basis.

Slack, Teams, and email are therefore input channels to the same policy and audit path. A surface button cannot become a direct money movement path.

## Deployment Notes

`services/surface-gateway` hosts the framework-neutral handlers as a separate Fastify v5 process:

| Route                                          | Purpose                                                                   |
| ---------------------------------------------- | ------------------------------------------------------------------------- |
| `POST /surfaces/slack/interactions`            | Slack interactivity with raw-body signature verification and retry dedupe |
| `GET /surfaces/email/approve`                  | Confirmation page for signed email approval links                         |
| `HEAD /surfaces/email/approve`                 | Link preview and health-safe email route check                            |
| `POST /surfaces/email/approve`                 | Email approval confirmation with signed-token validation                  |
| `POST /surfaces/email/recipients/verify/start` | Admin-gated recipient verification email initiation                       |
| `GET /surfaces/email/verify`                   | Confirmation page for signed recipient verification links                 |
| `HEAD /surfaces/email/verify`                  | Link preview-safe recipient verification route check                      |
| `POST /surfaces/email/verify`                  | Recipient verification confirmation                                       |
| `POST /surfaces/email/routes`                  | Admin-gated agent to verified-recipient routing config                    |
| `POST /surfaces/email/domains`                 | Admin-gated tenant sender-domain DNS verification                         |
| `POST /surfaces/email/domains/reverify`        | Admin-gated re-check for tenant sender-domain DNS                         |
| `POST /surfaces/email/events`                  | ESP bounce and complaint events with webhook signature verification       |
| `POST /surfaces/teams/messages`                | Bot Framework verified Teams submit activities                            |
| `POST /surfaces/teams/install`                 | Admin-gated Brain tenant to Azure AD tenant mapping                       |
| `POST /surfaces/teams/revoke`                  | Admin-gated Teams installation revocation                                 |
| `POST /surfaces/smoke/proposals`               | Explicitly gated smoke dispatch for release candidates                    |
| `GET /healthz`                                 | Process health check                                                      |

The gateway owns only surface persistence:

* `surface_external_identities`
* `surface_proposals`
* `surface_delivered_refs`
* `surface_decisions`
* `surface_slack_retries`
* `surface_slack_installations`
* `surface_slack_install_nonces`
* `surface_email_recipients`
* `surface_email_routes`
* `surface_email_domains`
* `surface_teams_conversation_refs`
* `surface_teams_installations`

Slack installations are resolved by verified Slack workspace id before tenant state is accepted. Teams installations are resolved by authenticated Azure AD tenant id before the card's Brain tenant is trusted. Teams conversation references are recorded only after that mapping succeeds, so proactive sends use `<brainTenantId>:<conversationId>` references tied to an active install. Email recipients must be verified before routing or identity resolution accepts them. Email GET and HEAD routes never mutate state; only POST confirmation verifies an address or applies an approval. ESP bounce and complaint events mark recipients disabled so future dispatch skips them. Tenant custom senders are only used after Brain verifies SPF, DKIM, and DMARC through DNS.

Surface onboarding routes require a Brain bearer JWT with `surfaces:admin`. Slack OAuth install, Teams install and revoke, and email recipient, route, and domain onboarding all derive the Brain tenant from the principal. Slack and ESP event webhooks stay on provider HMAC signatures because they are machine callbacks, not tenant-admin onboarding calls.

The production DB role is `brain_surface_gateway`. It is tenant-scoped, has no `BYPASSRLS`, and has no Ledger or execution outbox grants. Decisions delegate to the active Policy document at click time, write shared Audit with deterministic idempotency keys, and use the existing execution approval path for handoff.


# Tenant Isolation

Each tenant has its own logical instance of every layer, with hard isolation at the database, storage path, and policy boundaries. **Cross-tenant access is impossible by construction**, not by application-level access control.

### Isolation by Layer

| Layer        | Isolation Mechanism                                                                                        |
| ------------ | ---------------------------------------------------------------------------------------------------------- |
| **Raw**      | Azure Blob paths namespaced by `tenantId`. Source credentials use AES-256-GCM at the application boundary. |
| **Ledger**   | Logical partitions in Postgres. All queries forced through tenant scope.                                   |
| **Wiki**     | Separate graph per tenant. Embeddings indexed within tenant scope only.                                    |
| **Policy**   | One active policy per tenant. Policy verdicts include `tenantId` in their signed payload.                  |
| **Agent**    | Scope grants are per-tenant. An agent active for tenant A has zero visibility into tenant B.               |
| **Audit**    | Per-tenant hash chains. Per-tenant Merkle trees. Per-tenant anchored roots.                                |
| **Surfaces** | Slack, Teams, and email identities link to Brain actors through tenant-scoped RLS tables.                  |

### Encryption Posture

```
Azure Key Vault secret or BRAIN_SOURCE_CREDENTIAL_KEY
   └── Global AES-256-GCM source-credential key
         └── Encrypted source credentials in Postgres
```

| Property                    | Detail                                                                                                     |
| --------------------------- | ---------------------------------------------------------------------------------------------------------- |
| **Credential key location** | Azure Key Vault secret in production when configured, or `BRAIN_SOURCE_CREDENTIAL_KEY` outside prod.       |
| **Algorithm**               | AES-256-GCM in `shared/src/crypto/credential-key-provider.ts`.                                             |
| **Scope**                   | One global source-credential key today. Tenant-scoped envelope keys are not implemented.                   |
| **Compromise blast radius** | Tenant isolation relies on RLS, tenant-prefixed storage paths, and policy boundaries, not per-tenant keys. |

### Customer-Managed KMS

Customer-managed tenant keys are a planned enterprise hardening item, not a shipped capability.

| Status      | Detail                                                             |
| ----------- | ------------------------------------------------------------------ |
| **Current** | Brain-managed source-credential encryption key.                    |
| **Planned** | Customer-managed key support for enterprise tenants before launch. |

### RBAC Across Humans and Agents

Every API call is scoped by tenant, role, and policy. Agents are subjects in the same RBAC graph as humans. There is no special case for agent calls.

```
Subject (human user OR agent address)
   ↓
Tenant membership (with role)
   ↓
Policy scope (which capabilities, which resources)
   ↓
Action evaluation
   ↓
allow / confirm / reject
```

| Subject Type       | Identified By        | Authenticated By                   |
| ------------------ | -------------------- | ---------------------------------- |
| **Human**          | User ID              | Email + password, or wallet (SIWX) |
| **Internal agent** | Service principal ID | Service credentials                |
| **External agent** | Agent address        | SIWX (EIP-4361 over Base)          |

### Surface Gateway Role

Slack, Teams, and email approval webhooks run in `services/surface-gateway`, not inside the core API process. The production role is `brain_surface_gateway`:

| Property      | Detail                                                                         |
| ------------- | ------------------------------------------------------------------------------ |
| **RLS**       | Enabled and forced on surface tables. The role has no `BYPASSRLS`.             |
| **Writes**    | `surface_*` tables and approval rows only.                                     |
| **Reads**     | Linked surface identities, users, and active policy rows.                      |
| **No access** | No Ledger money-path grants and no `execution_outbox` grants.                  |
| **Secrets**   | Slack, Teams, and email provider credentials stay out of the core API process. |

### On-Chain Isolation

On-chain commitments are also tenant-isolated.

| Contract                | How Tenants Are Separated                                                                                     |
| ----------------------- | ------------------------------------------------------------------------------------------------------------- |
| `BrainAuditAnchor`      | All functions take `bytes32 tenantId`. Roots and batch indices stored per tenant.                             |
| `BrainPolicyRegistry`   | Policy versions stored per `tenantId`. EIP-712 signatures bind to a specific tenant.                          |
| `BrainMCPAgentRegistry` | Scope grants stored as `(tenantId, agent, capability)`. An agent scoped for tenant A cannot act for tenant B. |
| `BrainSmartAccount`     | One smart account contract per tenant. Each enforces its tenant's policy via the on-chain verifier.           |

{% hint style="success" %}
Cross-tenant access is impossible at the protocol level, not just the application level. There is no "share data with another tenant" code path because there is no code path that accepts a foreign `tenantId`.
{% endhint %}

### Data Minimisation

Brain ingests only what enabled capabilities require. Revoking a source triggers retention and deletion workflows.

| When                         | What Happens                                                                                         |
| ---------------------------- | ---------------------------------------------------------------------------------------------------- |
| **Source connected**         | Only the agreed scope of data flows from that source                                                 |
| **Source disconnected**      | New data stops; existing data enters a retention window                                              |
| **Retention window expires** | Raw artifacts deleted (Azure Blob lifecycle); Ledger records marked closed; Wiki references redacted |
| **Tenant deletion request**  | Full tenant erasure across tenant-scoped rows and storage prefixes                                   |

### 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>Security and Compliance</strong></td><td>Non-negotiable principles, compliance posture.</td><td><a href="/pages/02a6fa38c72a26d3f8a40cbaad4ac854d7353a1e">/pages/02a6fa38c72a26d3f8a40cbaad4ac854d7353a1e</a></td><td></td></tr><tr><td><strong>Risks and Mitigations</strong></td><td>Known risks and how Brain handles them.</td><td><a href="/pages/BBebQzrRNDafNKFzHG4I">/pages/BBebQzrRNDafNKFzHG4I</a></td><td></td></tr></tbody></table>


# Security and Compliance

Brain's security posture rests on a small set of non-negotiable principles. Each one shapes the architecture.

### Non-Negotiable Principles

<table data-view="cards"><thead><tr><th></th><th></th></tr></thead><tbody><tr><td><strong>Non-Custodial</strong></td><td>Brain never takes custody of customer funds. Money flows directly between the tenant's accounts and counterparties on the tenant's chosen rails.</td></tr><tr><td><strong>Tenant-Isolated</strong></td><td>Each tenant has dedicated logical database partitions and tenant-prefixed object paths. Source credentials are encrypted with a global AES-256-GCM key loaded from Azure Key Vault in production.</td></tr><tr><td><strong>Data Minimization</strong></td><td>Brain ingests only what enabled capabilities require. Revoking a source triggers retention and deletion workflows.</td></tr><tr><td><strong>RBAC Across Humans and Agents</strong></td><td>Every API call is scoped by tenant, role, and policy. Agents are subjects in the same RBAC graph as humans.</td></tr><tr><td><strong>Human Approval Thresholds</strong></td><td>Any action above a tenant-defined threshold, any new counterparty, or any new jurisdiction can require human sign-off before execution.</td></tr><tr><td><strong>Compliance Ready</strong></td><td>The pre-execution gate blocks any counterparty an operator has flagged as sanctioned and can require verification before money moves. Live third-party screening (Chainalysis and equivalents) is planned, not yet integrated.</td></tr></tbody></table>

### Standards and Certifications

| Standard                  | Status                                       |
| ------------------------- | -------------------------------------------- |
| **SOC 2 Type II**         | Targeted                                     |
| **ISO 27001**             | Targeted                                     |
| **Customer-managed KMS**  | Available for tenants that require it        |
| **Smart contract audits** | Independent audits before mainnet deployment |
| **Bug bounty**            | Public coverage                              |

### Three Layers of Action Gating

Every proposed action passes through three independent gates. **All three must pass.**

| Layer                                   | Where It Runs       | What It Catches                                                                                                                 |
| --------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| **1. Backend Policy Engine**            | Off-chain           | Most violations, fast feedback, dynamic risk conditions                                                                         |
| **2. Counterparty risk checks**         | Off-chain           | The gate rejects a counterparty whose operator-set `risk_level` is `sanctioned` and enforces the policy verification threshold  |
| **3. On-chain session-key enforcement** | `BrainSmartAccount` | Final gate. `executeViaSessionKey` enforces the key's scope (target/selector allowlists), spend caps, and bound `policyVersion` |

```
Agent proposes
   ↓
[ Gate 1: Policy Engine ]    ← signed verdict produced if allow
   ↓
[ Gate 2: Counterparty risk ]  ← operator-set sanctioned / verified checks
   ↓
[ Gate 3: BrainSmartAccount.executeViaSessionKey ]  ← on-chain
   ↓
Executes
```

{% hint style="warning" %}
**Defence in depth.** Even if the off-chain Policy Engine were fully compromised, the on-chain `BrainSmartAccount` would still reject any call outside the granted session key's policyVersion-bound scope and spend caps.
{% endhint %}

### Counterparty Risk Attributes

Sanctions and risk are operator-set attributes on the counterparty record, read by the pre-execution gate. They are not produced by a live third-party screening call in the current build.

| Attribute             | Source                    | What It Gates                                                           |
| --------------------- | ------------------------- | ----------------------------------------------------------------------- |
| **`risk_level`**      | Operator-set ledger field | A value of `sanctioned` is a hard reject at the gate                    |
| **`verified_status`** | Operator-set ledger field | Enforces the policy counterparty-verification threshold above an amount |
| **Anomaly detection** | Brain internal            | Statistical outliers vs tenant's baseline                               |

The gate reads these fields directly: it rejects a counterparty whose `risk_level` is `sanctioned`, and above a policy threshold it requires `verified_status` to be `document_verified` or `sanctions_cleared`. Live third-party screening (Chainalysis and equivalents) that would populate these fields automatically is planned, not yet integrated.

### Smart Contract Security

| Mitigation                        | Detail                                                                                                                        |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Minimal on-chain surface**      | Most logic off-chain. Less code = smaller attack surface                                                                      |
| **External audit before mainnet** | No money-moving contract ships to mainnet without an external audit; testnet/reference contracts are clearly marked unaudited |
| **Public bug bounty**             | Continuous coverage post-deployment                                                                                           |
| **Immutable contracts**           | No upgrade path in MVP; changes ship as audited redeploys                                                                     |
| **Anchorer key hardening**        | Current testnet publisher is a single EOA; HSM-backed signing is a pre-mainnet TODO                                           |
| **Session-key enforcement**       | On-chain scope, spend caps, `policyVersion` binding, and replay nonce enforced in `executeViaSessionKey`                      |

Additional Tier 0 hardening now makes the escrow audit gate non-testnet-wide, checks explicit `BASE_RPC_URL` chain id at boot, rejects ERC20 selectors in native-mode session-key grants, and replay-protects agent behavior updates and revocations with per-agent EIP-712 nonces.

### Privacy of Audit Anchors

On-chain anchors must not leak tenant data.

```
On-chain (public):
  - Merkle roots
  - Hashed tenantId
  - Anchor timestamp
  - Event count and period bounds (periodStart, periodEnd)

Off-chain (encrypted):
  - Event payloads
  - Raw artifacts
  - Ledger records, Wiki entities
  - Policy text and compiled rules
```

A counterparty verifying a Brain audit proof receives only the specific event(s) the tenant chooses to share, plus the Merkle path. **Nothing else is exposed.**

[**→ Audit and Proof in detail**](/protocol/audit-and-proof)

### Human Approval Thresholds

Tenants define when humans must be in the loop. Every threshold is enforced at policy evaluation time.

| Trigger                                  | Default Behaviour           |
| ---------------------------------------- | --------------------------- |
| **Action above threshold**               | ESCALATE to named approvers |
| **New counterparty**                     | DENY until reviewed         |
| **New jurisdiction**                     | DENY until reviewed         |
| **Outside time window**                  | DENY                        |
| **Outside tenant-defined frequency cap** | DENY                        |

These are configurable per tenant. The defaults err on the side of human review.

### 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>Tenant isolation</strong></td><td>How separation is enforced at every layer.</td><td><a href="/pages/6653bed462d23f79b20417161f16b62ffd97ed9c">/pages/6653bed462d23f79b20417161f16b62ffd97ed9c</a></td><td></td></tr><tr><td><strong>Risks and mitigations</strong></td><td>Known risks and how Brain handles them.</td><td><a href="/pages/BBebQzrRNDafNKFzHG4I">/pages/BBebQzrRNDafNKFzHG4I</a></td><td></td></tr><tr><td><strong>Smart contracts</strong></td><td>The on-chain enforcement layer.</td><td><a href="/pages/syCHThaHeCVPGceHTGJP">/pages/syCHThaHeCVPGceHTGJP</a></td><td></td></tr></tbody></table>


# Risks and Mitigations

A frank inventory of the technical risks Brain faces and how the architecture addresses each one.

### Source Data Quality

**Risk.** Bank feeds and emails contain noise and gaps. An invoice arrives in five formats. Counterparty names vary across sources. Reconciliation is genuinely hard.

**Mitigation.**

| Mechanism                                       | How It Helps                                                          |
| ----------------------------------------------- | --------------------------------------------------------------------- |
| Deterministic extractors with confidence scores | Low-confidence records flagged for review, not silently absorbed      |
| Replayable Raw layer                            | If the extractor improves, every higher layer can be rebuilt from Raw |
| Human-in-the-loop reconciliation queue          | Low-confidence records routed to a human before they affect Wiki      |

### Agent Misbehaviour

**Risk.** A buggy or malicious agent could attempt unauthorized actions: overspending, paying the wrong counterparty, looping requests.

**Mitigation.**

| Mechanism                                | How It Helps                                                                                                                      |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| EIP-712 ScopeAttestation                 | Every action requires a tenant-signed scope; outside-scope calls revert inside `executeViaSessionKey`                             |
| Policy bound at grant                    | The session key carries the `policyVersion` digest it was authorized under; a stored key can never have a missing binding         |
| `BrainSmartAccount` enforcement on-chain | Scope + spend caps are enforced inside `executeViaSessionKey` (bound to the policyVersion at grant time), not just in the backend |
| Account-level limits (per-tx, per-day)   | Hard cap on blast radius regardless of policy                                                                                     |
| On-chain reputation pointer              | `BrainReputationRegistry` is deployed on Base Sepolia; scoring remains a neutral placeholder until reputation inputs are live     |

### Policy Ambiguity

**Risk.** Plain-English policies can be ambiguous. "Allow recurring payments to known vendors". What counts as recurring? What counts as known?

**Mitigation.**

| Mechanism                                        | How It Helps                                                |
| ------------------------------------------------ | ----------------------------------------------------------- |
| Compiler emits deterministic compiled policy     | The signed form is unambiguous JSON, not prose              |
| Compiler also emits an explanation               | Tenants see exactly what they are signing in human terms    |
| Tenants sign the compiled form                   | Eliminates "but I meant..." disputes                        |
| ESCALATE is the default for unmatched conditions | Edge cases route to humans, not silent ALLOW or silent DENY |

### Source API Failures and Rate Limits

**Risk.** Upstream banks and processors have downtime, rate limits, and silent data loss.

**Mitigation.**

| Mechanism                        | How It Helps                                                                                                                                                            |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Idempotent ingestion             | Repeated webhooks or pulls produce the same Raw artifact                                                                                                                |
| Retries with exponential backoff | Transient failures recover automatically                                                                                                                                |
| Replay from Raw                  | If an extractor needs to re-run, no need to re-pull from upstream                                                                                                       |
| Poison-record quarantine         | One malformed record is retried, then quarantined (never silently dropped) and surfaced via metrics; siblings keep projecting and an operator can replay it after a fix |

### Smart Contract Risk

**Risk.** Bugs in `BrainSmartAccount` or `BrainAuditAnchor` could compromise execution or audit integrity.

**Mitigation.**

| Mechanism                     | How It Helps                                                                        |
| ----------------------------- | ----------------------------------------------------------------------------------- |
| Minimal on-chain surface      | Less code = smaller attack surface                                                  |
| External audit before mainnet | No money-moving contract ships to mainnet without an external audit                 |
| Public bug bounty             | Continuous post-deployment coverage                                                 |
| Immutable contracts           | No upgrade path in MVP; changes ship as audited redeploys                           |
| Anchorer key hardening        | Current testnet publisher is a single EOA; HSM-backed signing is a pre-mainnet TODO |

### L2 Finality and Reorgs

**Risk.** Base, like any L2, can experience reorgs. An audit anchor that vanishes from the chain would be a problem.

**Mitigation.**

| Mechanism                                  | How It Helps                                                                             |
| ------------------------------------------ | ---------------------------------------------------------------------------------------- |
| Confirmations tuned per action class       | High-value actions wait for deeper confirmation                                          |
| Audit anchors reference the previous batch | Small reorg windows tolerated automatically                                              |
| Off-chain log is canonical until anchored  | Anchoring is a commitment, not a creation. The audit log exists before it lands on-chain |

### Privacy of Audit Anchors

**Risk.** Putting audit data on a public chain could leak tenant information.

**Mitigation.**

| What's On-Chain   | What Stays Off-Chain                       |
| ----------------- | ------------------------------------------ |
| Merkle roots only | Event payloads                             |
| Hashed `tenantId` | Raw artifacts                              |
| Anchor timestamps | Ledger records, Wiki entities, policy text |

A counterparty verifying a proof receives only the specific event(s) the tenant chooses to share, plus the Merkle path. Everything else stays private.

### Regulatory Variance

**Risk.** Different jurisdictions have different rules: data residency, payment licensing, sanctions enforcement, AML reporting.

**Mitigation.**

| Mechanism                                  | How It Helps                                                            |
| ------------------------------------------ | ----------------------------------------------------------------------- |
| Jurisdiction-aware policy primitives       | Policies can reference `counterparty.jurisdiction` and gate accordingly |
| Per-region deployments                     | Data residency requirements respected at the infrastructure level       |
| Partnerships with regulated counterparties | UAE first via VARA-licensed entities; EU and US to follow               |

### Risk Summary

| Risk                | Severity Without Mitigation | Severity With Mitigation                   |
| ------------------- | --------------------------- | ------------------------------------------ |
| Source data quality | High                        | Medium (always some noise)                 |
| Agent misbehaviour  | Critical                    | Low (multiple gates)                       |
| Policy ambiguity    | High                        | Low (compiler + signing model)             |
| Source API failures | Medium                      | Low (idempotent + replay)                  |
| Smart contract bugs | Critical                    | Low (minimal surface + immutable + audits) |
| L2 reorgs           | Medium                      | Low (confirmations + tolerant anchors)     |
| Audit privacy leaks | High                        | Negligible (only roots on-chain)           |
| Regulatory variance | High                        | Medium (handled per-region)                |

{% hint style="info" %}
This covers only the technical risks of the threat model. Operational, governance, and business risks are addressed separately in compliance and operational documentation.
{% endhint %}

### 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>Security and Compliance</strong></td><td>Non-negotiable principles.</td><td><a href="/pages/02a6fa38c72a26d3f8a40cbaad4ac854d7353a1e">/pages/02a6fa38c72a26d3f8a40cbaad4ac854d7353a1e</a></td><td></td></tr><tr><td><strong>Tenant Isolation</strong></td><td>How tenants are separated.</td><td><a href="/pages/6653bed462d23f79b20417161f16b62ffd97ed9c">/pages/6653bed462d23f79b20417161f16b62ffd97ed9c</a></td><td></td></tr><tr><td><strong>Smart Contracts</strong></td><td>The on-chain enforcement layer.</td><td><a href="/pages/syCHThaHeCVPGceHTGJP">/pages/syCHThaHeCVPGceHTGJP</a></td><td></td></tr></tbody></table>


# Readiness Summary

One page. What's production-ready, what's pilot-ready, what's testnet-only, and what's blocked on external work. The deeper diligence index is at [Enterprise Readiness](/architecture/enterprise-readiness); this page distils it for fast reads.

{% hint style="info" %}
**Current positioning.** Brain Core is a credible **staging / controlled-pilot** autonomous finance core. It is **not** yet "unrestricted production mainnet" until the external smart-contract audit clears and mainnet bytecode is verified.
{% endhint %}

## What's production-ready today

* **Six-layer protocol** (Raw, Ledger, Wiki, Policy, Agent, Audit) with strict layer boundaries enforced by lint guards.
* **§6 deterministic pre-execution gate**. 23 entries (13 numbered + 10 hardening), no LLM judgement, no Wiki reads, no skip paths.
* **Append-only audit chain** with Merkle anchoring to Base. `/v1/audit/verify` is unauthenticated. Verifiable without trusting Brain.
* **External-agent MCP surface** (JSON-RPC 2.0) with HMAC handshake and per-tenant rate limits.
* **Postgres RLS + least-privilege role separation** at the storage layer: the request path runs as `brain_app` (`FORCE ROW LEVEL SECURITY`) and every cross-tenant job/resolver runs under one of eight scoped `BYPASSRLS` roles (each limited to its layer's tables), so cross-tenant access is impossible by construction once `infra/db-roles.sql` is applied.
* **AES-256-GCM credential encryption** at rest (Azure Key Vault in production).
* **7+ fail-closed boot fences**. Misconfigured deploys fail to start rather than running degraded.
* **Tenant deletion** (GDPR Article 17 database scope).

## What's pilot-ready

Suitable for controlled-pilot use under SLA, not yet for unrestricted production.

* **Payment rails** on Base **Sepolia**: `bank_ach` (Plaid sandbox or production), `onchain_base`, `x402_base`, `escrow_base`. All four register at boot when env is present.
* **Internal AI agents**: reconciliation, payment, anomaly. Run under inbound HMAC + the §6 gate on every proposal.
* **Document ingestion without Plaid** (RFC 0004): an uploaded document (CSV / text / XLSX; PDF deferred) flows through `POST /raw/{id}/parsed` → the `document_extractor` agent → `doc_obligation_v1` normalize → a candidate obligation (confidence ≤ 0.5), answerable by the Wiki question endpoint. The components are wired; the upload-to-extract auto-trigger is a follow-up.
* **Earned-autonomy confidence gate** (RFC 0004 §5.2): a tenant policy rule `agent.confidence.gte` gates a payment on the confidence of the evidence it rests on. A document-extracted obligation starts low-confidence and must be corroborated (reconciliation lifts it upward-only, ≤ 0.9) or confirmed before it can drive an unattended payment. The §6 gate is unchanged and still reads Ledger, never Wiki.
* **Investor-grade demo**: `pnpm run demo:golden-path` with `BRAIN_DEMO_STRICT_PROOF=true` proves the full chain end-to-end (propose → gate → execute → anchor → verify) in one command.
* **Operator readiness tools**: `pnpm run production-readiness` aggregates per-rail + per-fence + per-guard status (and reads `docs/risk-register.json`) into a single go/no-go readout. Open `P0` risks pin the result to red. Each row also carries `evidence_state` (`exercised`, `configured`, `scaffolded`, or `missing`), and profile gates fail when staging/mainnet evidence is too weak. The PR CI workflow uploads the JSON as a per-commit artifact; `pnpm run readiness-trend` prints the per-release trajectory from `docs/readiness-history/`. `pnpm run readiness:evidence -- --profile staging` emits a diligence-ready markdown report.

## What's testnet-only

* **`BrainEscrow`** (USDC custodial escrow, RFC 0001 §7.6). Deployed on Base Sepolia; mainnet deploy is boot-fenced pending external audit.
* **`onchain_base` and `x402_base`** rails are wired against Base Sepolia by default. Mainnet promotion is per-tenant config, gated by the same boot fences.

## What is automated today

* **Docker VM production deploy**. GitHub Actions builds GHCR images, deploys to the Docker VM on green `main`, runs migrations before recreate, and smokes `https://api.brain.fi/health`.

## What requires external work (not yet done)

* **External smart-contract audit** of the six contracts (`BrainAuditAnchor`, `BrainPolicyRegistry`, `BrainSmartAccount`, `BrainMCPAgentRegistry`, `BrainEscrow`, `BrainReputationRegistry`). `contracts/AUDIT-SCOPE.md` is ready; engagement is pending.

## What requires customer deployment work

* Applying `infra/db-roles.sql` to the production Postgres instance (creates `brain_app`, `brain_wiki_reader`, and the eight least-privilege cross-tenant roles) and provisioning each role's `BRAIN_*_DB_URL` (all required at boot in production).
* Configuring Azure Key Vault credentials for the source-credential AES key, or using the documented non-production env-var key path outside production.
* Setting `BRAIN_ESCROW_AUDIT_RECEIPT` to the audit report URL/hash (or the legacy `BRAIN_ESCROW_AUDIT_APPROVED="true"`) once the audit completes and bytecode is verified.
* Running the existing `pnpm run production-readiness` check against the customer env before promotion. It reports readiness **per deploy stage** (`demo` / `staging` / `mainnet`), so the staged story is explicit rather than one aggregate: demo is ready today, staging needs exercised testnet rail evidence, and mainnet needs the external contract audit. Scope a gate with `--profile <stage>`.
* Running `pnpm run readiness:evidence -- --profile staging` for every release candidate. The report lists row status, evidence state, promotion blockers, audit status, rail posture, connector guard status, and known limitations. Staging fails if core safety rows remain scaffolded rather than exercised.

## How to verify any claim on this page

Each item maps to a code or runtime anchor in [Enterprise Readiness](/architecture/enterprise-readiness). For runtime claims, the `brain.runtime.capabilities` log line at boot reports the per-rail + per-fence state; for static claims, the lint guards at `pnpm run lint` enforce the boundaries in CI.

## Risk register

The open risks corresponding to "what requires external work" and "pilot-ready" categorisation are tracked in the [Risk Register](https://github.com/braindotfi/brain-core/tree/main/docs/risk-register.md) with current mitigation, owner, status, and exit criteria per risk.


# Enterprise Readiness

Single diligence-facing index for fintech, bank, and platform buyers. For each enterprise concern, this page names the runtime guarantee, the code/test that enforces it, and the doc that explains it. So buyers don't have to read the source to answer "is this safe?".

{% hint style="info" %}
**Status as of `main`.** Anything marked **deferred** is on the engineering roadmap. Anything marked **external** depends on a third party. The blocker for unrestricted mainnet production is at the bottom of this page.
{% endhint %}

## At a glance

| Concern                              | Status       | Runtime / code anchor                                                                   |
| ------------------------------------ | ------------ | --------------------------------------------------------------------------------------- |
| Tenant isolation (DB)                | shipped      | Postgres RLS + `infra/db-roles.sql` + `composition/db-isolation.ts`                     |
| Tenant isolation (blob)              | shipped      | Per-tenant path prefix `<tenantId>/yyyy/mm/dd/sha256` (`blobPath`)                      |
| Wiki / Policy boundary               | shipped      | `check-policy-no-wiki-read` + `check-wiki-no-ledger-write`                              |
| Credential encryption at rest        | shipped      | AES-256-GCM + KMS provider (`shared/src/crypto/aes-gcm.ts`)                             |
| §6 deterministic payment gate        | shipped      | `shared/src/gate/gate.ts` (23 entries) + `check-gate-bypass`                            |
| External-agent HMAC handshake        | shipped      | `services/api/src/agents/sign-agent-request.ts` (signs and verifies)                    |
| MCP scope grants + per-tenant limits | shipped      | `BrainMCPAgentRegistry` + `services/mcp/src/server.ts`                                  |
| Audit log immutability               | shipped      | Append-only DB + Merkle anchoring on Base                                               |
| Audit log retention                  | shipped      | Preserved through tenant deletion (GDPR Art 17(3)(b))                                   |
| Tenant deletion                      | shipped      | `DELETE /v1/tenants/{id}` + 11 unit tests                                               |
| Tenant blob purge                    | deferred     | URIs surfaced; durable purge worker in RFC                                              |
| Production boot fences (7+)          | shipped      | DB isolation, escrow audit, rails, AES key, inbound secret, loader, and outbox fences   |
| Webhook delivery DLQ + retries       | shipped      | `services/audit/src/webhook-dispatch-worker.ts`                                         |
| On-chain PII guard                   | shipped      | `check-no-onchain-pii` + RFC 0001 §3                                                    |
| Per-tenant MCP rate limits           | shipped      | `services/mcp/src/server.ts` (Redis-backed)                                             |
| External smart-contract audit        | **external** | `contracts/AUDIT-SCOPE.md` ready, engagement pending                                    |
| Docker VM production deploy          | shipped      | `.github/workflows/main.yml` builds GHCR images, migrates, recreates, and smokes health |

## Detail

### Tenant isolation at the storage layer

**Database.** Postgres Row-Level Security is `ENABLE`d on every tenant table by migration. Enforcement requires `infra/db-roles.sql` to be applied in production. The request path runs as the non-owner `brain_app` role (`FORCE ROW LEVEL SECURITY`), and every cross-tenant background job/resolver runs under one of **eight least-privilege `BYPASSRLS` roles**, each granted only its layer's tables (raw worker, canonical projector, ledger projector, execution-outbox worker, audit verifier, audit publisher, resolver, tenant deletion) so a confused-deputy bug in one path cannot reach another layer. A boot fence (`composition/db-isolation.ts`) refuses to start the api in `NODE_ENV=production` when `BRAIN_WIKI_DB_URL` or any of the eight role URLs is missing, and a boot-time role check asserts each pool connects as its expected role with a forbidden-privilege list.

**Blob storage.** Every object lives under `<tenantId>/yyyy/mm/dd/sha256`. Paths are built by `blobPath()` in `shared/src/blob/types.ts` and never concatenated by hand. An RLS test against the non-owner `brain_app` role pins the boundary.

### Process isolation (api vs workers)

The same image runs as an HTTP-only api and as separate background-worker processes (`BRAIN_HTTP_ENABLED` + `BRAIN_WORKERS` select the role; `composition/process-roles.ts`). Workers (ingestion, projection, execution-outbox drain, audit verification/anchoring, blob purge) restart and scale independently of api deploys, and the worker-only least-privilege DB credentials are never handed to the public api runtime. `docker-compose.prod.yml` ships this split (an `api` service with `BRAIN_WORKERS=none` and a `worker` service with `BRAIN_HTTP_ENABLED=false`). Every worker holds a per-worker Postgres advisory lease (`leasedCycle`), so running multiple worker replicas is safe: one is active at a time and a crashed holder's lock auto-releases for failover.

### Wiki / Policy boundary

Brain's safety story rests on Policy reading **Ledger only**, never Wiki.

* `check-policy-no-wiki-read` (CI) scans Policy code for any Wiki import.
* `check-wiki-no-ledger-write` (CI) scans Wiki code for any Ledger write.
* 15 cross-layer invariants in `tests/invariants/` enforce this end-to-end.

### Credential encryption at rest

Plaid bank credentials are encrypted with **AES-256-GCM** before insert into `raw_plaid_items.credentials`. The key comes from Azure Key Vault in production (`shared/src/crypto/kms-provider.ts`) or `BRAIN_SOURCE_CREDENTIAL_KEY` in dev. A boot fence refuses to start in production with no provider configured.

### The §6 deterministic pre-execution gate

Every money-moving action runs 23 deterministic checks (13 numbered + 10 hardening additions). Identity, behavior pinning, policy DSL, ledger state binding, balance, evidence, approvals, duplicate detection, obligation direction (payable vs receivable), audit before and after.

{% hint style="success" %}
**No LLM. No Wiki text. No skip path.** The gate is pure code; the same inputs always produce the same outputs.
{% endhint %}

* `check-gate-bypass` (CI) enforces that no rail dispatch or `executed` transition can occur outside `PaymentIntentService.execute()`.
* Metrics: `brain.gate.check.count`, `brain.gate.outcome.count`, `brain.gate.duration_ms` (Grafana scaffold at `infra/grafana/gate.json`).
* `tests/e2e/signed-agent-gated-payment.e2e.test.ts` asserts checks 8 / 9.5 / 11.5 are `pass` and NOT `not_applicable` from staging history.

### External-agent HMAC handshake

External MCP agents call `/v1/agents/mcp` with a JWT validated against `BrainMCPAgentRegistry` (60s cache). Internal Python agents (reconciliation, payment, anomaly) verify `X-Brain-Auth: sha256=<hex>` over the request body via shared `BRAIN_AGENTS_INBOUND_SECRET`. Both sides fail closed in production:

* The api refuses to start when `RECONCILIATION_AGENT_URL` is set in prod without `BRAIN_AGENTS_INBOUND_SECRET`.
* The Python service raises `RuntimeError` before `FastAPI` is constructed when `BRAIN_ENV=production` and the secret is unset.

### Audit log + Merkle anchoring

Append-only `audit_events` table. Periodic Merkle anchor publication to Base via `BrainAuditAnchor`. The `/v1/audit/verify` endpoint is unauthenticated (verify-without-trusting-Brain). Tenant deletion preserves `audit_events` and `audit_anchors` under GDPR Article 17(3)(b) legitimate-interest carveout (financial integrity), and the deletion itself is recorded as a `tenant.deleted` audit event so it's verifiable on the chain.

### Tenant deletion (GDPR Article 17)

`DELETE /v1/tenants/{id}` walks every tenant-scoped table across the six layers in one transaction (`brain_tenant_deletion` role, BYPASSRLS, scoped to erasure). Returns per-table row counts + the list of `raw_artifacts.blob_uri` that require out-of-band purging (the database deletion is in-band; blob byte deletion is **deferred** to the privileged purge worker).

### Blob purge (deferred)

{% hint style="warning" %}
**This item is on the roadmap, not shipped.** A misconfigured operator runbook could leave blob bytes in Azure Blob Storage after a tenant deletion.
{% endhint %}

Layer-1 immutability ("Raw is the source of truth, never mutated", per `Brain_MVP_Architecture.md` Layer 1) blocks an in-band `BlobAdapter.purge()` today. The architectural carveout that reconciles Layer-1 immutability with GDPR Article 17 is in RFC 0003 (in the repo at `docs/rfcs/0003-blob-purge-article-17.md`). Once signed off, phase B implements:

* `tenant_blob_purge_jobs` durable queue table
* background worker that calls `BlobAdapter.purge(uri)` per row
* audit events `tenant_blob.purge_requested / completed / failed / retried`

Until phase B lands, operators run a separate cleanup pass against the URI list returned by the deletion endpoint.

### Production boot fences (7+)

At least seven fail-closed boot fences protect production. A misconfigured production deploy fails to start rather than running degraded:

1. **DB isolation** (`composition/db-isolation.ts`). Wiki + eight least-privilege role DB URLs required.
2. **Escrow audit** (`composition/escrow-audit-gate.ts`). Mainnet escrow requires `BRAIN_ESCROW_AUDIT_RECEIPT` (preferred. URL/filepath/hash pointing at the audit report) or the legacy `BRAIN_ESCROW_AUDIT_APPROVED="true"` boolean.
3. **Live rails** (`composition/rails-prod-fence.ts`). At least one production rail must register.
4. **AES-256-GCM**. Source-credential KMS provider must be configured.
5. **Inbound agent secret**. `BRAIN_AGENTS_INBOUND_SECRET` required when `RECONCILIATION_AGENT_URL` is set.
6. **Money-path loaders** (`composition/payment-loaders-prod-fence.ts`). Production requires always-applicable gate loaders.
7. **Outbox dispatch guard** (`composition/outbox-dispatch-guard-fence.ts`). The execution worker must re-check dispatch safety before rail dispatch.

Feature fences also protect demo provision-run and sandbox service-token routes when those routes are enabled.

Each fence emits the failure on stdout/stderr so log aggregators surface the exact missing env var or wiring error.

### Per-tenant MCP rate limits

Redis-backed limiter in `services/mcp/src/server.ts`. Per-tenant + per-tool buckets. Configurable via env. No customer can starve another via the MCP surface.

### Per-rail support matrix

A release-manager-facing per-rail support table lives in the repo at `docs/rails-matrix.md` (production\_allowed, required env, chain, audit status, failure mode). The runtime capability log emits the same fields per rail at boot:

```
brain.runtime.capabilities { ..., rails: [ {name, live, production_allowed,
  required_env_present, chain_id, audit_required, audit_approved}, ... ] }
```

## Diligence machinery (peer-review batch 7)

Beyond the runtime guarantees above, Brain ships repeatable operator and reviewer tooling that turns "is this safe to promote?" into a runnable check:

* **`pnpm run production-readiness`** evaluates the current env against every boot fence, every rail's `required_env_present`, every CI guard's wiring, and every open risk in the register. Each row includes `evidence_state`, separate from status, so a configured-but-unexercised control cannot masquerade as proven. Exit 1 (red) when any P0 risk is open, any fence would fail, or the selected profile's evidence minimum is not met. Add `--json` for machine output.
* **`pnpm run readiness:evidence -- --profile staging`** emits a diligence-ready markdown report with status, evidence state, testnet E2E posture, audit status, rail posture, connector certification guards, and known limitations. Use it as the release-candidate attachment for staging and mainnet reviews.
* **Machine-readable risk register** at `docs/risk-register.json` (mirrors `docs/risk-register.md`). The aggregator reads it directly; an open `P0` risk in the register automatically pins promotion to red.
* **CI artifact** uploaded per commit on the PR workflow (`production-readiness-${sha}`, 90-day retention). Diligence reviewers can pull any commit's readiness JSON without rebuilding.
* **Git-native trend tracking** at `docs/readiness-history/<tag>.json`. Per-release snapshots committed to the repo; `pnpm run readiness-trend` prints the trajectory (open P0 count, red/yellow/green counts, ΔP0 vs prior). No external dashboard required.

This is how the readiness story stays falsifiable: every claim has a code anchor, every claim has a runtime check, and every release has a snapshot you can compare against.

## Blockers for unrestricted mainnet production

{% hint style="danger" %}
**External contract audit must close before "unrestricted mainnet production-ready" is an honest claim.** Until then, Brain is staging / controlled-pilot ready.
{% endhint %}

1. **External smart-contract audit.** `contracts/AUDIT-SCOPE.md` is ready. Engagement is pending. Until the audit clears and the deployed bytecode is verified, the boot fence (#2 above) refuses mainnet escrow.

The deploy chain itself is no longer an unstarted blocker: `.github/workflows/main.yml` builds the Node and Python agent images, pushes them to GHCR, applies production migrations before compose recreate, starts `api`, `worker`, and `agents`, and smokes `https://api.brain.fi/health`.

The on-chain executor has a **testnet E2E** that drives the real rail against a deployed `BrainSmartAccount` on Base Sepolia (`tests/e2e/onchain-executor.testnet.e2e.test.ts`, CI job `testnet_onchain_executor_e2e`); it is gated behind a repo variable + RPC/key secrets and runs once those testnet fixtures are provisioned. `production-readiness --profile staging` treats this as required exercised evidence, so the scaffolded job no longer passes a release-candidate gate by itself. This overlaps blocker #2 (the same deploy/provisioning substrate).

## How to verify any one of these claims

Each row in the at-a-glance table names the code anchor. Reading that file is the verification. For runtime guarantees, the `brain.runtime.capabilities` log line is the single ops surface that proves which fences are armed and which rails are live in the running process.


# Overview

Brain exposes a **REST + JSON-RPC HTTP surface** and an **MCP server surface**. The same primitives are used by humans and agents. Only authentication differs.

### Base URLs

| Environment    | URL                               |
| -------------- | --------------------------------- |
| **Production** | `https://api.brain.fi/v1`         |
| **Sandbox**    | `https://staging-api.brain.fi/v1` |

### Authentication

| Caller     | Mechanism                                              |
| ---------- | ------------------------------------------------------ |
| **Humans** | Self-serve email + password, or a linked wallet (SIWX) |
| **Agents** | SIWX (EIP-4361 over Base) + EIP-712 ScopeAttestations  |

[**Authentication reference**](/api-reference/authentication)

### Representative Endpoints

```
POST   /v1/raw/ingest                       // ingest a Raw artifact
GET    /v1/ledger/transactions              // query structured Ledger records
POST   /v1/wiki/question                    // NL query over memory
POST   /v1/policy/{tenant_id}/compose       // compose a candidate policy
POST   /v1/policy/{tenant_id}/sign          // sign + activate
POST   /v1/execution/agents/register        // register an external agent
POST   /v1/agents/run                       // route -> resolve -> propose (gated)
POST   /v1/payment-intents                  // propose a payment
POST   /v1/payment-intents/{id}/approve     // approver signs
POST   /v1/payment-intents/{id}/execute     // run §6 gate; returns 202
GET    /v1/audit/event/{event_id}           // event + Merkle inclusion proof
GET    /v1/proof/{action_id}                // canonical Proof for an action
DELETE /v1/tenants/{id}                     // GDPR right-to-erasure (self-tenant only)
```

### Endpoint Reference

| Section                   | What's Covered                                                            |
| ------------------------- | ------------------------------------------------------------------------- |
| Authentication            | Email + password, SIWX, sessions, scopes                                  |
| Sources and Raw Ingestion | Ingest artifacts directly, provider webhooks, inspect and tombstone       |
| Ledger                    | Query transactions, balances, counterparties, invoices, reconcile         |
| Wiki                      | NL questions, entity browsing, evidence chains, memory pages              |
| Policy                    | Compose, sign, evaluate, lint, simulate, diff (tenant-scoped)             |
| Agents                    | Register, list catalog, route, run, halt, runs / why / gate-trace / proof |
| Actions (Payment Intents) | Propose, approve / reject, execute (runs §6 gate), pause / resume         |
| Audit                     | Events, entity history, Merkle proofs, verify, export, Proof artifact     |
| MCP Surface               | JSON-RPC tools, resources, prompts, on-chain scope check                  |

### Provenance on Every Response

Every response from Wiki, Policy, and Agent endpoints carries provenance.

| Field                | Description                                                                  |
| -------------------- | ---------------------------------------------------------------------------- |
| `source_ids`         | Raw artifact ids that produced a Ledger row                                  |
| `evidence_ids`       | Raw-parsed row ids the extractor consulted                                   |
| `evidence_path`      | Returned by Wiki answers: the chain of Raw / Ledger refs the answer rests on |
| `provenance`         | `extracted`, `inferred`, `ambiguous`, `human_confirmed`, `agent_contributed` |
| `confidence`         | Calibrated 0–1 score on every derived row                                    |
| `policy_decision_id` | Policy decision row joined to a PaymentIntent                                |

### Versioning

The API is versioned in the URL path: `/v1/...`. Breaking changes always bump the version. Non-breaking additions (new endpoints, new fields) ship in place.

| Behaviour                             | Considered Breaking? |
| ------------------------------------- | -------------------- |
| Adding an endpoint                    | No                   |
| Adding a field to a response          | No                   |
| Adding an optional field to a request | No                   |
| Removing or renaming a field          | Yes                  |
| Changing default behaviour            | Yes                  |
| Changing an HTTP status code          | Yes                  |

### Rate Limits

Rate limits apply per API key.

| Tier           | Requests / min | Burst  | Concurrent WebSockets |
| -------------- | -------------- | ------ | --------------------- |
| **Free**       | 60             | 100    | 5                     |
| **Developer**  | 600            | 1,000  | 25                    |
| **Production** | 6,000          | 10,000 | 250                   |
| **Enterprise** | Custom         | Custom | Custom                |

When rate-limited:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1700000000
```

{% hint style="warning" %}
Always honour the `Retry-After` header. Aggressive retries against rate limits will result in temporary key suspension.
{% endhint %}

### Errors

All errors share a common shape.

```json
{
  "error": {
    "code": "policy_denied",
    "message": "Counterparty not approved",
    "details": { "counterparty_id": "cp_x", "policy_version": 3 },
    "request_id": "req_8f3a92...",
    "docs_url": "https://docs.brain.fi/resources/errors#policy_denied"
  }
}
```

| Status | Meaning                                     |
| ------ | ------------------------------------------- |
| `400`  | Validation error                            |
| `401`  | Authentication failed                       |
| `403`  | Authenticated, but lacks scope              |
| `404`  | Not found                                   |
| `409`  | Conflict (e.g. duplicate registration)      |
| `422`  | Policy denied or escalation required        |
| `429`  | Rate limit exceeded                         |
| `500`  | Internal error (always logs a `request_id`) |

### 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>Authentication</strong></td><td>Email, wallet, and SIWX in detail.</td><td><a href="/pages/0WFaizrTZls13BjYF116">/pages/0WFaizrTZls13BjYF116</a></td><td></td></tr><tr><td><strong>MCP Surface</strong></td><td>Same primitives, MCP shape.</td><td><a href="/pages/zjEFSPkZADwcvDIrQ4kS">/pages/zjEFSPkZADwcvDIrQ4kS</a></td><td></td></tr></tbody></table>


# Authentication

Brain authenticates three caller types: humans, internal agents, and external agents. The same API endpoints serve all three. Only the credential differs.

| Caller             | Mode                                                             | Credential                            |
| ------------------ | ---------------------------------------------------------------- | ------------------------------------- |
| **Human**          | Self-serve email + password, **or** a linked wallet              | Bearer owner JWT                      |
| **Internal agent** | Brain-issued service token (your own backend)                    | Bearer service token                  |
| **API partner**    | Tenant API key in the sandbox integration environment, read-only | Bearer `brain_sk_…` key               |
| **External agent** | SIWX (EIP-4361 over Base) + on-chain scope                       | `access_token` from the SIWX 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 the `BRAIN_SELF_SERVE_SIGNUP` flag and is **sandbox-only** (RFC 0002): a new tenant can read and *propose*, but moves no money until the existing promotion + external-audit gates clear. Hosted SSO (Auth0/SAML) is **planned (roadmap)**, not in the MVP.
{% 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 Brain-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 Authentication (self-serve email + password)

A developer self-provisions a sandbox tenant, verifies their email, then logs in for a short-lived **owner JWT** carrying management/read/approve scopes only. Never `payment_intent:propose` / `payment_intent:execute` / `execution:propose` (money movement is an agent + §6-gate concern, never a human-login capability).

**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>
```

### Wallet Authentication (SIWX). Agents and humans

External agents. And humans who **link a wallet**. Authenticate with **Sign-In With X** (EIP-4361 over Base). 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** (the same management scopes as email login); an **agent** wallet (registered + active in `BrainMCPAgentRegistry`) mints an **agent token**.

#### Step 1: Construct the SIWX Message

```
brain.fi wants you to sign in with your Ethereum account:
0xAgentAddress

URI: https://api.brain.fi
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 additionally verifies the agent record is `active` and that the JWT's `scope_hash` matches the on-chain hash in `BrainMCPAgentRegistry`. Agents can read, contribute evidence, and **propose**. Never **execute** (there is no execute tool; every settlement passes the §6 gate).
{% endhint %}

### ScopeAttestation EIP-712 Type

```
ScopeAttestation(
  bytes32 tenantId,
  address agent,
  bytes32 capability,
  uint128 maxAmount,
  bytes32 resourceScope,
  uint64  notBefore,
  uint64  notAfter,
  uint256 nonce
)
```

### Token Lifetimes

| Token                                                    | Default TTL      | Refreshable                           |
| -------------------------------------------------------- | ---------------- | ------------------------------------- |
| **Owner JWT** (email/wallet)                             | 15 minutes       | Yes. Log in / re-sign again           |
| **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 Brain tenant from this bearer principal, not from request bodies.

### Revocation

| Type                   | How to Revoke                                            |
| ---------------------- | -------------------------------------------------------- |
| **Agent scope**        | `DELETE /v1/agents/{id}/scopes/{capability}`             |
| **Agent registration** | `POST /v1/agents/{id}/deactivate` (also called on-chain) |
| **Token**              | Short-lived by design; tokens expire (15 min / 1 hour)   |

### 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="/pages/5qYnfAd728x9RA3luyoG">/pages/5qYnfAd728x9RA3luyoG</a></td><td></td></tr><tr><td><strong>📜 BrainMCPAgentRegistry</strong></td><td>The on-chain agent registry.</td><td><a href="/pages/7cGQBqLnTUZjyofcuHlm">/pages/7cGQBqLnTUZjyofcuHlm</a></td><td></td></tr></tbody></table>


# Onboarding

Self-serve tenant signup, email verification, owner login, and wallet linking. The RFC 0002 surface. All of these routes are **public** (no bearer token) and gated behind the `BRAIN_SELF_SERVE_SIGNUP` environment flag. With the flag off (the default), `/signup` and `/auth/verify-email` return `404`. Sandbox-first by design.

| Operation              | Endpoint                               | Auth                       |
| ---------------------- | -------------------------------------- | -------------------------- |
| Sign up a new tenant   | `POST /v1/signup`                      | Public (rate-limited)      |
| Verify the owner email | `POST /v1/auth/verify-email`           | Public (rate-limited)      |
| Password login         | `POST /v1/auth/login`                  | Public (rate-limited)      |
| Link a wallet          | `POST /v1/tenants/{tenant_id}/wallets` | Owner JWT + `policy:write` |

For the conceptual walkthrough, see [Sign Up and Onboard](/build/sign-up-and-onboard). For the underlying error codes, see the [self-serve onboarding section](/resources/errors#self-serve-onboarding) of the errors reference.

### Sign Up

Provisions a new tenant + owner user and either emails a verification token (production) or returns it directly (sandbox / non-production).

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

{
  "email":    "owner@acme.com",
  "password": "a-strong-passphrase"
}
```

`password` is 12–4096 bytes and is stored as a scrypt hash (`shared/src/auth/password.ts`). The route returns `201 Created`:

```json
{
  "tenant_id": "tnt_01J0000000000000000000000A",
  "user_id": "usr_01J0000000000000000000000B",
  "status": "pending",
  "verification_token": "vtok_..."
}
```

The response carries exactly one of these fields, never both. `verification_token` is included **only outside production**. In production the API sends the token through the configured ESP client (`EMAIL_ENDPOINT`, `EMAIL_API_KEY`, optional `EMAIL_FROM`) and returns `verification_sent: true` in place of the token. If self-serve signup is enabled in production without ESP credentials, API boot fails before the route is served. Errors: `400` (validation), `409` (`signup_email_taken`), `429`.

### Verify Email

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

{
  "tenant_id": "tnt_01J0000000000000000000000A",
  "token":     "vtok_..."
}
```

```json
{
  "verified": true,
  "user_id": "usr_01J0000000000000000000000B",
  "status": "active"
}
```

Errors: `400` (`signup_token_invalid`. Invalid, expired, or already used), `429`.

### Password Login

Issues a 15-minute owner JWT. The same `401` is returned for an unknown email and a wrong password (no user enumeration); `403` if the owner email is unverified.

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

{
  "email":    "owner@acme.com",
  "password": "a-strong-passphrase"
}
```

```json
{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 900,
  "principal": {
    "id": "usr_01J0000000000000000000000B",
    "type": "user",
    "tenantId": "tnt_01J0000000000000000000000A",
    "scopes": [
      "ledger:read",
      "wiki:read",
      "raw:read",
      "raw:write",
      "policy:read",
      "policy:write",
      "audit:read",
      "execution:read",
      "payment_intent:approve",
      "surfaces:admin"
    ]
  }
}
```

{% hint style="warning" %}
The owner JWT **never** carries `payment_intent:propose`, `payment_intent:execute`, or `execution:propose`. The owner can read, approve, and manage policy. Proposing or executing payments is reserved for registered agents running through the §6 gate.
{% endhint %}

Errors: `401` (`auth_invalid_credentials`), `403` (`auth_email_unverified`), `429`.

### Link a Wallet

Once the owner is logged in (password JWT), they can link a wallet to the tenant. After linking, the same wallet can sign in over SIWX and receive an owner JWT. The "two linked principals" model (email/password for humans + wallet/SIWX for the agent runtime).

```http
POST /v1/tenants/{tenant_id}/wallets
Authorization: Bearer <owner JWT>
Content-Type: application/json

{
  "address":        "0xabc...",
  "principal_type": "human"
}
```

The body requires `address` and `principal_type` (`"human"` or `"agent"`); there is no `signature` field. A `human` link defaults to the calling owner; an `agent` link must also name `principal_id`. `tenant_id` in the path must equal the JWT's `tenantId`. Returns `201` with the linked wallet record. Errors: `400`, `401`, `403` (tenant mismatch), `409` (`wallet_already_linked`).

### What Comes Next

After login, the tenant typically:

1. Composes and signs a policy via [`POST /v1/policy/{tenant_id}/compose`](/api-reference/policy-api) → [`/sign`](/api-reference/policy-api).
2. Connects a financial source (Plaid, ERP, wallet) out-of-band and starts ingesting evidence into the [Raw layer](/api-reference/sources-api).
3. Registers any external agents via [`POST /v1/execution/agents/register`](/api-reference/agents-api).
4. Watches activity via the [Audit API](/api-reference/audit-api) and [Proof API](/api-reference/proof-api).

### 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>🪪 Authentication</strong></td><td>The fuller auth model (JWT, scopes, SIWX).</td><td><a href="/pages/0WFaizrTZls13BjYF116">/pages/0WFaizrTZls13BjYF116</a></td><td></td></tr><tr><td><strong>🚀 Sign Up and Onboard</strong></td><td>The narrative quickstart.</td><td><a href="/pages/Nt6Djhan47XRxLlsq7iM">/pages/Nt6Djhan47XRxLlsq7iM</a></td><td></td></tr></tbody></table>


# Sources and Raw Ingestion

The Brain HTTP surface does **not** expose a `/v1/sources/*` resource family. Source connectors (Plaid, on-chain extractors, ERP integrations) are configured out-of-band via the Console or per-tenant infra wiring and they push evidence into Brain through the **Raw layer**. The Raw API is what you call to ingest artifacts directly and to inspect what's been ingested.

| Concern                                                            | API                                                       |
| ------------------------------------------------------------------ | --------------------------------------------------------- |
| Push an artifact (file, URL, or provider webhook payload) into Raw | `POST /v1/raw/ingest`, `POST /v1/raw/webhooks/{provider}` |
| Read or tombstone a Raw artifact                                   | `GET /v1/raw/{raw_id}`, `DELETE /v1/raw/{raw_id}`         |
| Read the deterministic parser output for an artifact               | `GET /v1/raw/{raw_id}/parsed`                             |
| Promote parsed Raw into typed Ledger rows                          | `POST /v1/ledger/normalize` (see Ledger API)              |

The "Source Types" table further down is the conceptual taxonomy. The `source_type` you tag an ingested artifact with, not a list of HTTP resources you create.

### Ingest a Raw Artifact

Two body shapes are supported on `POST /v1/raw/ingest`: a binary upload via `multipart/form-data`, or a URL fetch via JSON. Both are idempotent by SHA-256: a re-submitted artifact (per tenant) returns the existing `raw_id` with `deduplicated: true`.

#### Bring your own source

Use `source_type: other` to submit an artifact from a source without a native Brain connector. This route requires a bearer principal with `raw:write`: a human JWT or a registered SIWX agent JWT that was granted that scope. Standard `brain_sk_` tenant API keys are read-only and cannot be issued `raw:write`; at launch API-key authentication is also disabled on the production API. A custom artifact is stored as raw evidence and is projected only after a compatible parser is registered. It is not a direct arbitrary Ledger-event write API.

Binary upload:

```http
POST /v1/raw/ingest
Authorization: Bearer <token>
Content-Type: multipart/form-data

source_type=pdf_upload
file=@invoice_8231.pdf
mime_type=application/pdf
```

URL fetch:

```http
POST /v1/raw/ingest
Authorization: Bearer <token>
Content-Type: application/json

{
  "source_type":  "csv_upload",
  "url":          "https://example.com/statement.csv",
  "source_ref":   { "account_id": "acct_ops" },
  "auth_header":  "Bearer <upstream-token>"
}
```

Response (201 on first ingest, 200 on dedup):

```json
{
  "raw_id": "raw_8231",
  "sha256": "abc123...",
  "source_type": "csv_upload",
  "bytes": 18420,
  "ingested_at": "2026-05-28T12:00:00Z",
  "deduplicated": false
}
```

Limits: 50 MB per artifact. Errors: `400`, `401`, `403`, `413`, `415`, `429`.

`plaid`, `stripe`, `finch`, and `merge_accounting` are reserved on this route: those source types are provider-authenticated only and may be created solely through their authenticated provider integration, so a caller cannot mint high-trust evidence by labeling an upload. Asserting any of them here returns `raw_source_reserved`.

### Source Types

The `source_type` you tag an ingested artifact with. Used for routing to the right parser.

| `source_type`       | Typical Origin                                          |
| ------------------- | ------------------------------------------------------- |
| `plaid`             | Plaid bank-account artifacts (statements, transactions) |
| `stripe`            | Stripe API objects                                      |
| `netsuite`          | NetSuite SuiteTalk extracts                             |
| `merge_accounting`  | Merge.dev accounting integrations (QuickBooks, Xero)    |
| `finch`             | Finch payroll and HR provider extracts                  |
| `email_inbound`     | Inbound email (e.g. invoices forwarded to a mailbox)    |
| `csv_upload`        | Direct CSV file upload                                  |
| `pdf_upload`        | Direct PDF / document upload                            |
| `alchemy_wallet`    | On-chain EVM extractor output (Alchemy indexer)         |
| `eth_address`       | Watched address chain events                            |
| `agent_contributed` | Pushed by an external agent with `raw:write` scope      |
| `wiki_annotation`   | Human corrections via the Wiki annotate path (internal) |
| `other`             | Universal fallback: any source with no native connector |

{% hint style="info" %}
The webhook path (`POST /v1/raw/webhooks/{provider}`) accepts a separate, narrower `provider` enum: `plaid`, `stripe`, `alchemy`, `netsuite`, `generic_hmac`. Webhook signature verification replaces bearer auth on that route.
{% endhint %}

### Provider Webhooks

Connected providers (Plaid, Stripe, etc.) push events at:

```http
POST /v1/raw/webhooks/{provider}
Content-Type: application/json
X-Provider-Signature: <hmac>

<provider-specific payload>
```

This route has `security: []`. The HMAC signature replaces bearer auth. Brain verifies the signature, stores the payload as one or more Raw artifacts, and returns `202 Accepted` with `{ accepted: true, trace_id: "...", artifacts: 1 }`, where `artifacts` is the count persisted (`0` on an idempotent replay). A signature mismatch returns `401` with `raw_webhook_signature_invalid`.

### Read a Raw Artifact

```http
GET /v1/raw/{raw_id}
Authorization: Bearer <token>
```

```json
{
  "raw_id": "raw_8231",
  "sha256": "abc123...",
  "signed_url": "https://blob.brain.fi/...",
  "expires_at": "2026-05-28T12:10:00Z",
  "mime_type": "application/pdf",
  "bytes": 18420
}
```

The signed URL is short-lived (10-minute TTL) and returns the bytes with `Content-Disposition: attachment`. The artifact itself lives in the tenant's Azure Blob partition. `404` if unknown, `410` if tombstoned.

### Tombstone a Raw Artifact

```http
DELETE /v1/raw/{raw_id}
Authorization: Bearer <token>
```

`204 No Content`. The artifact becomes inaccessible and is filtered from Wiki, but the underlying bytes are retained per regulatory retention policy. Re-tombstoning returns `410`.

### Read the Parsed Form

After ingestion, deterministic parsers extract structured fields. Their output is queryable:

```http
GET /v1/raw/{raw_id}/parsed?parser=invoice_v2&parser_version=3.1
Authorization: Bearer <token>
```

```json
{
  "raw_id": "raw_8231",
  "parsed": [
    {
      "id": "rp_001",
      "raw_artifact_id": "raw_8231",
      "parser": "invoice_v2",
      "parser_version": "3.1",
      "extracted": { "amount": "1234.56", "currency": "USD", "due_date": "2026-06-15" },
      "confidence": 0.98,
      "extracted_at": "2026-05-28T12:00:30Z"
    }
  ]
}
```

Parsed rows are append-only; a re-run with a new `parser_version` produces a new row rather than mutating the old one.

### Promoting Raw to Ledger

Parsed Raw becomes typed Ledger rows via `POST /v1/ledger/normalize` (documented in the Ledger API). Normalization is idempotent. The same `raw_parsed_id` produces the same Ledger row ids on re-run.

### 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>🧾 Ledger API</strong></td><td>Query the structured records produced from Raw.</td><td><a href="/pages/DLwkcOAta3FpGcVqamkB">/pages/DLwkcOAta3FpGcVqamkB</a></td><td></td></tr><tr><td><strong>📥 Raw and Ledger</strong></td><td>The conceptual model.</td><td><a href="/pages/pPTXzUZ6cZ8LCgvMmMRO">/pages/pPTXzUZ6cZ8LCgvMmMRO</a></td><td></td></tr></tbody></table>


# Ledger API

Query the deterministic structured records the Brain protocol produces from Raw evidence. The Ledger is the single source of financial truth. Every row carries provenance, evidence references, and a confidence score.

| Operation                         | Endpoint                                                             |
| --------------------------------- | -------------------------------------------------------------------- |
| List accounts                     | `GET /v1/ledger/accounts`                                            |
| Account detail (+ latest balance) | `GET /v1/ledger/accounts/{account_id}`                               |
| List balances (point-in-time)     | `GET /v1/ledger/balances`                                            |
| List counterparties               | `GET /v1/ledger/counterparties`                                      |
| Grant counterparty trust          | `POST /v1/ledger/counterparties/{counterparty_id}/trust/grant`       |
| Pause counterparty trust          | `POST /v1/ledger/counterparties/{counterparty_id}/trust/pause`       |
| Restore counterparty trust        | `POST /v1/ledger/counterparties/{counterparty_id}/trust/restore`     |
| Acknowledge counterparty review   | `POST /v1/ledger/counterparties/{counterparty_id}/trust/acknowledge` |
| List invoices                     | `GET /v1/ledger/invoices`                                            |
| List obligations                  | `GET /v1/ledger/obligations`                                         |
| List transactions                 | `GET /v1/ledger/transactions`                                        |
| Transaction detail                | `GET /v1/ledger/transactions/{transaction_id}`                       |
| Promote Raw → Ledger              | `POST /v1/ledger/normalize`                                          |
| Trigger reconciliation            | `POST /v1/ledger/reconcile`                                          |
| List reconciliation matches       | `GET /v1/ledger/reconciliation-matches`                              |

### List Transactions

```http
GET /v1/ledger/transactions?account_id=acct_ops&since=2026-01-01&until=2026-03-31&direction=outflow
Authorization: Bearer <token>
```

```json
{
  "transactions": [
    {
      "id": "tx_001",
      "account_id": "acct_ops",
      "external_transaction_id": "plaid_tx_abc",
      "amount": "-1234.56",
      "currency": "USD",
      "direction": "outflow",
      "transaction_date": "2026-01-15",
      "posted_date": "2026-01-16",
      "counterparty_id": "cp_aws",
      "category_id": "cat_cloud",
      "status": "posted",
      "description_normalized": "AWS - cloud services",
      "reconciliation_status": "matched",
      "source_ids": ["raw_8231"],
      "evidence_ids": ["rp_001"],
      "confidence": 0.98
    }
  ],
  "next_cursor": "..."
}
```

Filters: `account_id`, `counterparty_id`, `direction` (`inflow | outflow | transfer | adjustment`), `status` (`pending | posted | cleared | failed | reversed | disputed`), `since`, `until`, `limit` (default 100, max 1000), `cursor`. `amount` is a signed decimal string.

### Get a Single Transaction

```http
GET /v1/ledger/transactions/{transaction_id}
Authorization: Bearer <token>
```

Returns the same `Transaction` shape. `404` if unknown.

### List Accounts

```http
GET /v1/ledger/accounts?status=active&account_type=bank_checking&limit=50
Authorization: Bearer <token>
```

```json
{
  "accounts": [
    {
      "id": "acct_ops",
      "owner_id": "acme",
      "account_type": "bank_checking",
      "name": "Operating",
      "currency": "USD",
      "status": "active",
      "institution": "Mercury",
      "external_account_id": "plaid_acc_xyz",
      "current_balance": "182431.45",
      "available_balance": "181009.12"
    }
  ],
  "next_cursor": null
}
```

`account_type` enum: `bank_checking | bank_savings | card | loan | line_of_credit | onchain | payment_processor`. Filters: `status` (`active | closed | frozen | pending`), `account_type`, `limit` (default 50, max 500), `cursor`.

For one account plus its latest balance:

```http
GET /v1/ledger/accounts/{account_id}
Authorization: Bearer <token>
```

```json
{ "account": { ... }, "latest_balance": { "current_balance": "182431.45", "as_of": "2026-05-28T11:55:00Z", "currency": "USD" } }
```

### List Balances (Point-in-Time)

```http
GET /v1/ledger/balances?account_id=acct_ops&as_of=2026-03-31T23:59:59Z
Authorization: Bearer <token>
```

Returns the balance row(s) effective at `as_of` (or the latest if omitted).

### List Counterparties

```http
GET /v1/ledger/counterparties?q=AWS&type=vendor&verified_status=document_verified
Authorization: Bearer <token>
```

`type` enum: `merchant | vendor | customer | employer | employee | bank | wallet | exchange | tax_authority | agent | other`. `verified_status`: `unverified | self_attested | document_verified | sanctions_cleared`. `trust_status`: `unreviewed | trusted | paused | acknowledged`. Each counterparty carries `risk_level` (`low | medium | high | sanctioned`), `aliases[]`, and `linked_accounts[]`. Filter list results with `trust_status` when needed.

### Counterparty Trust

Trust transitions apply uniformly to every counterparty type, including vendors and customers. Each route requires a user bearer JWT with `ledger:write`. Platform shared-secret callers and API keys cannot use these routes.

| Transition  | Endpoint                                                             | Allowed prior state                     | Result         |
| ----------- | -------------------------------------------------------------------- | --------------------------------------- | -------------- |
| Grant       | `POST /v1/ledger/counterparties/{counterparty_id}/trust/grant`       | `unreviewed`, `acknowledged`            | `trusted`      |
| Pause       | `POST /v1/ledger/counterparties/{counterparty_id}/trust/pause`       | `unreviewed`, `trusted`, `acknowledged` | `paused`       |
| Restore     | `POST /v1/ledger/counterparties/{counterparty_id}/trust/restore`     | `paused`                                | `trusted`      |
| Acknowledge | `POST /v1/ledger/counterparties/{counterparty_id}/trust/acknowledge` | `unreviewed`, `paused`                  | `acknowledged` |

Every transition accepts an optional audit-only reason:

```json
{ "reason": "Reviewed supporting documents" }
```

Invalid transitions return `409` with `ledger_status_invalid`. Successful responses return the updated `counterparty` and `previous_trust_status`.

Trust state is informational today: payment execution is enforced through `verified_status`, sanctions screening, and policy checks. Trust state does not gate payment execution.

### List Invoices and Obligations

```http
GET /v1/ledger/invoices?status=sent
GET /v1/ledger/obligations?direction=receivable&status=due&due_before=2026-06-30
GET /v1/ledger/obligations?scenario=ar
```

`Invoice.status`: `draft | sent | partial | paid | overdue | cancelled | disputed`. `Obligation.type`: `bill | invoice | subscription | loan | rent | payroll | tax | card_statement | other`.

Both endpoints use opaque cursor pagination. Pass `limit` (default `50`, max `500`) and the preceding response's `next_cursor` as `cursor`; a final page returns `next_cursor: null`. Use `direction=receivable` on obligations when listing receivable obligations, or `scenario=ar` to select explicitly AR-marked rows. AR-sourced invoice and receivable-obligation rows carry `metadata.scenario: "ar"`. Clients should use that positive marker for classification rather than treating every non-AP row as receivable.

`GET /v1/ledger/invoices` is the complete Ledger source for a receivables inventory. `GET /v1/ledger/obligations?direction=receivable` contains only receivables that have an obligation projection, so it is not an interchangeably complete AR list.

### Promote Raw → Ledger

Normalize a Raw-parsed row into typed Ledger entities. Idempotent. Re-running with the same input returns the same Ledger row ids.

```http
POST /v1/ledger/normalize
Authorization: Bearer <token>
Content-Type: application/json

{
  "raw_parsed_id":  "rp_001"
}
```

```json
{
  "ledger_rows_created": [
    { "entity": "transaction", "id": "tx_001" },
    { "entity": "counterparty", "id": "cp_aws" }
  ]
}
```

Normalization derives every entity implied by the parsed row. The route accepts an optional `target_entities` field for forward compatibility, but the current handler ignores it: it reads only `raw_parsed_id` and produces the full entity set the parsed row supports.

### Reconciliation

Trigger an async reconciliation pass:

```http
POST /v1/ledger/reconcile
Authorization: Bearer <token>
Content-Type: application/json

{ "since": "2026-03-01", "match_types": ["transaction_receipt", "invoice_payment"] }
```

`202 Accepted` → `{ "job_id": "rec_4711" }`.

List matches:

```http
GET /v1/ledger/reconciliation-matches?status=matched&match_type=invoice_payment
Authorization: Bearer <token>
```

```json
{
  "matches": [
    {
      "id": "rm_001",
      "match_type": "invoice_payment",
      "left_entity_type": "invoice",
      "left_entity_id": "inv_8231",
      "right_entity_type": "transaction",
      "right_entity_id": "tx_001",
      "confidence_score": 0.97,
      "status": "matched",
      "evidence_ids": ["rp_001"],
      "explanation": "amount + counterparty + date within tolerance"
    }
  ]
}
```

`match_type` enum: `transaction_receipt | invoice_payment | statement_balance | wallet_transfer | payroll_bank_debit | subscription_charge | card_charge | onchain_settlement | obligation_duplicate`. `status`: `unmatched | matched | partially_matched | duplicate_possible | disputed | cleared | failed | reversed`.

### Provenance on Every Row

Every Ledger row carries:

\| Field | Description | | --------------------------- | ------------------------------------------ | -------- | --------- | --------------- | ------------------ | | `source_ids` | Raw artifact ids that produced it | | `evidence_ids` | Raw-parsed row ids the extractor consulted | | `provenance` | `extracted | inferred | ambiguous | human_confirmed | agent_contributed` | | `confidence` | Calibrated 0 to 1 score | | `created_at` / `updated_at` | Bitemporal timestamps |

{% hint style="info" %}
Records are immutable. Corrections are written as **superseding** records that reference what they correct (`supersedes` field). The history is preserved end-to-end. Query `GET /v1/audit/entity/{entityType}/{entityId}` for the full causal trail.
{% endhint %}

### 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>🧠 Wiki API</strong></td><td>Reason over the Ledger in natural language.</td><td><a href="/pages/m25lyIz3DPEyKYuR5mU1">/pages/m25lyIz3DPEyKYuR5mU1</a></td><td></td></tr><tr><td><strong>📥 Raw and Ledger</strong></td><td>The conceptual model.</td><td><a href="/pages/pPTXzUZ6cZ8LCgvMmMRO">/pages/pPTXzUZ6cZ8LCgvMmMRO</a></td><td></td></tr></tbody></table>


# Wiki API

Natural-language and structured access to the tenant's memory graph. The Wiki is downstream of the Ledger. Narrative, evidence-cited recall. And is never the source of truth for balances, transactions, or permissions.

| Operation                        | Endpoint                                   |
| -------------------------------- | ------------------------------------------ |
| Ask a natural-language question  | `POST /v1/wiki/question`                   |
| Get suggested questions          | `GET /v1/wiki/suggested-questions`         |
| Get persisted assistant records  | `GET /v1/assistant/questions`              |
| Search entities                  | `GET /v1/wiki/search`                      |
| Get an entity                    | `GET /v1/wiki/entity/{entity_id}`          |
| Evidence chain for an entity     | `GET /v1/wiki/entity/{entity_id}/evidence` |
| Temporal history for an entity   | `GET /v1/wiki/entity/{entity_id}/history`  |
| Annotate (human correction)      | `POST /v1/wiki/annotate`                   |
| Get the entity-kind JSON Schemas | `GET /v1/wiki/schema`                      |
| List memory pages                | `GET /v1/memory/pages`                     |
| Get a memory page                | `GET /v1/memory/pages/{slug_or_id}`        |
| Regenerate a memory page         | `POST /v1/memory/regenerate`               |
| Search memory pages              | `GET /v1/memory/search`                    |

### Ask a Question

```http
POST /v1/wiki/question
Authorization: Bearer <token>
Content-Type: application/json

{
  "question":             "What did we spend on AWS last quarter, by environment?",
  "as_of":                "2026-03-31T23:59:59Z",
  "max_evidence_depth":   3
}
```

```json
{
  "question": "How many transactions do I have in June 2026?",
  "answered": true,
  "answer": "You have 19 transactions in June 2026.",
  "evidence": [
    {
      "entityType": "transaction",
      "entityId": "tx_01HQ7K3AAAAAAAAAAAAAAAAAAAA",
      "excerpt": "outflow 500.00 USD on 2026-06-12 cp=cp_example vendor payment"
    }
  ],
  "model": "structured-ledger-query",
  "usage": { "inputTokens": 0, "outputTokens": 0 }
}
```

`question` is 1–2000 chars. `max_evidence_depth` defaults to 3 (max 5). Transaction count, sum, and average questions with an unambiguous transaction scope run as deterministic Ledger queries. Listing questions with an explicit `show`, `list`, or `display` intent plus a recency, count, or date bound also route deterministically: transactions and cash flow return transaction rows, while invoice listings return invoice rows. Other questions use the LLM path and may incur per-call costs. `answered` is the machine-readable result status: `true` means the response is grounded or deterministic; `false` means `answer` is a refusal rather than an answer. Every response carries cited Ledger evidence.

### Suggested Questions

```http
GET /v1/wiki/suggested-questions
Authorization: Bearer <token>
```

```json
{
  "suggestions": [
    {
      "intent_id": "transaction_listing",
      "display_text": "Show my last 10 transactions",
      "usage_rank_score": 4
    }
  ]
}
```

This endpoint requires `wiki:read`. It returns only deterministic questions that are eligible against the calling tenant's current Ledger data. An intent with no matching rows is omitted. `usage_rank_score` is the calling tenant's all-time invocation count for that intent, so clients can rank suggestions without sharing usage data between tenants. The deterministic intent registry is the single source for both question execution and this endpoint, so a suggestion cannot advertise a question that the grounded Q\&A layer cannot answer.

`GET /v1/assistant/questions` is a separate, legacy persisted-record feed. It returns rows from `assistant_questions` and does not evaluate deterministic intent eligibility. Clients that need tenant-aware question suggestions must use `GET /v1/wiki/suggested-questions`.

### Search Entities

```http
GET /v1/wiki/search?kind=policy&q=wire&limit=10
Authorization: Bearer <token>
```

```json
{
  "results": [
    {
      "id": "ent_policy_v4",
      "kind": "policy",
      "attributes": { "name": "Wire approval policy v4" },
      "valid_from": "2025-01-15",
      "valid_to": null,
      "provenance": "human_confirmed",
      "confidence": 1.0,
      "source_evidence": ["raw_8231"]
    }
  ],
  "next_cursor": null
}
```

Query params: `kind` (`policy | agent`), `q` (full-text), `semantic` (pgvector), `since`, `until`, `limit` (default 50, max 500), `cursor`. Pass `semantic=<string>` to run a pgvector similarity search instead of (or in addition to) full-text.

Wiki search returns only Wiki-resident kinds. The four Ledger kinds (`account`, `counterparty`, `transaction`, `obligation`) are rejected with `request_params_invalid` and a redirect hint to the corresponding `/v1/ledger/*` endpoint, since financial truth lives in the Ledger, not the Wiki.

### Get an Entity

```http
GET /v1/wiki/entity/{entity_id}?include_neighbors=true&as_of=2026-03-31T23:59:59Z
Authorization: Bearer <token>
```

```json
{
  "entity": {
    "id": "cp_aws",
    "kind": "counterparty",
    "attributes": { "name": "Amazon Web Services", "tax_id": "..." },
    "valid_from": "2025-01-15",
    "valid_to": null,
    "provenance": "extracted",
    "confidence": 0.97,
    "source_evidence": ["raw_8231"]
  },
  "neighbors": [
    { "relation": { "type": "billed_via" }, "entity": { "id": "acct_aws_main", "kind": "account" } }
  ]
}
```

`as_of` enables bitemporal reads. The entity as it was known at that moment.

### Evidence Chain

The full provenance trail behind a Wiki entity:

```http
GET /v1/wiki/entity/{entity_id}/evidence
Authorization: Bearer <token>
```

```json
{
  "entity_id": "cp_aws",
  "chain": [
    {
      "raw_parsed_id": "rp_001",
      "parser": "invoice_v2",
      "confidence": 0.98,
      "extracted_fields": ["counterparty.name", "counterparty.tax_id"]
    }
  ]
}
```

### Temporal History

Every version of the entity, oldest first:

```http
GET /v1/wiki/entity/{entity_id}/history
Authorization: Bearer <token>
```

```json
{
  "entity_id": "cp_aws",
  "versions": [
    { "id": "cp_aws", "valid_from": "2025-01-15", "valid_to": "2025-06-01", "attributes": {...} },
    { "id": "cp_aws", "valid_from": "2025-06-01", "valid_to": null,         "attributes": {...} }
  ]
}
```

### Annotate (Human Correction)

A human can correct a Wiki entity or relation; the annotation is applied as a **new temporal version** with `provenance: "human_confirmed"` rather than mutating the prior row.

```http
POST /v1/wiki/annotate
Authorization: Bearer <token>
Content-Type: application/json

{
  "entity_id":   "cp_aws",
  "corrections": { "name": "Amazon Web Services, Inc." },
  "note":        "Updated to legal name from latest contract"
}
```

The body is `oneOf`: an `EntityAnnotation` (above) or a `RelationAnnotation` (`relation_id` instead of `entity_id`).

```json
{ "annotation_id": "ann_001", "new_version_id": "cp_aws_v3" }
```

### Get the Entity-Kind Schemas

The JSON Schema(s) describing every Wiki entity kind:

```http
GET /v1/wiki/schema?kind=counterparty
Authorization: Bearer <token>
```

Returns `{ counterparty: <JSON Schema document>, ... }`. Omit `kind` for the full set.

### Memory Pages

Memory pages are pre-rendered narrative views (Markdown) over the Ledger graph. "the AWS page," "Q1 cash flow," "vendor X relationship." Browsable and searchable.

```http
GET /v1/memory/pages?page_type=counterparty&q=AWS
Authorization: Bearer <token>
```

```json
{
  "pages": [
    {
      "id": "wp_001",
      "page_type": "counterparty",
      "subject_id": "cp_aws",
      "slug": "amazon-web-services",
      "body_md": "# Amazon Web Services\n...",
      "rendered_at": "2026-05-28T11:00:00Z",
      "source_revision": "rev_4127"
    }
  ]
}
```

`page_type` enum: `account | counterparty | obligation | invoice | agent | policy | monthly_summary | cash_flow`.

| Page type         | Subject                                                              |
| ----------------- | -------------------------------------------------------------------- |
| `account`         | One Ledger account and its balances, transactions, and evidence      |
| `counterparty`    | One customer, vendor, merchant, bank, wallet, or related party       |
| `obligation`      | One payable, receivable, payroll, tax, subscription, loan, or bill   |
| `invoice`         | One invoice-shaped receivable with linked documents and transactions |
| `agent`           | One agent definition or run context page                             |
| `policy`          | One policy document or active-policy summary                         |
| `monthly_summary` | One month of tenant cash movement, balances, invoices, and bills     |
| `cash_flow`       | A cash-flow period or forecast page                                  |

These are the same page types proposal evidence may cite as `wiki_entity` targets. Ledger-backed evidence is still resolved through the Ledger evidence resolver first; Wiki pages provide narrative context, not financial truth.

Get one page by slug or id:

```http
GET /v1/memory/pages/{slug_or_id}
Authorization: Bearer <token>
```

Regenerate a page (after the underlying Ledger has changed):

```http
POST /v1/memory/regenerate
Authorization: Bearer <token>
Content-Type: application/json

{ "slug_or_id": "amazon-web-services" }
```

Search memory pages by content:

```http
GET /v1/memory/search?q=cloud%20overspend&limit=20
Authorization: Bearer <token>
```

```json
{
  "results": [
    { "page": { "id": "wp_001", "slug": "amazon-web-services", ... }, "score": 0.91 }
  ]
}
```

### Provenance Fields

\| Field | Description | | ----------------- | --------------------------------------------------- | -------- | --------- | --------------- | ------------------ | | `evidence_path` | Ledger and Raw refs the answer depends on (Q\&A) | | `source_evidence` | Raw refs the entity was extracted from (entity get) | | `provenance` | `extracted | inferred | ambiguous | human_confirmed | agent_contributed` | | `confidence` | Calibrated 0 to 1 score |

### 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>🧠 The Wiki</strong></td><td>The conceptual model.</td><td><a href="/pages/IDbmiD3RRs6QlgR8UT8z">/pages/IDbmiD3RRs6QlgR8UT8z</a></td><td></td></tr></tbody></table>


# Policy API

Compose a policy, sign it, activate it, evaluate proposed actions against it, and lint or simulate before signing. Every Policy route is tenant-scoped: the `{tenant_id}` (UUID) appears in the path, and your token's tenant must match.

| Operation                  | Endpoint                                          |
| -------------------------- | ------------------------------------------------- |
| Get the active policy      | `GET /v1/policy/{tenant_id}`                      |
| Compose a candidate policy | `POST /v1/policy/{tenant_id}/compose`             |
| Sign + activate            | `POST /v1/policy/{tenant_id}/sign`                |
| List versions              | `GET /v1/policy/{tenant_id}/versions`             |
| Evaluate an action         | `POST /v1/policy/{tenant_id}/evaluate`            |
| Lint a draft               | `POST /v1/policy/{tenant_id}/lint`                |
| Simulate against a version | `POST /v1/policy/{tenant_id}/simulate`            |
| Replay a period            | `POST /v1/policy/{tenant_id}/simulate-historical` |
| Diff two versions          | `POST /v1/policy/{tenant_id}/diff`                |

There is no separate `register` or `revoke` endpoint. Activation happens at `sign`, and superseding a version means signing a new one. The previous active version is recorded in `versions` history.

### Compose a Candidate Policy

The DSL is structured JSON, not prose. The compose route validates it and returns the canonical hash plus the EIP-712 typed-data payload the tenant signers will sign.

```http
POST /v1/policy/{tenant_id}/compose
Authorization: Bearer <token>
Content-Type: application/json

{
  "content": {
    "version": 5,
    "rules": [
      {
        "id": "rule_invoice_under_5k",
        "applies_to": ["outbound_payment"],
        "when": {
          "amount.lte": { "currency": "USD", "value": "5000" },
          "counterparty.in": "vendors.trusted"
        },
        "execute": "auto"
      },
      {
        "id": "rule_invoice_above_5k",
        "applies_to": ["outbound_payment"],
        "when": { "amount.gt": { "currency": "USD", "value": "5000" } },
        "require": "cfo_approval",
        "execute": "confirm"
      }
    ],
    "lists": { "vendors.trusted": ["cp_aws", "cp_gcp"] }
  }
}
```

The `content` wrapper and the numeric `version` are mandatory. An optional top-level `quorum_required` sets how many distinct authorized signers `sign` needs (default `1`).

Response:

```json
{
  "policy_id":       "pol_8231",
  "state":           "pending_signatures",
  "signing_payload": { "domain": {...}, "types": {...}, "message": {...} }
}
```

`execute` is one of `auto | confirm | reject`. These are the rule-level outcomes that produce the policy decision (`allow | confirm | reject`).

### Sign and Activate

Each required signer signs the `signing_payload` from `compose`, then someone (any caller with `policy:sign`) submits the `policy_id` and all signatures together:

```http
POST /v1/policy/{tenant_id}/sign
Authorization: Bearer <token>
Content-Type: application/json

{
  "policy_id": "pol_8231",
  "signatures": [
    { "address": "0xCFO...", "signature": "0x..." },
    { "address": "0xCTO...", "signature": "0x..." }
  ]
}
```

`200 OK` with the serialized policy, an `activated` flag, and any activation `warnings`:

```json
{
  "policy": {
    "id":             "pol_8231",
    "version":        4,
    "state":          "active",
    "content":        { "version": 4, "rules": [...] },
    "content_hash":   "abc123...",
    "signers":        [...],
    "quorum_required": 2,
    "activated_at":   "2026-05-28T12:00:00Z",
    "deactivated_at": null,
    "created_by":     "usr_...",
    "created_at":     "2026-05-28T11:59:00Z"
  },
  "activated": true,
  "warnings":  []
}
```

The signature count reaching `quorum_required` flips the policy to `active` (`activated: true`). Below quorum, the signatures are recorded and the policy stays `pending_signatures`. A signature that fails to verify, a duplicate signer, or a signer that is not an authorized tenant signer returns `401` with `policy_signature_invalid`. Signing a policy that is not awaiting signatures returns `409` with `policy_quorum_not_met`.

### Get the Active Policy

```http
GET /v1/policy/{tenant_id}
Authorization: Bearer <token>
```

Returns the currently active `Policy` for the tenant (`404` if none has been activated). For a specific historical version, use `/versions`.

### List Versions

```http
GET /v1/policy/{tenant_id}/versions
Authorization: Bearer <token>
```

```json
{
  "versions": [
    {
      "id": "pol_8231",
      "version": 4,
      "content_hash": "0xabc...",
      "activated_at": "2026-05-28T...",
      "deactivated_at": null
    },
    {
      "id": "pol_5417",
      "version": 3,
      "content_hash": "0x111...",
      "activated_at": "2026-03-01T...",
      "deactivated_at": "2026-05-28T..."
    }
  ]
}
```

### Evaluate an Action

Dry-run an action against the active policy. This is the same evaluator the §6 pre-execution gate uses internally; it does **not** propose, reserve, or audit. It just returns the decision.

```http
POST /v1/policy/{tenant_id}/evaluate
Authorization: Bearer <token>
Content-Type: application/json

{
  "action": {
    "kind":            "outbound_payment",
    "counterparty_id": "cp_aws",
    "amount":          { "currency": "USD", "value": "7800" }
  }
}
```

```json
{
  "outcome": "confirm",
  "matched_rule_id": "rule_invoice_above_5k",
  "required_approvers": ["cfo"],
  "trace": [
    {
      "rule_id": "rule_invoice_under_5k",
      "matched": false,
      "checks": [{ "key": "amount.lte", "passed": false, "detail": "USD 5000" }]
    },
    {
      "rule_id": "rule_invoice_above_5k",
      "matched": true,
      "checks": [{ "key": "amount.gt", "passed": true, "detail": "USD 5000" }]
    }
  ]
}
```

### Three Possible Decisions

| Decision  | Meaning                                                                               |
| --------- | ------------------------------------------------------------------------------------- |
| `allow`   | Rule matched with `execute: "auto"`; action can proceed straight to the §6 gate       |
| `confirm` | Rule matched with `execute: "confirm"`; named `required_approvers` must sign first    |
| `reject`  | Rule matched with `execute: "reject"`, or no rule matched the default-deny vocabulary |

Casing is **lowercase**. `allow | confirm | reject`. The historical-simulation counters mirror it (`would_allow`, `would_confirm`, `would_reject`).

#### Unmatched high-risk agent actions

The policy VM remains deny-by-default. A normal unmatched rule evaluation still returns `reject`, and low-stakes or money-path proposal types keep their existing fail-closed behavior.

There is one compatibility wrapper for reviewable high-risk non-money proposals: `PolicyService.evaluateLegacy` converts unmatched `agent_action` fallthroughs for `collections`, `fraud_anomaly`, and `vendor_risk` to `confirm` with `required_approvers: ["signer"]`. The same fallback also applies to stored high-risk action types such as `flag_transaction`, `block_payment`, `flag_vendor_risk`, `freeze_card`, `create_dispute_draft`, `require_approval`, and `escalate`. This keeps suspicious transactions, vendor-risk holds, and collections follow-ups visible for human review instead of silently closing them as policy rejections. `payment` proposals are deliberately not included in this fallback.

#### Decision vocabulary across surfaces

`allow | confirm | reject` is the canonical protocol decision. The rule-level `execute` field and the SDK use aliases that map 1:1; the PaymentIntent status reflects the same outcome:

| Protocol decision (HTTP/MCP) | Rule-level `execute` | SDK `decision.outcome` / `action.status` | Resulting PaymentIntent status |
| ---------------------------- | -------------------- | ---------------------------------------- | ------------------------------ |
| `allow`                      | `auto`               | `auto`                                   | `approved`                     |
| `confirm`                    | `confirm`            | `needs_approval`                         | `pending_approval`             |
| `reject`                     | `reject`             | `rejected`                               | `rejected`                     |

Compare against `allow | confirm | reject` over HTTP/MCP; the `auto | needs_approval | rejected` triple is an SDK alias, not the protocol vocabulary.

### Action Vocabulary

The evaluate `action.kind` is one of the following. There is no `rail` field on the evaluate action.

| `kind`             | Domain                                            |
| ------------------ | ------------------------------------------------- |
| `outbound_payment` | Money leaving (ACH, wire, on-chain, x402, escrow) |
| `inbound_payment`  | Money arriving                                    |
| `ledger_write`     | A Ledger-row mutation (e.g. agent normalization)  |
| `onchain_tx`       | A non-payment on-chain transaction                |
| `agent_action`     | A non-money agent action gated by policy          |
| `any`              | Only valid inside a rule's `applies_to` catch-all |

A rule's `applies_to` accepts the same `kind` values, including `any`. (The PaymentIntent layer uses a separate, broader `action_type` set. `ach_outbound`, `wire`, `x402_settle`, etc.. Those map onto the `kind` values internally.)

Proposal read APIs expose a separate public proposal `type`. Stored agent `action_type` values are preserved as `stored_action_type` and then mapped to a public proposal type through the proposal read model. For example, `flag_transaction` maps to `fraud_anomaly`, `block_payment` maps to `vendor_risk`, and `propose_match` maps to `reconciliation`. Ambiguous stored actions such as `notify`, `escalate`, `create_task`, and `recommend_action` use the agent role or kind instead of action-name guessing.

### Lint a Draft

Before composing, run a linter against a policy-content blob to catch shape / semantic problems:

```http
POST /v1/policy/{tenant_id}/lint
Authorization: Bearer <token>
Content-Type: application/json

{ "policy_content": { "rules": [...] } }
```

```json
{
  "tenant_id": "acme",
  "errors": 0,
  "warnings": 2,
  "findings": [
    {
      "code": "rule_amount_currency_missing",
      "severity": "WARN",
      "rule_id": "rule_3",
      "message": "amount.gt without explicit currency"
    }
  ]
}
```

### Simulate Against a Version

Replay a single action against a specific historical policy version:

```http
POST /v1/policy/{tenant_id}/simulate
Authorization: Bearer <token>
Content-Type: application/json

{
  "action":  { "kind": "outbound_payment", "counterparty_id": "cp_aws", "amount": { "currency": "USD", "value": "7800" } },
  "version": 3
}
```

Returns `{ "decision": <same shape as /evaluate>, "policy_version": 3 }`. Unlike `/evaluate`, simulate wraps the decision with the `policy_version` it replayed against.

### Replay a Period

Replay every action in a time window against a candidate (unsigned) policy. Useful for asking "would version 5 have changed anything?":

```http
POST /v1/policy/{tenant_id}/simulate-historical
Authorization: Bearer <token>
Content-Type: application/json

{
  "policy_content": { "rules": [...] },
  "period_start":   "2026-01-01",
  "period_end":     "2026-04-30"
}
```

```json
{
  "total": 4127,
  "would_allow": 3902,
  "would_confirm": 201,
  "would_reject": 24,
  "diff_vs_active": { "newly_rejected": 7, "newly_confirmed": 14, "loosened": 0 }
}
```

### Diff Two Versions

```http
POST /v1/policy/{tenant_id}/diff
Authorization: Bearer <token>
Content-Type: application/json

{ "from_version": 3, "to_version": 4 }
```

```json
{
  "from_version": 3,
  "to_version": 4,
  "added": ["rule_x402_micropayment_cap"],
  "removed": [],
  "modified": [
    {
      "rule_id": "rule_invoice_above_5k",
      "field": "require",
      "before": null,
      "after": "cfo_approval"
    }
  ]
}
```

### 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>📋 Policy and Permissioning</strong></td><td>The conceptual model.</td><td><a href="/pages/GSe2ntE9CLqxoQAiQuzG">/pages/GSe2ntE9CLqxoQAiQuzG</a></td><td></td></tr><tr><td><strong>📜 BrainPolicyRegistry</strong></td><td>The on-chain registry.</td><td><a href="/pages/Qk74oUUATzrLis1xhGcb">/pages/Qk74oUUATzrLis1xhGcb</a></td><td></td></tr></tbody></table>


# Agents API

Register external agents, list the first-party agent catalog, route events to agents, run agents end-to-end, inspect runs, and halt agents.

| Operation                                             | Endpoint                                             |
| ----------------------------------------------------- | ---------------------------------------------------- |
| Register an external agent                            | `POST /v1/execution/agents/register`                 |
| List first-party agent catalog                        | `GET /v1/agents`                                     |
| Get an agent (definition; registration not yet wired) | `GET /v1/agents/{agent_id}`                          |
| List an agent's actions                               | `GET /v1/agents/{agent_id}/actions`                  |
| Route an event/intent                                 | `POST /v1/agents/route`                              |
| Run an agent end-to-end                               | `POST /v1/agents/run`                                |
| Enqueue an event for async routing                    | `POST /v1/agents/events`                             |
| Inspect a routing decision                            | `GET /v1/agents/routing-decisions/{id}`              |
| List runs                                             | `GET /v1/agents/runs`                                |
| Run detail                                            | `GET /v1/agents/runs/{run_id}`                       |
| Why a run did what it did                             | `GET /v1/agents/runs/{run_id}/why`                   |
| Evidence used for a run                               | `GET /v1/agents/runs/{run_id}/evidence`              |
| §6 gate trace for a run                               | `GET /v1/agents/runs/{run_id}/gate-trace`            |
| Canonical Proof for a run                             | `GET /v1/agents/runs/{run_id}/proof`                 |
| Halt one agent                                        | `POST /v1/agents/{agent_id}/halt`                    |
| Halt every agent in a category                        | `POST /v1/agents/halt-category`                      |
| MCP JSON-RPC entry                                    | `POST /v1/agents/mcp` (see MCP Server API Reference) |

{% hint style="warning" %}
`POST /v1/agents/register` and `POST /v1/agents/{agent_id}/propose` are marked **deprecated** in the spec and **return 404** today. Register external agents via `POST /v1/execution/agents/register` (below), and propose actions through `POST /v1/agents/run` (which routes → resolves → dry-runs the §6 gate → proposes through the gated path).
{% endhint %}

### Register an External Agent

External agents are registered by an `execution:admin` caller. Brain stores the record in a `pending_onchain` state; the caller supplies the identity fields and, optionally, the on-chain attestation references (`scope_hash`, `onchain_address`, `registered_tx`) once the `BrainMCPAgentRegistry` write exists.

```http
POST /v1/execution/agents/register
Authorization: Bearer <tenant token>
Content-Type: application/json

{
  "agent_id":        "ag_reconciliation_v1",
  "role":            "reconciliation",
  "display_name":    "Reconciliation Agent",
  "scope_hash":      "abc123...",
  "onchain_address": "0xagent...",
  "registered_tx":   "0x..."
}
```

`agent_id`, `role`, and `display_name` are required; `scope_hash` (hex), `onchain_address`, and `registered_tx` are optional.

Response (`201 Created`):

```json
{
  "id": "ag_reconciliation_v1",
  "kind": "external",
  "role": "reconciliation",
  "display_name": "Reconciliation Agent",
  "scope_hash": "abc123...",
  "onchain_address": "0xagent...",
  "state": "pending_onchain",
  "registered_tx": "0x...",
  "registered_at": null
}
```

The agent then connects over MCP using a JWT whose `scope_hash` claim must equal the `scope_hash` stored on-chain. The MCP server verifies that match on every call.

### List the First-Party Agent Catalog

`GET /v1/agents` returns the **internal** first-party agent definitions (capability, category, default-enabled state). Not the external-agent registry.

```http
GET /v1/agents?category=business&state=enabled
Authorization: Bearer <token>
```

```json
{
  "agents": [
    {
      "agent_key": "collections",
      "provenance": "first_party",
      "category": "business",
      "capabilities": ["invoice_followup", "dunning"],
      "enabled_by_default": true
    }
  ]
}
```

Filters: `kind`, `capability`, `category` (`business | consumer | agnostic`), `state` (`enabled | disabled`).

### Get an Agent

```http
GET /v1/agents/{agent_id}
Authorization: Bearer <token>
```

Returns `{ "definition": <catalog def with` shadow\_mode`>, "registration": null }`. The on-chain registration join is not wired yet, so `registration` is always `null` today; the `BrainMCPAgentRegistry` reader is a pending follow-up.

### Route an Event

The router scores candidate agents by capability + tenant scope grants + evidence and returns the best one. Routing is advisory. The selected agent still proposes through the gated path. The selection is itself an audit event.

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

{
  "event":   "invoice.overdue",
  "context": { "invoice_id": "inv_8231", "counterparty_id": "cp_x" }
}
```

Provide `event` (a domain-event name) **or** `intent` (free-form text), plus optional `context`. Tenant-equality required.

```json
{
  "selected_agent_id": "collections",
  "fallback_agent_ids": [],
  "confidence": 0.92,
  "evidence_score": 1,
  "policy_status": "routed",
  "execution_mode": "propose",
  "reason": "selected collections (confidence 0.92)"
}
```

| Field                | Meaning                                                              |
| -------------------- | -------------------------------------------------------------------- |
| `selected_agent_id`  | The chosen agent, or `null` when nothing matches                     |
| `fallback_agent_ids` | Other eligible agents, best first                                    |
| `confidence`         | Router confidence in the selection (0..1)                            |
| `evidence_score`     | Fraction of the agent's required evidence that is present (0..1)     |
| `policy_status`      | `routed`, `unscoped` (matched but tenant scoped none), or `no_match` |
| `execution_mode`     | `execute`, `propose`, `confirm`, `notify_only`, `reject`, or `null`  |

### Run an Agent End-to-End

The full route → resolve action → dry-run §6 gate → persist `agent_runs` row → propose pipeline. **Money-movers are shadowed by default**. A financial proposal from an un-promoted agent terminates as `shadow_completed` and moves no money. Going live is a deliberate per-agent promotion with strict caps + allowlisted rails.

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

{ "event": "invoice.overdue", "context": { "invoice_id": "inv_8231" } }
```

```json
{
  "status":              "proposal_created",
  "routing_decision_id": "rd_001",
  "run_id":              "run_001",
  "selected_agent_id":   "collections",
  "action":              { "type": "outbound_payment", ... },
  "shadow_mode":         false,
  "proposed":            { "id": "pi_a1b2c3", "status": "pending_approval", "policy_decision_id": "pd_7331" },
  "reason":              "matched dunning rule for inv_8231"
}
```

A proposal-layer idempotency collision returns `409` with `agent_proposal_duplicate`.

### Enqueue an Event (async)

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

{ "event": "invoice.overdue", "context": { "invoice_id": "inv_8231" } }
```

```json
{ "job_id": "job_001", "status": "queued" }
```

### Routing Decisions & Run History

| Endpoint                                  | Purpose                                                                             |
| ----------------------------------------- | ----------------------------------------------------------------------------------- |
| `GET /v1/agents/routing-decisions/{id}`   | Routing decision detail                                                             |
| `GET /v1/agents/runs`                     | List runs (filter `agent_id`, `status`, `category`, `limit`)                        |
| `GET /v1/agents/runs/{run_id}`            | Run summary (`status` ∈ `completed`, `failed`, `shadow_completed`, `rejected`)      |
| `GET /v1/agents/runs/{run_id}/why`        | Structured reason + (redacted) reasoning trace + candidate agents + `behavior_hash` |
| `GET /v1/agents/runs/{run_id}/evidence`   | Evidence the run consulted                                                          |
| `GET /v1/agents/runs/{run_id}/gate-trace` | The §6 gate-check rows for the run's PaymentIntent                                  |
| `GET /v1/agents/runs/{run_id}/proof`      | Proxy to the canonical Proof artifact for the run's PaymentIntent                   |
| `GET /v1/agents/{agent_id}/actions`       | All actions a given agent produced (proposal + payment\_intent + status)            |

### Kill-Switch

| Endpoint                          | Purpose                                                                       |
| --------------------------------- | ----------------------------------------------------------------------------- |
| `POST /v1/agents/{agent_id}/halt` | Pause every in-flight intent for the agent and set its state to `quarantined` |
| `POST /v1/agents/halt-category`   | Emergency-stop every agent in a category. Body `{ "category": "business" }`   |

Both routes are tenant-root and emit audit events. Halting an agent atomically pauses its in-flight PaymentIntents (the rail dispatcher re-reads state immediately before submission and aborts cleanly if the intent was paused).

### 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>📤 Payment Intents</strong></td><td>The Ledger entity agents propose.</td><td><a href="/pages/5QWRsON4u2eB2ZQRS5b1">/pages/5QWRsON4u2eB2ZQRS5b1</a></td><td></td></tr><tr><td><strong>📜 BrainMCPAgentRegistry</strong></td><td>The on-chain registry.</td><td><a href="/pages/7cGQBqLnTUZjyofcuHlm">/pages/7cGQBqLnTUZjyofcuHlm</a></td><td></td></tr></tbody></table>


# Governance API

Review registered agents, change agent lifecycle state, and build audit-derived governance reports for compliance workflows.

{% hint style="info" %}
These governance routes are staging-only and BFF-only today. They use `X-Platform-Service-Auth` with the `governance:read` scope, not an end-user bearer token.
{% endhint %}

| Operation                         | Endpoint                                 |
| --------------------------------- | ---------------------------------------- |
| List registered agents            | `GET /v1/governance/agents`              |
| Get one registered agent          | `GET /v1/governance/agents/{agent_id}`   |
| Pause, resume, or revoke an agent | `PATCH /v1/governance/agents/{agent_id}` |
| Build a governance report         | `GET /v1/governance/reports`             |
| Create a report snapshot          | `POST /v1/governance/reports/snapshot`   |
| Get a report snapshot             | `GET /v1/governance/reports/{report_id}` |

No external agent creation endpoint is exposed. Agents continue to be created through existing provisioning flows. The policy check catalog is not exposed in this cycle because it is deferred pending a future security and legal review.

### Authentication

All Governance API routes require the platform service header:

```http
X-Platform-Service-Auth: <secret-with-governance-read>
```

The platform credential must carry `governance:read`.

### List Registered Agents

```http
GET /v1/governance/agents
X-Platform-Service-Auth: <secret-with-governance-read>
```

Query parameters:

| Parameter   | Required | Notes                                                                   |
| ----------- | -------- | ----------------------------------------------------------------------- |
| `tenant_id` | Yes      | Tenant whose agent registry should be listed.                           |
| `status`    | No       | `active`, `pending`, `quarantined`, or `revoked`.                       |
| `owner`     | No       | Phase 1 tenant owner alias. A non-matching value returns an empty list. |
| `limit`     | No       | Default `100`, maximum `500`.                                           |
| `cursor`    | No       | Cursor returned by the previous page.                                   |

```json
{
  "agents": [
    {
      "id": "agent_example",
      "tenant_id": "tnt_example",
      "kind": "internal",
      "role": "finance_ops",
      "display_name": "Finance Ops Agent",
      "status": "active",
      "scopes": null,
      "scope_hash": "abcdef",
      "onchain_address": null,
      "registered_tx": null,
      "registered_at": "2026-07-22T00:00:00.000Z",
      "created_at": "2026-07-22T00:00:00.000Z"
    }
  ],
  "next_cursor": null
}
```

`scopes` is `null` in Phase 1 because the registry stores `scope_hash`, not the original scope list.

### Get One Registered Agent

```http
GET /v1/governance/agents/agent_example
X-Platform-Service-Auth: <secret-with-governance-read>
```

```json
{
  "agent": {
    "id": "agent_example",
    "tenant_id": "tnt_example",
    "kind": "internal",
    "role": "finance_ops",
    "display_name": "Finance Ops Agent",
    "status": "active",
    "scopes": null,
    "scope_hash": "abcdef",
    "onchain_address": null,
    "registered_tx": null,
    "registered_at": "2026-07-22T00:00:00.000Z",
    "created_at": "2026-07-22T00:00:00.000Z",
    "lifecycle_events": [
      {
        "audit_event_id": "evt_example",
        "actor": "user_admin",
        "action": "governance.agent.lifecycle_changed",
        "policy_decision_id": null,
        "policy_check_id": null,
        "outcome": null,
        "created_at": "2026-07-22T00:00:00.000Z",
        "inputs": { "agent_id": "agent_example", "transition": "pause" },
        "outputs": { "after_state": "quarantined" }
      }
    ]
  }
}
```

### Change Agent Lifecycle

```http
PATCH /v1/governance/agents/agent_example
X-Platform-Service-Auth: <secret-with-governance-read>
Content-Type: application/json

{
  "tenant_id": "tnt_example",
  "transition": "pause",
  "reason": "review requested",
  "actor": "user_admin"
}
```

`transition` is one of `pause`, `resume`, or `revoke`. The route writes `governance.agent.lifecycle_changed` to the existing audit event store with the actor and reason.

```json
{
  "agent": {
    "id": "agent_example",
    "tenant_id": "tnt_example",
    "kind": "internal",
    "role": "finance_ops",
    "display_name": "Finance Ops Agent",
    "status": "quarantined",
    "scopes": null,
    "scope_hash": "abcdef",
    "onchain_address": null,
    "registered_tx": null,
    "registered_at": "2026-07-22T00:00:00.000Z",
    "created_at": "2026-07-22T00:00:00.000Z"
  }
}
```

### Build A Governance Report

```http
GET /v1/governance/reports
X-Platform-Service-Auth: <secret-with-governance-read>
```

Query parameters:

| Parameter      | Required | Notes                                         |
| -------------- | -------- | --------------------------------------------- |
| `tenant_id`    | Yes      | Tenant whose audit events should be reported. |
| `period_start` | Yes      | Inclusive report start timestamp.             |
| `period_end`   | Yes      | Exclusive report end timestamp.               |
| `agent_id`     | No       | Filters policy-relevant events to one agent.  |
| `format`       | No       | `json` by default, or `csv`.                  |

Reports include policy-relevant audit events in the requested period. Historical rows are joined to `policy_decisions` when `policy_decision_id` is present. `policy_decision_id` is not populated on all historical audit events, so rows without a native outcome or resolvable policy decision are returned with `decision_data_status` set to `unavailable` rather than omitted.

```json
{
  "tenant_id": "tnt_example",
  "period_start": "2026-07-01T00:00:00.000Z",
  "period_end": "2026-08-01T00:00:00.000Z",
  "summary": {
    "totals": {
      "proposed": 2,
      "approved": 1,
      "blocked": 0,
      "escalated": 0,
      "decision_data_unavailable": 1
    },
    "coverage": {
      "events": 2,
      "with_policy_decision_id": 1,
      "joined_policy_decision": 1,
      "with_native_outcome": 0
    }
  },
  "events": [
    {
      "audit_event_id": "evt_policy_joined",
      "created_at": "2026-07-22T00:00:00.000Z",
      "actor": "agent_example",
      "agent_id": "agent_example",
      "action": "payment_intent.execute.before",
      "policy_decision_id": "dec_example",
      "policy_check_id": "rule_example",
      "raw_policy_outcome": "allow",
      "outcome": "approved",
      "decision_data_status": "available",
      "unavailable_reason": null
    },
    {
      "audit_event_id": "evt_missing_decision",
      "created_at": "2026-07-22T00:01:00.000Z",
      "actor": "agent_example",
      "agent_id": "agent_example",
      "action": "agent.action.proposed",
      "policy_decision_id": null,
      "policy_check_id": null,
      "raw_policy_outcome": null,
      "outcome": null,
      "decision_data_status": "unavailable",
      "unavailable_reason": "policy_decision_id_missing"
    }
  ]
}
```

The full request and response schema is maintained in `Brain_API_Specification.yaml`.

### Create A Report Snapshot

```http
POST /v1/governance/reports/snapshot
X-Platform-Service-Auth: <secret-with-governance-read>
Idempotency-Key: <optional-retry-key>
Content-Type: application/json

{ "created_by": "user_admin" }
```

Query parameters:

| Parameter      | Required | Notes                                            |
| -------------- | -------- | ------------------------------------------------ |
| `tenant_id`    | Yes      | Tenant whose audit events should be reported.    |
| `period_start` | Yes      | Inclusive report start timestamp.                |
| `period_end`   | Yes      | Exclusive report end timestamp.                  |
| `agent_id`     | No       | Filters policy-relevant events to one agent.     |
| `format`       | No       | `json` only for snapshots. CSV is not persisted. |

Snapshot creation generates the same JSON `GovernanceReport` as `GET /v1/governance/reports`, stores that exact payload with its filters, and returns a `grpt_` report id. The stored payload is immutable.

`Idempotency-Key` is optional. When supplied, a retry with the same key and same snapshot request returns the original `201` response with the same `report_id`. Reusing the same key with different snapshot parameters returns `409`.

```json
{
  "report_id": "grpt_example",
  "snapshot": {
    "report_id": "grpt_example",
    "tenant_id": "tnt_example",
    "period_start": "2026-07-01T00:00:00.000Z",
    "period_end": "2026-08-01T00:00:00.000Z",
    "agent_id": null,
    "created_by": "user_admin",
    "created_at": "2026-07-22T00:02:00.000Z",
    "report": {
      "tenant_id": "tnt_example",
      "period_start": "2026-07-01T00:00:00.000Z",
      "period_end": "2026-08-01T00:00:00.000Z",
      "summary": {
        "totals": {
          "proposed": 2,
          "approved": 1,
          "blocked": 0,
          "escalated": 0,
          "decision_data_unavailable": 1
        },
        "coverage": {
          "events": 2,
          "with_policy_decision_id": 1,
          "joined_policy_decision": 1,
          "with_native_outcome": 0
        }
      },
      "events": []
    }
  }
}
```

### Get A Report Snapshot

```http
GET /v1/governance/reports/grpt_example
X-Platform-Service-Auth: <secret-with-governance-read>
```

Query parameters:

| Parameter   | Required | Notes                     |
| ----------- | -------- | ------------------------- |
| `tenant_id` | Yes      | Tenant that owns the row. |

This route returns the frozen snapshot and does not re-query the live audit store.


# Payment Intents API

The canonical Brain HTTP surface for proposing, approving, and executing financial actions is the **PaymentIntent** family. The `agent_id`-keyed proposal route from earlier drafts (`POST /v1/agents/{agent_id}/propose`) and the `/v1/actions/*` paths are **not implemented**. Both are documented as deprecated stubs in the spec and return 404. Use the routes below.

| Operation              | Endpoint                                            |
| ---------------------- | --------------------------------------------------- |
| Create (propose)       | `POST /v1/payment-intents`                          |
| Get                    | `GET /v1/payment-intents/{id}`                      |
| Approve (confirm-mode) | `POST /v1/payment-intents/{id}/approve`             |
| Reject                 | `POST /v1/payment-intents/{id}/reject`              |
| Execute (gated)        | `POST /v1/payment-intents/{id}/execute`             |
| Pause / Resume         | `POST /v1/payment-intents/{id}/{pause,resume}`      |
| Replay-investigation   | `GET /v1/payment-intents/{id}/replay-investigation` |
| Agent-driven full run  | `POST /v1/agents/run` (see Agents API)              |

### Propose a Payment

```http
POST /v1/payment-intents
Authorization: Bearer <token>
Content-Type: application/json

{
  "action_type":                "ach_outbound",
  "source_account_id":          "acct_ops",
  "destination_counterparty_id": "cp_aws",
  "amount":                     "7800.00",
  "currency":                   "USD",
  "invoice_id":                 "inv_8231",
  "evidence_ids":               ["rp_001"]
}
```

`action_type` is one of `ach_outbound | ach_inbound | wire | onchain_transfer | erp_writeback | card_payment | x402_settle | escrow_release`. `amount` is a decimal string. The valid `currency` depends on `action_type`: the two on-chain settlement actions (`x402_settle`, `escrow_release`) require `USDC` and reject three-letter codes, while every other action requires a three-letter code matching `^[A-Z]{3}$` and rejects `USDC`.

For the special invoice shortcut (resolves amount / currency / counterparty / source / evidence from a Ledger invoice):

```json
{ "type": "pay_invoice", "invoice_id": "inv_8231" }
```

Response (`201 Created`) is a full PaymentIntent with a PolicyDecision already attached:

```json
{
  "id": "pi_a1b2c3",
  "owner_id": "acme",
  "created_by_agent_id": "ag_payment_v1",
  "action_type": "ach_outbound",
  "source_account_id": "acct_ops",
  "destination_counterparty_id": "cp_aws",
  "amount": "7800.00",
  "currency": "USD",
  "invoice_id": "inv_8231",
  "status": "pending_approval",
  "policy_decision_id": "pd_7331",
  "approval_ids": [],
  "execution_receipt_ids": []
}
```

Errors: `400`, `403`, `404` (invoice not found / not accessible), `409` (invoice already paid / `agent_proposal_duplicate`), `422`.

### Get a PaymentIntent

```http
GET /v1/payment-intents/{id}
Authorization: Bearer <token>
```

Returns the same `PaymentIntent` shape as above. `404` if unknown or tenant-isolated.

### Status Lifecycle

| Status                     | Meaning                                                         |
| -------------------------- | --------------------------------------------------------------- |
| `proposed`                 | Created; Policy is evaluating                                   |
| `pending_approval`         | Policy returned `confirm`; awaiting approver signatures         |
| `awaiting_second_approval` | First approval recorded; a distinct second approver must sign   |
| `approved`                 | All required approvals collected (or Policy returned `allow`)   |
| `paused`                   | Kill-switch hold on an approved intent; resume re-runs the gate |
| `dispatching`              | Gate passed; execution enqueued to the outbox, settling async   |
| `rejected`                 | Policy returned `reject`, or an approver rejected               |
| `executed`                 | Rail dispatch succeeded                                         |
| `failed`                   | §6 gate failed or rail dispatch errored                         |
| `cancelled`                | Cancelled before approval, or from `paused → cancelled`         |

`dispatching` is a full PaymentIntent state, not an outbox-only one: `execute` transitions the intent `approved → dispatching` and it stays there until the outbox worker settles it to `executed` or `failed`. The `execution` row the worker drives has its own separate `ExecutionState` values (`dispatched`, `in_flight`, `completed`, `failed`).

**SDK status aliases.** The SDK's higher-level `action.status` collapses these HTTP states onto the policy-decision triple: `proposed` / `approved` → **`auto`**, `pending_approval` → **`needs_approval`**, `rejected` → **`rejected`**; `executed`, `failed`, and `cancelled` pass through unchanged. So SDK code branching on `"auto"` is matching the same state HTTP code sees as `approved`. See [Policy → decision vocabulary across surfaces](/api-reference/policy-api#decision-vocabulary-across-surfaces).

### Approve a `pending_approval` Intent

```http
POST /v1/payment-intents/{id}/approve
Authorization: Bearer <approver token>
```

No request body. Returns `200` with the updated `PaymentIntent`. Approvers are determined by Policy (the `confirm` rule's `required_approvers` / quorum); each approver hits this endpoint independently and the intent flips to `approved` once the quorum is met.

### Reject

```http
POST /v1/payment-intents/{id}/reject
Authorization: Bearer <approver token>
Content-Type: application/json

{ "reason": "Vendor on internal hold pending PO reconciliation" }
```

`reason` is optional (≤ 500 chars). Returns `200` with the rejected `PaymentIntent`.

### Execute an Approved Intent

```http
POST /v1/payment-intents/{id}/execute
Authorization: Bearer <token>
```

No request body. Runs the deterministic §6 pre-execution gate against live Ledger state, then atomically transitions the intent `approved → dispatching` and enqueues a `pending` outbox row. The outbox worker dispatches the rail and settles asynchronously.

`202 Accepted`:

```json
{
  "payment_intent_id": "pi_a1b2c3",
  "outbox_id": "ob_001",
  "execution_id": null,
  "rail": "bank_ach",
  "status": "dispatching"
}
```

`execution_id` is `null` on this immediate response and populated when the worker picks the row up. Settlement notifications arrive via the rail-specific webhook (e.g. Plaid `TRANSFER_EVENTS_UPDATE`).

A gate failure returns `409` with `payment_intent_gate_failed` and `details` naming the failing check (see Errors → Pre-execution gate failures).

### Rails

The `rail` returned on `execute` is **not** the same vocabulary as the create-time `action_type`. The mapping:

| `rail`          | Implementation                                                                               |
| --------------- | -------------------------------------------------------------------------------------------- |
| `bank_ach`      | Plaid Transfer (authorize → create; settled async via webhook)                               |
| `onchain_base`  | `BrainSmartAccount.executeViaSessionKey` (Base)                                              |
| `erp_writeback` | NetSuite SuiteTalk (fail-closed stub)                                                        |
| `x402_base`     | USDC-on-Base settlement (mapped from `x402_settle`; unregistered at boot, fail-closed)       |
| `escrow_base`   | `BrainEscrow` lock release (mapped from `escrow_release`; unregistered at boot, fail-closed) |
| `notification`  | Surface-to-human (no money path)                                                             |

The `x402_base` and `escrow_base` rails are **shadow-first**: they throw rather than fake-settle until promoted.

### Pause / Resume (Kill-Switch)

An `approved` intent can be held without a terminal transition, then released:

```http
POST /v1/payment-intents/{id}/pause      # approved → paused
POST /v1/payment-intents/{id}/resume     # paused → approved (re-runs the live §6 gate)
```

No request body for either. Resume re-evaluates the §6 gate against the **current** Ledger state. Defending against drift while paused. And returns `409` if any check now fails.

A halted agent (`POST /v1/agents/{agent_id}/halt`) pauses every one of its in-flight intents at once.

### Replay Investigation

```http
GET /v1/payment-intents/{id}/replay-investigation
Authorization: Bearer <token>
```

Typed forensic record. The intent, each execution (with its typed rail receipt), and the linking ids you'd join to reconstruct exactly what happened:

```json
{
  "payment_intent":     { "id": "pi_a1b2c3", "status": "executed", ... },
  "executions":         [ { "id": "ex_4711", "rail": "bank_ach", "rail_receipt": {...} } ],
  "policy_decision_id": "pd_7331",
  "evidence_ids":       ["rp_001"]
}
```

The policy decision and the audit chain are referenced by id and joined via their owning service APIs (Policy + Audit).

### Agent-Driven Runs

Most agent activity goes through the higher-level run endpoint, which routes → resolves an action → dry-runs the §6 gate → persists an `agent_runs` row → proposes through this same gated path:

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

{ "event": "invoice.overdue", "context": { "invoice_id": "inv_8231" } }
```

See the Agents API for the full run / routing / kill-switch surface.

### 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>📜 Audit API</strong></td><td>Pull proofs for executed PaymentIntents.</td><td><a href="/pages/BqeKz3FmbRDmRILK0zaA">/pages/BqeKz3FmbRDmRILK0zaA</a></td><td></td></tr><tr><td><strong>🤖 Agents API</strong></td><td>Register agents, route events, run agents.</td><td><a href="/pages/6zFwU1VB8lTyM59Se4l7">/pages/6zFwU1VB8lTyM59Se4l7</a></td><td></td></tr></tbody></table>


# Proposals and Evidence API

Read agent proposals, record a human decision, and resolve proposal evidence.

The Proposals API is the customer-facing surface over everything Brain's agents produce. It unifies money-path payment intents and non-money agent findings into one tenant-scoped, cursor-paginated feed, lets a human decide on any one of them, and resolves the typed evidence a proposal cites into readable summaries.

This is the read-and-decide half of the agent loop. Agents propose through the gated agent path; humans list, inspect, and decide here.

| Operation                 | Endpoint                         | Scope                                                                           |
| ------------------------- | -------------------------------- | ------------------------------------------------------------------------------- |
| List proposals            | `GET /v1/proposals`              | `execution:read`                                                                |
| Get one proposal          | `GET /v1/proposals/{id}`         | `execution:read`                                                                |
| Decide on a proposal      | `POST /v1/proposals/{id}/decide` | `execution:read` or `payment_intent:approve`, plus member authority (see below) |
| Resolve proposal evidence | `POST /v1/evidence/resolve`      | `execution:read`                                                                |

{% hint style="info" %}
The same read model and decision service back the MCP tools `proposals.list`, `proposals.get`, `proposals.decide`, and `evidence.resolve`. HTTP and MCP share one code path, so tenant scoping, actor resolution, member authority, and the money-path approval gates behave identically on both. See [MCP Tools](/mcp-server/tools).
{% endhint %}

## List Proposals

```http
GET /v1/proposals?type=collections&status=pending_approval&limit=25
Authorization: Bearer <tenant token>
```

Tenant-scoped and cursor-paginated. Every filter is optional.

| Query parameter  | Type    | Description                                                             |
| ---------------- | ------- | ----------------------------------------------------------------------- |
| `type`           | string  | One of the public proposal types (see below).                           |
| `status`         | string  | Lifecycle status filter (see below).                                    |
| `risk_band`      | string  | `low`, `standard`, `elevated`, or `high`.                               |
| `min_confidence` | number  | Float in `[0, 1]`. Returns proposals at or above this agent confidence. |
| `limit`          | integer | Page size, `1` to `100`.                                                |
| `cursor`         | string  | Opaque pagination cursor from a prior response's `next_cursor`.         |

### Response

```json
{
  "proposals": [
    {
      "id": "prop_9f2a...",
      "type": "collections",
      "created_at": "2026-07-20T14:03:11Z",
      "status": "pending_approval",
      "risk_band": "standard",
      "confidence": 0.82,
      "mode": "propose",
      "narrative": "Invoice INV-2231 is 34 days overdue. Recommend a second-notice follow-up.",
      "evidence": [
        { "kind": "invoice", "ref": "inv_2231", "resolvable": true },
        { "kind": "counterparty", "ref": "cp_88", "resolvable": true }
      ],
      "agent": { "id": "agt_collections", "kind": "collections", "display_name": "Collections" },
      "payment_intent_id": null,
      "action_type": null,
      "stored_action_type": "draft_followup",
      "details": {
        "invoice_id": "inv_2231",
        "counterparty_id": "cp_88",
        "days_overdue": 34,
        "recommended_tone": "firm"
      },
      "policy": {
        "decision": "confirm",
        "policy_id": "pol_8231",
        "policy_version": 4,
        "matched_rule_id": "default-agent-action-requires-review",
        "explanation": "Collections follow-up requires human confirmation.",
        "required_approvers": ["signer"],
        "trace": []
      },
      "presentation": {
        "headline": "Follow up on overdue invoice INV-2231.",
        "recommendation": "Send a second-notice collections follow-up.",
        "key_facts": [
          { "label": "Invoice", "value": "INV-2231" },
          { "label": "Days overdue", "value": 34 }
        ],
        "confidence_band": "high",
        "policy": {
          "decision": "confirm",
          "policy_id": "pol_8231",
          "policy_version": 4,
          "matched_rule_id": "default-agent-action-requires-review",
          "explanation": "Collections follow-up requires human confirmation.",
          "required_approvers": ["signer"],
          "trace": []
        },
        "consequences": {
          "approve": "Brain records the human approval and lets the internal workflow continue.",
          "reject": "Brain closes the proposal without continuing the workflow.",
          "acknowledge": null
        },
        "actions": [
          {
            "id": "approve",
            "label": "Approve",
            "meaning": "Approve this proposed action."
          },
          {
            "id": "reject",
            "label": "Reject",
            "meaning": "Reject this proposed action."
          }
        ],
        "technical_detail": {
          "1_ingest": { "evidence": [{ "kind": "invoice", "ref": "inv_2231" }] },
          "2_extract": { "days_overdue": 34 },
          "3_classify": { "type": "collections", "stored_action_type": "draft_followup" },
          "4_score": { "confidence": 0.82, "confidence_band": "high" },
          "5_policy": {
            "decision": "confirm",
            "policy_id": "pol_8231",
            "policy_version": 4,
            "matched_rule_id": "default-agent-action-requires-review",
            "explanation": "Collections follow-up requires human confirmation.",
            "required_approvers": ["signer"],
            "trace": []
          },
          "6_propose": { "status": "pending_approval", "mode": "propose" }
        }
      },
      "available_decisions": [
        {
          "id": "approve",
          "label": "Approve",
          "meaning": "Approve this proposed action."
        },
        {
          "id": "reject",
          "label": "Reject",
          "meaning": "Reject this proposed action."
        }
      ]
    }
  ],
  "next_cursor": "eyJvIjoxMjV9"
}
```

`next_cursor` is `null` on the last page. A money-path proposal carries a `payment_intent_id` and `action_type`; a non-money finding leaves both `null`. `confidence` and `risk_band` are `null` when the agent did not score them.

The compact fields remain stable for existing clients. The read model also returns the additive fields below for rich proposal cards:

| Field                 | Description                                                                                                                                          |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `stored_action_type`  | Original stored action type, for example `flag_transaction`, `block_payment`, `draft_followup`, or a PaymentIntent `action_type`.                    |
| `details`             | Stored action fields or PaymentIntent Ledger columns shaped as proposal details. Common keys include `risk_score`, `ranked_signals`, and entity ids. |
| `policy`              | Policy decision summary: `decision`, `policy_id`, `policy_version`, `matched_rule_id`, `explanation`, `required_approvers`, and `trace`.             |
| `presentation`        | Normalized UI card data: `headline`, `recommendation`, `key_facts`, `confidence_band`, `policy`, `consequences`, `actions`, and technical detail.    |
| `available_decisions` | Semantic decisions accepted by `POST /v1/proposals/{id}/decide`, with labels and meanings for the current proposal type.                             |

`presentation.technical_detail` always uses the six stable layer keys `1_ingest`, `2_extract`, `3_classify`, `4_score`, `5_policy`, and `6_propose`.

### Public proposal types

`vendor_risk`, `payment`, `collections`, `treasury`, `cash_forecast`, `dispute`, `compliance`, `revenue_intel`, `reconciliation`, `subscription`, `fraud_anomaly`, `personal_budget`, `financial_health`, `purchase_advisor`, `tax_prep`, `travel_finance`, `bill_management`, `debt_optimization`, `savings`.

The public `type` is resolved deterministically:

1. A stored action type that is already a public proposal type is used directly.
2. Otherwise Brain uses the agent role or agent kind from the stored action or joined agent row.
3. Otherwise Brain uses the explicit stored-action map. Examples: `flag_transaction -> fraud_anomaly`, `block_payment -> vendor_risk`, `propose_match -> reconciliation`, `recommend_card -> travel_finance`, `tag_tax_item -> tax_prep`, `remind -> bill_management`, and `recommend_savings_transfer -> savings`.

Ambiguous stored action names such as `notify`, `escalate`, `create_task`, and `recommend_action` resolve through the agent role. Brain does not guess their public type from the action name alone.

This expansion is backward-compatible. No new API version or route was introduced because all compact fields remain in place and the richer fields are additive.

### Lifecycle status values

`proposed`, `pending`, `pending_approval`, `awaiting_second_approval`, `approved`, `acknowledged`, `reconciling`, `paused`, `dispatching`, `rejected`, `executed`, `failed`, `cancelled`, `undone`, `unknown`.

## Get One Proposal

```http
GET /v1/proposals/{id}
Authorization: Bearer <tenant token>
```

Returns the same object shape as a list item. An unknown or cross-tenant id returns `404 execution_proposal_not_found`; the read is tenant-scoped, so a proposal from another tenant is indistinguishable from one that does not exist.

## Decide on a Proposal

```http
POST /v1/proposals/{id}/decide
Authorization: Bearer <member session token>
Content-Type: application/json

{ "decision": "approve" }
```

`decision` is one of `approve`, `reject`, `acknowledge`, or `undo`.

{% hint style="warning" %}
**A decision is a human authority action, not a token-scope action.** The route accepts `execution:read` or `payment_intent:approve`, but it then resolves the caller through the same `ProposalDecisionService` as every other approval surface. The actor must be a **user-principal, active tenant member with approval authority**. Agent principals are rejected with `actor_unresolved`; a propose-only agent token can read proposals but can never decide one.
{% endhint %}

Approving a money-path proposal runs the full money-path authority gate, in order: active tenant member, admin or approver role, authorized approval domain, per-item limit, actor is not the payee (self-approval block), and a tenant-wide distinct second approver where the policy requires one. A first valid approval on a proposal that needs two moves it to `awaiting_second_approval`; a distinct second member's approval clears it for dispatch. The same member approving twice returns `second_approval_required`. Every decision is written to the Audit log before any status transition.

`acknowledge` records that a human saw a non-money finding without acting. `reject` closes a proposal. `undo` reverses an eligible prior decision. There is no execute call here or anywhere on the API: approval authorizes Brain's internal settlement path, it does not dispatch the rail itself.

## Resolve Proposal Evidence

Proposals cite evidence as typed `{ kind, ref }` pairs. This endpoint turns those refs into tenant-scoped summaries and deep links, so a UI can render what a proposal is standing on without knowing each ref format.

```http
POST /v1/evidence/resolve
Authorization: Bearer <tenant token>
Content-Type: application/json

{
  "refs": [
    { "kind": "invoice", "ref": "inv_2231" },
    { "kind": "counterparty", "ref": "cp_88" }
  ]
}
```

At most **50** refs per call. Response:

```json
{
  "results": [
    {
      "kind": "invoice",
      "ref": "inv_2231",
      "resolvable": true,
      "not_found": false,
      "summary": "INV-2231, 4,200.00 USD, due 2026-06-16, 34 days overdue",
      "deep_link": "/invoices/inv_2231"
    },
    {
      "kind": "agent",
      "ref": "agt_collections",
      "resolvable": false,
      "not_found": false,
      "summary": null,
      "deep_link": null,
      "reason": "unsupported_kind"
    }
  ]
}
```

Resolution fails closed and is tenant-scoped. A supported ref that does not exist in the tenant returns `resolvable: true, not_found: true`. An unsupported kind or malformed ref returns `resolvable: false` with a `reason` of `unsupported_kind` or `malformed_ref`; it is never an error.

**Resolvable kinds:** `account`, `counterparty`, `invoice`, `obligation`, `transaction`, `wiki_entity`. Other evidence kinds a proposal may cite (for example `document`, `payment_intent`, `policy`, `raw_artifact`) are returned unresolved today rather than rejected, so a mixed evidence list always resolves partially instead of failing whole.

## Related

| Topic                      | Page                                                      |
| -------------------------- | --------------------------------------------------------- |
| How agents run and propose | [Agents API](/api-reference/agents-api)                   |
| The money-path proposal    | [Payment Intents API](/api-reference/payment-intents-api) |
| The same tools over MCP    | [MCP Tools](/mcp-server/tools)                            |
| Who may approve            | [Internal Agents](/concepts/internal-agents)              |


# Audit API

Query audit events, pull a Merkle inclusion proof, verify a proof independently, walk the full history for any Ledger entity, export, and pull the canonical **Proof** for an action. All event payloads land in the append-only hash chain and are batch-anchored to `BrainAuditAnchor` on Base.

| Operation                           | Endpoint                                       |
| ----------------------------------- | ---------------------------------------------- |
| Get the latest anchor               | `GET /v1/audit/anchor/latest`                  |
| Walk an entity's history            | `GET /v1/audit/entity/{entityType}/{entityId}` |
| Get one event (+ inclusion proof)   | `GET /v1/audit/event/{event_id}`               |
| Query events                        | `GET /v1/audit/events`                         |
| Export (not implemented, see below) | `POST /v1/audit/export`                        |
| Independent verification            | `POST /v1/audit/verify`                        |
| Canonical Proof for an action       | See the [Proof API](/api-reference/proof-api)  |

### Get the Latest Anchor

```http
GET /v1/audit/anchor/latest
Authorization: Bearer <token>
```

```json
{
  "merkle_root": "0xabc...",
  "event_count": 4127,
  "period_start": "2026-05-28T11:00:00Z",
  "period_end": "2026-05-28T11:30:00Z",
  "onchain_tx_hash": "0xdef...",
  "onchain_block_number": 8829110
}
```

### Walk an Entity's History

Every audit event that touched a specific Ledger row, in causal order.

```http
GET /v1/audit/entity/{entityType}/{entityId}
Authorization: Bearer <token>
```

`entityType` is one of `account | balance | transaction | counterparty | obligation | document | invoice | payment_intent | reconciliation_match | proposal | execution`.

```json
{
  "entity_type": "payment_intent",
  "entity_id": "pi_a1b2c3",
  "events": [
    {
      "id": "audit_evt_001",
      "tenant_id": "acme",
      "layer": "agent",
      "actor": "ag_payment_v1",
      "action": "payment_intent.proposed",
      "inputs": { "evidence_ids": ["rp_001"], "policy_version": 4 },
      "outputs": { "payment_intent_id": "pi_a1b2c3" },
      "policy_decision_id": "pd_7331",
      "before_state": null,
      "after_state": "proposed",
      "event_hash": "0x...",
      "prev_event_hash": "0x...",
      "created_at": "2026-05-28T12:00:00Z"
    }
  ]
}
```

`inputs` and `outputs` carry **hashes and evidence references only**. Never raw payloads or PII. The full encrypted payload stays off-chain.

### Get One Event with Inclusion Proof

```http
GET /v1/audit/event/{event_id}
Authorization: Bearer <token>
```

```json
{
  "event": { "id": "audit_evt_001", ... },
  "inclusion_proof": {
    "merkle_root":     "0xabc...",
    "merkle_proof":    ["0x111...", "0x222..."],
    "anchor_tx_hash":  "0xdef...",
    "anchor_block":    8829110
  }
}
```

### Query Events

```http
GET /v1/audit/events?layer=agent
Authorization: Bearer <token>
```

Filters: `layer` (`raw | ledger | wiki | policy | agent | execution | audit`), `actor`, `since`, `until`, `limit` (default 100, max 1000), `cursor`. Returns `{ events: AuditEvent[], next_cursor }`.

### Independent Verification

A counterparty (or an auditor) verifies an event without trusting Brain: supply the event hash, the Merkle proof, and the claimed root. The endpoint runs the path computation and returns whether it lands on the supplied root.

```http
POST /v1/audit/verify
Content-Type: application/json

{
  "event_hash":   "0x...",
  "merkle_proof": ["0x111...", "0x222..."],
  "merkle_root":  "0xabc..."
}
```

```json
{ "verified": true, "onchain_block": 8829110 }
```

Brain also publishes a `verifyMerkleProof(...)` helper in `@brainfinance/sdk` and the on-chain `BrainAuditAnchor.verifyInclusion(root, leaf, proof)` view function. Check `isPublished(tenantId, root)` to verify the root belongs to the tenant. Three independent paths reach the same conclusion.

### Export

`POST /v1/audit/export` is a declared stub. It validates the request shape and then always returns `501` (error code `dependency_unavailable`) -- there is no job row, worker, or status/download route behind it. Use the working tenant export instead:

```http
POST /v1/tenants/{tenant_id}/export
Authorization: Bearer <token>
```

Poll `GET /v1/tenants/{tenant_id}/export/{job_id}` for status and fetch the result from `GET /v1/tenants/{tenant_id}/export/{job_id}/download` once ready.

### The Canonical Proof for an Action

For investor / compliance / counterparty use cases, the flagship artifact is the per-action **Proof**. Assembled from the §6 gate trace, evidence chain, policy decision, and anchored audit Merkle chain. It has its own page so this one can stay focused on raw events and anchors.

[**Proof API**](/api-reference/proof-api)

### 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>Proof API</strong></td><td>The per-action trust artifact.</td><td><a href="/pages/dFcxorG4eXpkqN8rsLD6">/pages/dFcxorG4eXpkqN8rsLD6</a></td><td></td></tr><tr><td><strong>Audit Concepts</strong></td><td>How the hash chain and Merkle anchoring work.</td><td><a href="/pages/PIgNXssgtEUZDLnC4b4d">/pages/PIgNXssgtEUZDLnC4b4d</a></td><td></td></tr><tr><td><strong>BrainAuditAnchor</strong></td><td>The on-chain anchor contract.</td><td><a href="/pages/5njwTjZlypdSt55BbRDG">/pages/5njwTjZlypdSt55BbRDG</a></td><td></td></tr></tbody></table>


# Proof API

The Proof API returns the canonical, single-artifact record of how a financial action was decided, gated, executed, and anchored. It's the flagship trust artifact: one fetch, every dimension. Policy decision, §6 gate trace, evidence chain, audit Merkle proof, on-chain anchor, rail receipt, plain-English explanation.

| Operation                   | Endpoint                         | Scope        |
| --------------------------- | -------------------------------- | ------------ |
| Canonical Proof (JSON)      | `GET /v1/proof/{action_id}`      | `audit:read` |
| Human-readable Proof (HTML) | `GET /v1/proof/{action_id}/view` | `audit:read` |

`action_id` is the PaymentIntent id. Both routes are **tenant-isolated**: a cross-tenant id returns `404`. The existence of the action is never leaked across tenants.

### Get a Proof

```http
GET /v1/proof/{action_id}
Authorization: Bearer <token>
```

```json
{
  "action_id": "pi_a1b2c3",
  "tenant_id": "acme",
  "agent_id": "ag_payment_v1",
  "behavior_hash": "0x...",
  "outcome": "executed",
  "policy_version": 4,
  "policy_hash": "0xabc...",
  "matched_rule_id": "rule_invoice_above_5k",
  "gate_checks": [
    { "index": 1, "name": "intent_exists_and_approved", "passed": true },
    { "index": 1.5, "name": "agent_behavior_pinned", "passed": true },
    { "index": 5, "name": "source_balance_sufficient", "passed": true },
    { "index": 7, "name": "counterparty_not_sanctioned", "passed": true },
    { "index": 13, "name": "audit_chain_healthy", "passed": true }
  ],
  "evidence": [{ "raw_id": "raw_8231", "parser": "invoice_v2", "confidence": 0.98 }],
  "ledger_snapshot_hash": "0x...",
  "audit_events": ["audit_evt_001", "audit_evt_002"],
  "merkle_root": "0xabc...",
  "merkle_proof": ["0x111...", "0x222..."],
  "chain_anchor": { "tx_hash": "0xdef...", "block": 8829110 },
  "rail_receipt": { "rail": "bank_ach", "provider_id": "..." },
  "human_explanation": "Paid invoice inv_8231 for $7,800.00 to Amazon Web Services on Mercury operating account..."
}
```

### Fields

| Field                            | Description                                                                                        |
| -------------------------------- | -------------------------------------------------------------------------------------------------- |
| `outcome`                        | `allowed` \| `confirmed` \| `rejected` \| `executed` \| `failed` \| `shadow_completed`             |
| `behavior_hash`                  | The agent's runtime `behaviorHash`. Must equal the value registered on-chain (§6 check 1.5)        |
| `policy_version` / `policy_hash` | The policy version evaluated and its content hash                                                  |
| `matched_rule_id`                | The DSL rule that fired                                                                            |
| `gate_checks[]`                  | Every numbered + hardening check, in execution order, with `passed: boolean` and optional `reason` |
| `evidence[]`                     | The Raw-parsed rows the agent and the gate consulted                                               |
| `ledger_snapshot_hash`           | Hash of the Ledger state Policy decided against (the snapshot the §6 7.5 check re-validates)       |
| `audit_events[]`                 | Every audit event id covering this action                                                          |
| `merkle_root` / `merkle_proof`   | The Merkle inclusion proof for the audit-chain leaves                                              |
| `chain_anchor`                   | The on-chain anchor for the containing batch. `null` until the batch lands on Base                 |
| `rail_receipt`                   | The typed rail receipt (`ach` / `wire` / `erp` / `onchain` schemas)                                |
| `human_explanation`              | One-paragraph plain-English summary, deterministically generated                                   |

### Verifying a Proof

Three independent paths to the same conclusion:

| Method                       | Description                                                                                                               |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **SDK helper**               | `verifyMerkleProof(...)` from `@brainfinance/sdk`. No Brain account required                                              |
| **On-chain call**            | `BrainAuditAnchor.isPublished(tenantId, root)` plus `verifyInclusion(root, leaf, merkleProof)` from any Solidity contract |
| **Public verifier endpoint** | `POST /v1/audit/verify`. Supply event hash, Merkle proof, and claimed root                                                |

### Human-Readable View

For compliance, investor, or counterparty screens. The same Proof rendered as a single HTML page:

```http
GET /v1/proof/{action_id}/view
Authorization: Bearer <token>
```

Returns `text/html` with the same data laid out for human reading. Same tenant-isolation and `audit:read` scope.

### 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>Audit API</strong></td><td>Underlying events, anchors, exports.</td><td><a href="/pages/BqeKz3FmbRDmRILK0zaA">/pages/BqeKz3FmbRDmRILK0zaA</a></td><td></td></tr><tr><td><strong>The Pre-Execution Gate</strong></td><td>What the gate-check rows mean.</td><td><a href="/pages/GcCCOqv3BXHFtEpuypqD">/pages/GcCCOqv3BXHFtEpuypqD</a></td><td></td></tr><tr><td><strong>Audit and Proof</strong></td><td>The conceptual model.</td><td><a href="/pages/PIgNXssgtEUZDLnC4b4d">/pages/PIgNXssgtEUZDLnC4b4d</a></td><td></td></tr></tbody></table>


# Tenants API (GDPR deletion)

Endpoints that operate on a tenant as a whole. Today there is one: the GDPR right-to-erasure deletion.

### Delete a Tenant (GDPR Right-to-Erasure)

```http
DELETE /v1/tenants/{id}
Authorization: Bearer <owner JWT>
```

Walks every tenant-scoped table across the six layers and deletes rows for the target tenant under the privileged DB role (BYPASSRLS). The Merkle audit chain itself is preserved (financial-integrity legitimate-interest carveout); the deletion records a `tenant.deleted` event with per-table row counts so the erasure is itself verifiable.

#### Authorization Posture

| Caller                                  | Result                               |
| --------------------------------------- | ------------------------------------ |
| User principal where `tenantId === :id` | Permitted                            |
| User principal where `tenantId !== :id` | `auth_tenant_mismatch` (HTTP 403)    |
| Agent principal                         | `auth_scope_insufficient` (HTTP 403) |
| Unauthenticated                         | `auth_token_missing` (HTTP 401)      |

Self-tenant only by design: the data subject (or their representative user) is the authorized agent of the erasure request. No machine credential (agent, API partner, or webhook signer) can trigger deletion.

#### Response (HTTP 200)

```json
{
  "tenantId": "tnt_...",
  "deletedRows": {
    "raw_artifacts": 1240,
    "ledger_payment_intents": 32,
    "wiki_pages": 18,
    "policy_decisions": 47,
    "agents": 3,
    "...": "..."
  },
  "totalRows": 1421
}
```

#### What Is Preserved

`audit_events` and `audit_anchors` are not deleted. The Merkle chain backs Brain's "verify without trusting Brain" promise; GDPR Article 17(3)(b) permits retention where required for the establishment or defense of legal claims. The tombstone `tenant.deleted` event includes a `preserved: ["audit_events", "audit_anchors"]` field so the policy is explicit on chain.

#### Error Codes

| Code                      | HTTP | Meaning                               |
| ------------------------- | ---- | ------------------------------------- |
| `auth_token_missing`      | 401  | No JWT presented                      |
| `auth_scope_insufficient` | 403  | Principal type is not `user`          |
| `auth_tenant_mismatch`    | 403  | JWT tenant differs from target tenant |


# Webhooks

Inspect and replay failed deliveries on Brain's outbound webhook endpoints. This is the operator-facing surface for dead-lettered events. The inbound provider webhook (`POST /v1/raw/webhooks/{provider}`) lives in [Sources & Raw Ingestion](/api-reference/sources-api).

| Operation                 | Endpoint                                      |
| ------------------------- | --------------------------------------------- |
| List dead-letter events   | `GET /v1/webhooks/{endpoint_id}/dead-letters` |
| Replay dead-letter events | `POST /v1/webhooks/{endpoint_id}/replay`      |

Both routes are tenant-isolated. The `endpoint_id` belongs to the calling tenant; a cross-tenant id returns `404`.

### Event Types

Brain forwards a fixed allowlist of audit actions to registered endpoints. These are the only `event_type` values an outbound webhook carries:

| `event_type`                              | Fires when                                                    |
| ----------------------------------------- | ------------------------------------------------------------- |
| `agent.action.proposed`                   | An agent proposed an action                                   |
| `payment_intent.created`                  | A PaymentIntent is proposed                                   |
| `payment_intent.approved`                 | A required approval was recorded (or Policy said `allow`)     |
| `payment_intent.awaiting_second_approval` | A first approval landed and a distinct second approver is due |
| `proposal.awaiting_second_approval`       | Contract-named event for the awaiting-second-approval move    |
| `proposal.decided`                        | A surface proposal reached a terminal decision                |
| `payment_intent.rejected`                 | Policy or an approver rejected                                |
| `payment_intent.executed`                 | The intent was executed                                       |
| `payment_intent.failed`                   | Execution failed                                              |
| `payment_intent.reconciling`              | The intent was parked for reconciliation                      |
| `member.changed`                          | A member was created, changed, or deactivated                 |
| `payment_intent.execute.after`            | The §6 gate ran and the intent was dispatched to a rail       |
| `ledger.counterparty.created`             | A counterparty row was created                                |
| `ledger.counterparty.updated`             | A counterparty identity was edited                            |
| `ledger.transaction.created`              | A transaction row was created                                 |
| `ledger.obligation.created`               | An obligation row was created                                 |
| `policy.evaluate`                         | A policy decision was recorded                                |
| `raw.ingest.new`                          | A new Raw artifact was ingested                               |
| `raw.ingest.deduplicated`                 | A re-submitted artifact matched an existing one               |
| `raw.extraction.status_changed`           | A Raw extraction changed status                               |
| `raw.source.status_changed`               | A connected source changed status                             |

There is no `payment_intent.settled` event: rail settlement is async and confirmed via the rail-specific provider webhook plus the proof endpoint. `payment_intent.failed` **is** emitted when execution fails. The legacy bare `action.*` names are **not** emitted.

### How Dead-Lettering Works

Brain dispatches webhook deliveries asynchronously. Each row in the dead-letter table tracks an `attempt_count`; the delivery worker retries with exponential backoff up to **5 attempts**, after which the row is marked exhausted and stops auto-retrying. Replay (below) is the manual escape hatch.

### List Dead-Letter Events

```http
GET /v1/webhooks/{endpoint_id}/dead-letters
Authorization: Bearer <token>
```

```json
{
  "endpoint_id": "wh_ops_alerts",
  "dead_letters": [
    {
      "id": "dl_001",
      "event_id": "audit_evt_xyz",
      "event_type": "payment_intent.execute.after",
      "last_error": "503 Service Unavailable",
      "attempt_count": 5,
      "created_at": "2026-05-27T08:15:00Z",
      "last_attempt_at": "2026-05-27T08:47:12Z"
    }
  ]
}
```

### Replay Dead-Letter Events

Re-attempts delivery for every dead-letter row that is still under the attempt cap. Successes clear the row; failures bump `attempt_count`. The operation is **idempotent**. It accepts an `Idempotency-Key` header and is safe to retry.

```http
POST /v1/webhooks/{endpoint_id}/replay
Authorization: Bearer <token>
Idempotency-Key: <stable-key>
```

```json
{
  "endpoint_id": "wh_ops_alerts",
  "attempted": 7,
  "redelivered": 5,
  "still_failing": 2
}
```

If `still_failing > 0`, those rows had their `attempt_count` bumped; once a row hits 5 attempts it stops being auto-replayed and you can only retry it via this manual route after fixing the receiver.

### 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>📜 Audit API</strong></td><td>The events that drive outbound webhooks.</td><td><a href="/pages/BqeKz3FmbRDmRILK0zaA">/pages/BqeKz3FmbRDmRILK0zaA</a></td><td></td></tr><tr><td><strong>📥 Sources &#x26; Raw Ingestion</strong></td><td>The inbound webhook side (provider HMAC).</td><td><a href="/pages/EQ4MJytikUzDXJfFnfp5">/pages/EQ4MJytikUzDXJfFnfp5</a></td><td></td></tr></tbody></table>


# MCP Server (API Reference)

The MCP server is reachable at the canonical host `https://mcp.brain.fi` (which maps root traffic onto the internal `POST /v1/agents/mcp` route), JSON-RPC 2.0 over single-shot HTTP. This page is the API-style summary; for the full reference (tool list, resources, prompts, on-chain auth flow), see the dedicated MCP Server section.

### Endpoint

```
POST /
Host: mcp.brain.fi
Authorization: Bearer <jwt>
Content-Type: application/json
```

The canonical public host is **`mcp.brain.fi`**, which maps root traffic onto the internal `/v1/agents/mcp` route. Either form reaches the same JSON-RPC surface; new integrations should use the canonical host.

| Environment    | Canonical host          | Internal / compatibility route               |
| -------------- | ----------------------- | -------------------------------------------- |
| **Production** | `https://mcp.brain.fi`  | `https://api.brain.fi/v1/agents/mcp`         |
| **Sandbox**    | `https://mcp.brain.dev` | `https://api.sandbox.brain.fi/v1/agents/mcp` |

Sandbox is wired to Base Sepolia; production is wired to Base mainnet.

### Methods

The methods the JSON-RPC entry accepts (matches the spec's `JsonRpcRequest.method` enum):

| Method           | Purpose                                                    |
| ---------------- | ---------------------------------------------------------- |
| `initialize`     | Capability negotiation                                     |
| `ping`           | Liveness                                                   |
| `tools/list`     | List tools the agent has scope for                         |
| `tools/call`     | Invoke a tool                                              |
| `resources/list` | List resources (and resource templates) the agent can read |
| `resources/read` | Read a resource by URI                                     |
| `prompts/list`   | List the canned prompts                                    |
| `prompts/get`    | Render a canned prompt with arguments                      |

Once a request reaches JSON-RPC dispatch, the HTTP layer returns `200` and application errors live in the JSON-RPC response's `error` field. **Authentication and authorization fail&#x20;*****before*****&#x20;dispatch**, so they return an HTTP `401`/`403` Brain error envelope (not a `200` with a JSON-RPC `error`). See [Error Codes](#error-codes).

### The 16 Tools

Five Ledger reads, two Wiki reads, one Raw contribute, three PaymentIntent tools (`payment_intent.propose`, `payment_intent.cancel`, `payment_intent.list`), three proposal tools (`proposals.list`, `proposals.get`, `proposals.decide`), one evidence resolve (`evidence.resolve`), and one agent action propose. **There is no `payment_intent.execute` tool, and there will never be one**. Execution is reserved for internal Brain workers running under tenant policy and the §6 gate.

[**→ Tool reference**](/mcp-server/tools)

### The 7 Resource Templates

Resource templates addressable by `brain://` URIs:

```
brain://ledger/accounts/{account_id}
brain://ledger/transactions/{transaction_id}
brain://ledger/obligations/{obligation_id}
brain://ledger/payment-intents/{payment_intent_id}
brain://wiki/pages/{slug}
brain://payments/action_types
brain://proofs/{action_id}
```

[**→ Resources reference**](/mcp-server/resources)

### The 5 Prompts

`wiki.question.cash_flow_summary`, `wiki.question.bills_due`, `wiki.question.spending_change`, `wiki.question.invoice_status`, `wiki.question.subscriptions`.

[**→ Prompts reference**](/mcp-server/prompts)

### Authentication

JWT (Fastify JWT plugin) plus three pre-call checks before any method dispatches:

1. The agent record is **active** in `BrainMCPAgentRegistry`.
2. The JWT's `scope_hash` claim matches the agent's on-chain `scopeHash` (60-second cache, Base RPC fallback).
3. The JWT's `tenantId` claim equals the agent's registered `tenantId`.

Per-tool scope (e.g. `payment_intent:propose`) is enforced at invocation time.

[**→ Authentication reference**](/mcp-server/mcp-authentication)

### Error Codes

There are **two error surfaces**, depending on where the request fails:

* **Pre-dispatch auth failures**. The route guard (`services/mcp/src/transport/http.ts`) checks the JWT/principal type, then the auth verifier (`services/mcp/src/auth.ts`, invoked at the top of `server.handle`) checks on-chain registration, scope-hash, and tenant **before** any method is dispatched. These throw `BrainError`s that propagate out of the handler, so the client receives an HTTP `401`/`403` **Brain error envelope** (`{ "error_code": ..., "message": ... }`), *not* a JSON-RPC response. The relevant codes: `auth_token_missing`, `auth_token_invalid`, `auth_token_expired`, `auth_scope_insufficient`, `auth_tenant_mismatch`, `agent_not_registered`, `agent_not_registered_onchain`, `agent_scope_hash_missing`, `agent_scope_hash_mismatch`.
* **Post-auth JSON-RPC errors**. Once dispatch begins, the HTTP status is `200` and the failure is carried in the JSON-RPC `error` field using the Brain-specific codes below (`-32001..-32005`) plus the standard JSON-RPC codes.

| Code     | Meaning                                                                                        |
| -------- | ---------------------------------------------------------------------------------------------- |
| `-32001` | Auth token missing, invalid, or expired (`auth_token_missing/invalid/expired`)                 |
| `-32002` | Scope insufficient (also tenant mismatch) (`auth_scope_insufficient` / `auth_tenant_mismatch`) |
| `-32003` | Agent not registered or inactive (`agent_not_registered`, `agent_not_registered_onchain`)      |
| `-32004` | Pre-execution gate failed. Covers every `gate_*` sub-code (`payment_intent_gate_failed`)       |
| `-32005` | Agent `scope_hash` mismatch against on-chain registration (`agent_scope_hash_mismatch`)        |
| `-32600` | Invalid request (standard JSON-RPC)                                                            |
| `-32601` | Method not found                                                                               |
| `-32602` | Invalid params                                                                                 |
| `-32603` | Internal error                                                                                 |
| `-32700` | Parse error                                                                                    |

The mapping is enforced in `services/mcp/src/types.ts` and `dispatcher.ts`. Every Brain HTTP error code that surfaces *inside* JSON-RPC dispatch routes deterministically into one of these five Brain-specific JSON-RPC codes. (The `-3200x` codes above only apply once a call has authenticated; pre-dispatch auth failures use the HTTP envelope described above.)

### A First Call

```http
POST / HTTP/1.1
Host: mcp.brain.fi
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "wiki.question",
    "arguments": {
      "tenant_id": "acme",
      "question":  "What's our cash position right now?"
    }
  }
}
```

### 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>🔌 MCP Overview</strong></td><td>The full architecture and surface map.</td><td><a href="/pages/zjEFSPkZADwcvDIrQ4kS">/pages/zjEFSPkZADwcvDIrQ4kS</a></td><td></td></tr><tr><td><strong>🛠️ Tools</strong></td><td>The 16 tools in detail.</td><td><a href="/pages/LEWpOYJSpmIuTr20aNe8">/pages/LEWpOYJSpmIuTr20aNe8</a></td><td></td></tr><tr><td><strong>🪪 Authentication</strong></td><td>JWT and on-chain scope verification.</td><td><a href="/pages/ZNnH2CuEVwz0dbBj8bG0">/pages/ZNnH2CuEVwz0dbBj8bG0</a></td><td></td></tr></tbody></table>


# Overview

Brain exposes a **Model Context Protocol (MCP) server** so agents can read financial state, retrieve memory, and propose actions within tenant-signed policy. Brain runs the underlying ingest, normalization, policy, and execution; the agent works against a single scoped surface.

Agents propose actions. Policies decide what runs. Humans stay in control where the policy says they should.

| Property      | Value                                                             |
| ------------- | ----------------------------------------------------------------- |
| **Endpoint**  | `https://mcp.brain.fi` (canonical; maps to `POST /v1/agents/mcp`) |
| **Transport** | JSON-RPC 2.0 over single-shot HTTP                                |
| **Backed by** | The same Ledger, Wiki, and PaymentIntent surface as the HTTP API  |

{% hint style="info" %}
The MCP surface uses single-shot HTTP. One request, one response, one audit event. Streaming transports may follow once we see a use case that needs them.
{% endhint %}

### Surface Map

The MCP surface is intentionally small. **16 tools, 7 resource templates, 5 canned prompts.**

<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>🛠️ 16 Tools</strong></td><td>Five Ledger reads, two Wiki reads, one Raw contribute, three PaymentIntent (propose, cancel, list), three proposal tools (list, get, decide), one evidence resolve, one agent action propose.</td><td><a href="/pages/LEWpOYJSpmIuTr20aNe8">/pages/LEWpOYJSpmIuTr20aNe8</a></td><td></td></tr><tr><td><strong>📦 7 Resources</strong></td><td>Resource templates addressable by <code>brain://</code> URIs: ledger accounts/transactions/obligations/payment-intents, wiki pages, payments/action_types catalog, and per-action proofs.</td><td><a href="/pages/L2DEzPLxdznDACZBKJEr">/pages/L2DEzPLxdznDACZBKJEr</a></td><td></td></tr><tr><td><strong>💬 5 Prompts</strong></td><td>Canned prompts for the most common agent loops: cash flow, bills, spending, invoices, subscriptions.</td><td><a href="/pages/IGFOhlkkHijzYzj8v8lp">/pages/IGFOhlkkHijzYzj8v8lp</a></td><td></td></tr><tr><td><strong>🪪 Authentication</strong></td><td>JWT plus on-chain scope hash verification against <code>BrainMCPAgentRegistry</code>. Per-tenant rate limit on the route so one misbehaving agent cannot crowd out other tenants.</td><td><a href="/pages/ZNnH2CuEVwz0dbBj8bG0">/pages/ZNnH2CuEVwz0dbBj8bG0</a></td><td></td></tr></tbody></table>

### What an External Agent Can Do

| Capability               | Tools                                                                                                                             | On-chain Scope                               |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| **Read Ledger**          | `ledger.account.get`, `ledger.accounts.list`, `ledger.transactions.list`, `ledger.obligations.list`, `ledger.counterparties.list` | `ledger:read`                                |
| **Read Wiki**            | `wiki.question`, `wiki.page.get`                                                                                                  | `wiki:read`                                  |
| **Contribute to Raw**    | `raw.contribute`                                                                                                                  | `raw:write`                                  |
| **Propose payment**      | `payment_intent.propose`                                                                                                          | `payment_intent:propose`                     |
| **Read proposals**       | `proposals.list`, `proposals.get`, `evidence.resolve`                                                                             | `execution:read`                             |
| **Decide a proposal**    | `proposals.decide`                                                                                                                | `payment_intent:approve` or `execution:read` |
| **Propose agent action** | `agent.action.propose`                                                                                                            | `execution:propose`                          |

{% hint style="info" %}
`proposals.decide` declares no tool scope of its own. It accepts either `payment_intent:approve` or `execution:read` at the call boundary, then enforces member approval authority downstream (user-principal actor resolution, active-member and approval-role checks, and the money-path approval gates).
{% endhint %}

{% hint style="warning" %}
There is no `payment_intent.execute` on the MCP surface. External agents may **propose** but never **execute**. Execution always goes through Brain's deterministic pre-execution gate (13 numbered checks + 4 hardening additions), behind human approval where policy demands it.
{% endhint %}

[**→ The pre-execution gate**](/protocol/the-pre-execution-gate)

### What Makes the MCP Surface Different

The MCP tools call the same Ledger, Wiki, and PaymentIntent code paths that back the HTTP API. That has three concrete consequences:

| Property                     | Effect                                                                                                                   |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Identical Policy gating**  | A `payment_intent.propose` over MCP runs through the same Policy evaluator as one created via HTTP                       |
| **Identical audit emission** | Tools that mutate state emit the same inner audit events the HTTP API emits, plus an outer `agent.mcp.tool_called` event |
| **No bypass path**           | There is no shortcut. MCP cannot skip Policy or write to the Ledger directly.                                            |

### Architecture

```
        ┌─────────────────────────────────┐
        │  External agent              │
        └────────────┬────────────────────┘
                     │  JSON-RPC 2.0 over HTTPS
                     │  Authorization: Bearer <jwt>
                     ▼
        ┌─────────────────────────────────┐
        │  Brain edge                     │
        │  Validates JWT, resolves        │
        │  principal (tenant + scopes)    │
        └────────────┬────────────────────┘
                     │
                     ▼
        ┌─────────────────────────────────┐
        │  MCP dispatcher                 │
        │  - Method routing               │
        │  - 3 pre-call checks:           │
        │    a) agent active              │
        │    b) JWT scope_hash matches    │
        │       on-chain hash             │
        │    c) JWT tenant == agent       │
        │       tenant                    │
        │  - Per-tool scope enforcement   │
        └────────────┬────────────────────┘
                     │
                     ▼
        ┌─────────────────────────────────┐
        │  Shared Brain services:         │
        │  Ledger reads & writes          │
        │  Wiki Q&A and pages             │
        │  PaymentIntent proposal flow    │
        └─────────────────────────────────┘
                     │
                     ▼
            Audit events emitted at every step
```

### A First Call

```http
POST / HTTP/1.1
Host: mcp.brain.fi
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "wiki.question",
    "arguments": {
      "tenant_id": "acme",
      "question": "What's our top expense category this month?"
    }
  }
}
```

Response:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [{ "type": "text", "text": "AWS at $61,404 across 3 environments." }],
    "metadata": {
      "ledger_evidence": [
        { "type": "ledger_transactions", "id": "tx_4127" },
        { "type": "ledger_transactions", "id": "tx_4128" }
      ],
      "audit_event_id": "evt_a1b2c3..."
    }
  }
}
```

### 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>🛠️ Tools</strong></td><td>The 16 tools in detail.</td><td><a href="/pages/LEWpOYJSpmIuTr20aNe8">/pages/LEWpOYJSpmIuTr20aNe8</a></td><td></td></tr><tr><td><strong>🪪 Authentication</strong></td><td>How JWT and on-chain scope verification work together.</td><td><a href="/pages/ZNnH2CuEVwz0dbBj8bG0">/pages/ZNnH2CuEVwz0dbBj8bG0</a></td><td></td></tr></tbody></table>


# Tools

Brain's MCP surface exposes **16 tools** across five capability groups. Each tool requires a specific scope, granted to the agent via on-chain registration in `BrainMCPAgentRegistry`.

### At a Glance

| Tool                         | Group          | Required Scope               | Mutates State                        |
| ---------------------------- | -------------- | ---------------------------- | ------------------------------------ |
| `ledger.account.get`         | Ledger read    | `ledger:read`                | No                                   |
| `ledger.accounts.list`       | Ledger read    | `ledger:read`                | No                                   |
| `ledger.transactions.list`   | Ledger read    | `ledger:read`                | No                                   |
| `ledger.obligations.list`    | Ledger read    | `ledger:read`                | No                                   |
| `ledger.counterparties.list` | Ledger read    | `ledger:read`                | No                                   |
| `wiki.question`              | Wiki read      | `wiki:read`                  | No                                   |
| `wiki.page.get`              | Wiki read      | `wiki:read`                  | No                                   |
| `raw.contribute`             | Raw contribute | `raw:write`                  | Yes (writes Raw artifact)            |
| `payment_intent.propose`     | PaymentIntent  | `payment_intent:propose`     | Yes (writes PaymentIntent in Ledger) |
| `payment_intent.cancel`      | PaymentIntent  | `payment_intent:propose`     | Yes (cancels own proposal)           |
| `payment_intent.list`        | PaymentIntent  | `payment_intent:propose`     | No                                   |
| `agent.action.propose`       | Agent action   | `execution:propose`          | Yes (writes Proposal)                |
| `proposals.list`             | Proposals read | `execution:read`             | No                                   |
| `proposals.get`              | Proposals read | `execution:read`             | No                                   |
| `proposals.decide`           | Proposals read | member authority (see below) | Yes (records a human decision)       |
| `evidence.resolve`           | Proposals read | `execution:read`             | No                                   |

{% hint style="warning" %}
**There is no `payment_intent.execute` tool.** External agents only ever propose. Execution is Brain-internal: an approved intent is dispatched by Brain's own settlement path, never by the proposing agent. A human (or a signed `allow` policy decision) supplies the approval that an intent needs before that internal path runs it; the human does not call a settlement endpoint. Every execution, attended or unattended, passes the same [deterministic pre-execution gate](/protocol/the-pre-execution-gate): 13 numbered checks plus 10 hardening additions (23 entries total; several record `not_applicable` until their loaders are wired, so the canonical happy path is the 13 numbered checks). It is the only path to settlement.
{% endhint %}

### Ledger Reads

#### `ledger.account.get`

Fetch a single account by id.

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "ledger.account.get",
    "arguments": {
      "tenant_id": "acme",
      "account_id": "acct_8231"
    }
  }
}
```

Returns the full Ledger account row including `current_balance`, `available_balance`, `provenance`, `confidence`, and the `source_ids` and `evidence_ids` arrays.

#### `ledger.accounts.list`

List accounts for a tenant.

| Argument       | Type   | Description                                 |
| -------------- | ------ | ------------------------------------------- |
| `tenant_id`    | string | Required                                    |
| `account_type` | string | Optional: `bank`, `card`, `loan`, `onchain` |
| `status`       | string | Optional: `active`, `closed`                |
| `cursor`       | string | Optional pagination cursor                  |

#### `ledger.transactions.list`

Filter and paginate Ledger transactions.

| Argument                   | Type     | Description                                                                |
| -------------------------- | -------- | -------------------------------------------------------------------------- |
| `tenant_id`                | string   | Required                                                                   |
| `account_id`               | string   | Optional, filter to one account                                            |
| `from`, `to`               | ISO date | Optional date range                                                        |
| `direction`                | string   | Optional: `inflow`, `outflow`, `transfer`, `adjustment`                    |
| `counterparty_id`          | string   | Optional                                                                   |
| `status`                   | string   | Optional: `pending`, `posted`, `cleared`, `failed`, `reversed`, `disputed` |
| `min_amount`, `max_amount` | decimal  | Optional                                                                   |
| `cursor`                   | string   | Optional                                                                   |

#### `ledger.obligations.list`

List the tenant's outstanding obligations: bills, invoices, subscriptions, loans, rent, payroll, tax, card statements.

| Argument          | Type     | Description                                                             |
| ----------------- | -------- | ----------------------------------------------------------------------- |
| `tenant_id`       | string   | Required                                                                |
| `status`          | string   | Optional: `upcoming`, `due`, `paid`, `overdue`, `cancelled`, `disputed` |
| `due_before`      | ISO date | Optional                                                                |
| `counterparty_id` | string   | Optional                                                                |
| `type`            | string   | Optional                                                                |

#### `ledger.counterparties.list`

List or search counterparties.

| Argument          | Type   | Description                                                                                                    |
| ----------------- | ------ | -------------------------------------------------------------------------------------------------------------- |
| `tenant_id`       | string | Required                                                                                                       |
| `query`           | string | Optional, fuzzy-matches `name`, `normalized_name`, `aliases[]`                                                 |
| `type`            | string | Optional: `merchant`, `vendor`, `customer`, `employer`, `bank`, `wallet`, `exchange`, `tax_authority`, `other` |
| `verified_status` | string | Optional                                                                                                       |

### Wiki Reads

#### `wiki.question`

Ask the tenant's financial brain a natural-language question. The answer grounds in **Ledger rows**, not Wiki text. Wiki provides retrieval scaffolding; cited facts come from the Ledger.

```json
{
  "name": "wiki.question",
  "arguments": {
    "tenant_id": "acme",
    "question": "Did our cloud spend grow faster than revenue this quarter?"
  }
}
```

Returns:

```json
{
  "content": [{ "type": "text", "text": "..." }],
  "metadata": {
    "ledger_evidence": [{ "type": "ledger_transactions", "id": "tx_..." }],
    "wiki_pages_cited": [{ "slug": "/monthly-summaries/2025-09", "page_id": "wpg_..." }],
    "audit_event_id": "evt_..."
  }
}
```

#### `wiki.page.get`

Fetch a Wiki page by slug or id. Eight page types are available: `/accounts/{id}`, `/counterparties/{id}`, `/obligations/{id}`, `/invoices/{id}`, `/agents/{id}`, `/policies/{id}`, `/monthly-summaries/{YYYY-MM}`, `/cash-flow/{period}`.

| Argument     | Type   | Description |
| ------------ | ------ | ----------- |
| `tenant_id`  | string | Required    |
| `slug_or_id` | string | Required    |

The response includes the markdown body, structured sections (Current Truth, Key Linked Entities, Recent Activity, Open Questions, Risk Notes, Timeline, Evidence Links), and the `source_revision` checksum at render time.

### Raw Contribute

#### `raw.contribute`

Push a Raw artifact (transcript, document, structured observation) into the tenant's Raw layer. Artifact is content-addressed by SHA-256, attributed to the agent's on-chain registration record, and carries the agent's signature in its provenance.

| Argument        | Type          | Description                                                           |
| --------------- | ------------- | --------------------------------------------------------------------- |
| `tenant_id`     | string        | Required                                                              |
| `artifact_type` | string        | Required: `transcript`, `document`, `observation`                     |
| `mime_type`     | string        | Required                                                              |
| `content`       | base64 string | Required, the artifact bytes                                          |
| `source_ref`    | object        | Optional: source-specific identifiers                                 |
| `signature`     | hex string    | Required: the agent's signature over content + tenant\_id + timestamp |

{% hint style="info" %}
**Quarantine on first N contributions.** Agent-contributed artifacts are filtered from standard extraction pipelines until the tenant confirms the agent is trusted. Default trust level: quarantine for the first N contributions, auto-approve after.
{% endhint %}

Confidence on derived Ledger rows is capped at **0.5** for `provenance=agent_contributed`. Tenant or human review is required to lift the cap.

[**→ Agent Contributions**](/protocol/agent-contributions)

### PaymentIntent Propose

#### `payment_intent.propose`

Propose a financial action. Brain creates a `PaymentIntent` row in the Ledger in `proposed` status, runs Policy, and returns a decision. **No execute path on MCP.**

| Argument                      | Type    | Description                                                                                                                                                                                                                                                                |
| ----------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tenant_id`                   | string  | Required                                                                                                                                                                                                                                                                   |
| `action_type`                 | string  | Required: `ach_outbound`, `ach_inbound`, `wire`, `onchain_transfer`, `erp_writeback`, `card_payment`, `x402_settle`, `escrow_release` (same enum as the HTTP API). `x402_settle` additionally requires `pay_to`; `escrow_release` requires `escrow_id` + `job_terms_hash`. |
| `source_account_id`           | string  | Required                                                                                                                                                                                                                                                                   |
| `destination_counterparty_id` | string  | Required                                                                                                                                                                                                                                                                   |
| `amount`                      | decimal | Required                                                                                                                                                                                                                                                                   |
| `currency`                    | string  | Required                                                                                                                                                                                                                                                                   |
| `obligation_id`               | string  | Optional: links the intent to an obligation                                                                                                                                                                                                                                |
| `invoice_id`                  | string  | Optional                                                                                                                                                                                                                                                                   |
| `idempotency_key`             | string  | Required: caller-supplied unique key per intent                                                                                                                                                                                                                            |

Response includes the `payment_intent_id`, the `PolicyDecision`, and the next-step instruction (`pending_approval` with required approvers, or `approved` if policy returned `auto`).

[**→ Payment Intents**](/protocol/payment-intents)

### Agent Action Propose

#### `agent.action.propose`

Propose a non-financial action. Used by reconciliation, anomaly, or any agent action that doesn't move money.

| Argument          | Type   | Description                                                                                                                |
| ----------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- |
| `tenant_id`       | string | Required                                                                                                                   |
| `action_type`     | string | Required: `reconciliation_match`, `anomaly_flag`, `categorize_transaction`, `merge_counterparty`, `link_document`, `other` |
| `payload`         | object | Action-specific payload                                                                                                    |
| `linked_entities` | array  | Optional: array of `{ type, id }` references                                                                               |
| `idempotency_key` | string | Required                                                                                                                   |

The proposal goes through Policy and lands as a `proposals` row. Approval and dispatch follow the standard flow.

### Proposals and Evidence

These four tools mirror the HTTP [Proposals API](/api-reference/proposals-api) exactly. They share its read model and decision service, so tenant scoping, actor resolution, member authority, and the money-path approval gates behave identically over MCP.

#### `proposals.list`

List customer-facing agent proposals across payment intents and non-money findings. Tenant-scoped and cursor-paginated.

| Argument         | Type    | Description                                      |
| ---------------- | ------- | ------------------------------------------------ |
| `type`           | string  | Optional: one of the public proposal types.      |
| `status`         | string  | Optional: lifecycle status filter.               |
| `risk_band`      | string  | Optional: `low`, `standard`, `elevated`, `high`. |
| `min_confidence` | number  | Optional: float in `[0, 1]`.                     |
| `limit`          | integer | Optional: `1` to `100`.                          |
| `cursor`         | string  | Optional: pagination cursor.                     |

Public proposal types are `vendor_risk`, `payment`, `collections`, `treasury`, `cash_forecast`, `dispute`, `compliance`, `revenue_intel`, `reconciliation`, `subscription`, `fraud_anomaly`, `personal_budget`, `financial_health`, `purchase_advisor`, `tax_prep`, `travel_finance`, `bill_management`, `debt_optimization`, and `savings`.

Each returned proposal mirrors the HTTP read model, including the compact fields plus `stored_action_type`, `details`, `policy`, `presentation`, and `available_decisions`. Stored action names are mapped to public proposal types by Brain Core before they are returned. For example, `flag_transaction` returns as `fraud_anomaly`, `block_payment` as `vendor_risk`, and `propose_match` as `reconciliation`; ambiguous names such as `notify` resolve through the agent role.

#### `proposals.get`

Read one tenant-scoped proposal by id.

| Argument      | Type   | Description |
| ------------- | ------ | ----------- |
| `proposal_id` | string | Required    |

#### `proposals.decide`

Record a human decision on a proposal. Delegates to the same decision service as the HTTP route, including user-principal actor resolution, active-member checks, approval-role checks, money-path approval gates, and audit.

| Argument      | Type   | Description                                          |
| ------------- | ------ | ---------------------------------------------------- |
| `proposal_id` | string | Required                                             |
| `decision`    | string | Required: `approve`, `reject`, `acknowledge`, `undo` |

{% hint style="warning" %}
`proposals.decide` declares no tool-level scope because authority is enforced downstream. The caller must resolve to a **user-principal, active member with approval authority**. A propose-only agent principal is rejected with `actor_unresolved`: an agent can list and read proposals but can never decide one.
{% endhint %}

#### `evidence.resolve`

Resolve typed proposal evidence refs into tenant-scoped summaries and deep links where the ref kind is supported.

| Argument | Type  | Description                               |
| -------- | ----- | ----------------------------------------- |
| `refs`   | array | Required: up to 50 `{ kind, ref }` pairs. |

Resolution fails closed: a supported ref that does not exist returns `not_found`, and an unsupported kind or malformed ref returns `resolvable: false` with a `reason`, never an error. Resolvable kinds: `account`, `counterparty`, `invoice`, `obligation`, `transaction`, `wiki_entity`.

### Per-Call Scope Enforcement

Even with the right top-level scope, each tool call is scope-checked at invocation. A token with `ledger:read` cannot call `wiki.question`. A token with `wiki:read` cannot call `raw.contribute`. The MCP layer rejects scope mismatches with JSON-RPC error `-32002` (scope insufficient / tenant mismatch). `-32004` is reserved for pre-execution gate failures; see the [error reference](/resources/errors).

### Idempotency

Mutating tools (`raw.contribute`, `payment_intent.propose`, `agent.action.propose`) require an `idempotency_key`. The key is per-tool, per-tenant, per-agent. Brain caches the response for 24 hours and returns the cached result on retry.

### 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>📦 Resources</strong></td><td>Address Ledger and Raw rows by URI.</td><td><a href="/pages/L2DEzPLxdznDACZBKJEr">/pages/L2DEzPLxdznDACZBKJEr</a></td><td></td></tr><tr><td><strong>💬 Prompts</strong></td><td>Canned prompts for common agent loops.</td><td><a href="/pages/IGFOhlkkHijzYzj8v8lp">/pages/IGFOhlkkHijzYzj8v8lp</a></td><td></td></tr><tr><td><strong>🪪 Authentication</strong></td><td>JWT and on-chain scope verification.</td><td><a href="/pages/ZNnH2CuEVwz0dbBj8bG0">/pages/ZNnH2CuEVwz0dbBj8bG0</a></td><td></td></tr></tbody></table>


# Resources

Brain's MCP server exposes **7 resource templates** that let agents address Brain entities by URI. Resources complement tools: where tools are verbs (`tools/call`), resources are nouns (`resources/read`).

| Property           | Value                            |
| ------------------ | -------------------------------- |
| **URI scheme**     | `brain://`                       |
| **MCP method**     | `resources/read`                 |
| **Required scope** | Same as the equivalent read tool |

### The 7 Templates

| Resource                       | URI Pattern                                          | Required Scope           |
| ------------------------------ | ---------------------------------------------------- | ------------------------ |
| **Ledger account**             | `brain://ledger/accounts/{account_id}`               | `ledger:read`            |
| **Ledger transaction**         | `brain://ledger/transactions/{transaction_id}`       | `ledger:read`            |
| **Ledger obligation**          | `brain://ledger/obligations/{obligation_id}`         | `ledger:read`            |
| **Payment intent**             | `brain://ledger/payment-intents/{payment_intent_id}` | `ledger:read`            |
| **Wiki page**                  | `brain://wiki/pages/{slug}`                          | `wiki:read`              |
| **PaymentIntent action types** | `brain://payments/action_types`                      | `payment_intent:propose` |
| **Action proof (H-07)**        | `brain://proofs/{action_id}`                         | `audit:read`             |

### Why Resources

Tools are good for queries with arguments. Resources are good for entities with stable identifiers that an agent already knows about: a transaction id from a recent `ledger.transactions.list` response, a payment intent id from a previous propose, a wiki page slug like `/monthly-summaries/2025-09`.

Treating them as resources rather than tool calls has three benefits:

| Benefit              | Detail                                                                                         |
| -------------------- | ---------------------------------------------------------------------------------------------- |
| **Cacheable**        | An MCP runtime can cache resource reads by URI without understanding the tool's argument shape |
| **Context-friendly** | Agents can pass URIs back and forth in their planning context without re-fetching              |
| **Discoverable**     | `resources/list` enumerates the URI templates Brain advertises                                 |

### Reading a Resource

```http
POST /v1/agents/mcp HTTP/1.1
Authorization: Bearer <jwt>
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "resources/read",
  "params": {
    "uri": "brain://ledger/transactions/tx_4127"
  }
}
```

Response:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "contents": [
      {
        "uri": "brain://ledger/transactions/tx_4127",
        "mimeType": "application/json",
        "text": "{ \"id\": \"tx_4127\", \"amount\": \"61404.12\", \"currency\": \"USD\", ... }"
      }
    ]
  }
}
```

### URI Examples

```
brain://ledger/accounts/acct_8231
brain://ledger/transactions/tx_4127
brain://ledger/obligations/obl_5521
brain://ledger/payment-intents/pi_a1b2c3
brain://wiki/pages/monthly-summaries/2025-09
brain://wiki/pages/counterparties/cp_aws
brain://payments/action_types
brain://proofs/act_01HW3X9...
```

{% hint style="info" %}
The Wiki URI uses the page slug, not the page id. Slugs are stable across regenerations; ids change when a page is regenerated. For agent context that needs to survive regeneration, use the slug.
{% endhint %}

### Resource Discovery

#### `resources/list`

Returns the 7 static URI templates Brain advertises. It is not a per-entity enumeration: the response is the fixed template set below, not one row per account, page, or artifact.

```json
{
  "resources": [
    {
      "uri": "brain://ledger/accounts/{account_id}",
      "name": "Account",
      "description": "Account row + latest balance.",
      "mimeType": "application/json"
    },
    {
      "uri": "brain://ledger/transactions/{transaction_id}",
      "name": "Transaction",
      "description": "Transaction row.",
      "mimeType": "application/json"
    },
    {
      "uri": "brain://ledger/obligations/{obligation_id}",
      "name": "Obligation",
      "description": "Obligation row.",
      "mimeType": "application/json"
    },
    {
      "uri": "brain://ledger/payment-intents/{id}",
      "name": "PaymentIntent",
      "description": "PaymentIntent row + PolicyDecision id.",
      "mimeType": "application/json"
    },
    {
      "uri": "brain://wiki/pages/{slug}",
      "name": "Wiki page",
      "description": "Memory page (markdown body).",
      "mimeType": "text/markdown"
    },
    {
      "uri": "brain://payments/action_types",
      "name": "PaymentIntent action types",
      "description": "Canonical action_type vocabulary + required fields for payment_intent.propose.",
      "mimeType": "application/json"
    },
    {
      "uri": "brain://proofs/{action_id}",
      "name": "Action proof (H-07)",
      "description": "Canonical proof for an executed action: gate trace, policy decision, audit before/after, Merkle proof, and on-chain anchor tx hash.",
      "mimeType": "application/json"
    }
  ]
}
```

{% hint style="info" %}
Only `resources/list` and `resources/read` are implemented. There is no `resources/templates/list` method on the Brain MCP surface.
{% endhint %}

### What Resources Are Not

| Not a Resource                 | Why                                                                          |
| ------------------------------ | ---------------------------------------------------------------------------- |
| Lists, queries, search results | Tools handle those (`*.list`)                                                |
| Newly proposed entities        | The propose tool is the canonical entry point; resources are for fetch-by-id |
| Streaming feeds                | Single-shot HTTP today; streaming may follow when there is a clear use case  |

### Audit

Every successful `resources/read` emits an `agent.mcp.tool_called` audit event with `method: "resources/read"` and the URI in `inputs`. This means a tenant can see exactly which agent fetched which entity at which time, just like for tool calls.

### 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>🛠️ Tools</strong></td><td>The 16 tools at the heart of the MCP surface.</td><td><a href="/pages/LEWpOYJSpmIuTr20aNe8">/pages/LEWpOYJSpmIuTr20aNe8</a></td><td></td></tr><tr><td><strong>💬 Prompts</strong></td><td>Canned prompts for common agent loops.</td><td><a href="/pages/IGFOhlkkHijzYzj8v8lp">/pages/IGFOhlkkHijzYzj8v8lp</a></td><td></td></tr></tbody></table>


# Prompts

Brain's MCP server ships **5 canned prompts** for the most common agent loops. Prompts are pre-templated invocations that combine a question, the right resources to read, and the expected response shape.

| Property           | Value                             |
| ------------------ | --------------------------------- |
| **MCP method**     | `prompts/get` and `prompts/list`  |
| **Required scope** | Same as the underlying read tools |

### Why Canned Prompts

Most external agents end up reinventing the same five questions in their first day of integration. Canned prompts give them a one-shot way to get a high-quality answer without designing the chain themselves.

| Prompt                            | Question It Answers                                                     | Underlying Reads                          |
| --------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------- |
| `wiki.question.cash_flow_summary` | "What's our cash position right now and over the last 30 days?"         | accounts, balances, transactions          |
| `wiki.question.bills_due`         | "What bills are coming due in the next N days, in priority order?"      | obligations, counterparties               |
| `wiki.question.spending_change`   | "What changed in our spending versus the prior period?"                 | transactions, categories                  |
| `wiki.question.invoice_status`    | "What invoices are outstanding, and which are overdue?"                 | invoices, transactions, counterparties    |
| `wiki.question.subscriptions`     | "What recurring subscriptions are we paying for, and which are unused?" | obligations, transactions, counterparties |

### Anatomy of a Prompt

A prompt is a structured object that tells the agent's LLM how to use Brain's MCP surface to answer a specific class of question.

```json
{
  "name": "wiki.question.cash_flow_summary",
  "description": "Ask Brain to summarize cash flow over a period.",
  "arguments": [
    {
      "name": "period",
      "description": "A human-readable period, e.g. 'this month', 'Q1 2026', '2026-04'.",
      "required": true
    }
  ]
}
```

### `wiki.question.cash_flow_summary`

Pulls all active accounts, fetches the latest balances, lists transactions in the period grouped by direction (`inflow` vs `outflow`), and returns a structured summary plus a narrative.

Typical inputs:

```json
{ "period": "this month" }
```

Typical output sections:

| Section             | Content                                                   |
| ------------------- | --------------------------------------------------------- |
| **Cash position**   | Sum of `current_balance` across active accounts           |
| **30-day inflows**  | Total inflows, top 5 sources                              |
| **30-day outflows** | Total outflows, top 5 destinations                        |
| **Net change**      | Inflows minus outflows                                    |
| **Anomalies**       | Flagged transactions over the agent's heuristic threshold |
| **Evidence**        | Ledger transaction ids cited                              |

### `wiki.question.bills_due`

Lists obligations with `status in (upcoming, due, overdue)` ordered by `due_date`, with priority hints based on amount, counterparty risk, and days-until-due.

Typical inputs:

```json
{ "days": 14 }
```

Each entry includes the `obligation_id`, `amount_due`, `due_date`, `counterparty.name`, `counterparty.verified_status`, and a recommended action: `pay_now`, `schedule`, `review`, or `escalate`.

{% hint style="info" %}
The recommendation is generated by the calling agent, not by Brain. Brain returns the structured facts; the agent's reasoning produces the priority order.
{% endhint %}

### `wiki.question.spending_change`

Compares the given period against the prior comparable period and surfaces the categories with the largest delta.

Typical inputs:

```json
{ "period": "2025-09" }
```

Returns categories sorted by absolute change, with citations to specific transactions and counterparties driving the change.

### `wiki.question.invoice_status`

Reports the status of a specific invoice: whether it has been paid in full, partially, or not at all, with the linked transactions cited.

Typical inputs:

```json
{ "invoice_number": "INV-1042" }
```

Aging buckets: `current`, `1-30 days`, `31-60 days`, `61-90 days`, `90+ days`. Each invoice includes `linked_transaction_ids[]` so the agent can verify partial payments.

### `wiki.question.subscriptions`

Identifies recurring obligations and pairs them with usage signals where available.

This prompt takes no arguments.

Returns each subscription's `counterparty`, `monthly_amount`, `start_date`, `last_charge`, `recurrence`, and a `freshness` signal computed from related Raw evidence (e.g., when the agent has contributed usage transcripts via `raw.contribute`, those are surfaced here).

### Listing and Getting Prompts

```http
POST /v1/agents/mcp HTTP/1.1
{ "jsonrpc": "2.0", "id": 1, "method": "prompts/list" }
```

```http
POST /v1/agents/mcp HTTP/1.1
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "prompts/get",
  "params": {
    "name": "wiki.question.cash_flow_summary",
    "arguments": { "period": "this month" }
  }
}
```

The `prompts/get` response contains a `messages[]` array suitable for direct injection into an LLM's context window. The agent runtime can render the messages, execute the embedded tool calls (Brain returns them with the right URIs and arguments pre-filled), and produce the final answer.

### Audit

Like tools and resources, every `prompts/get` invocation emits an `agent.mcp.tool_called` audit event with `method: "prompts/get"` and the prompt name plus arguments in `inputs`.

### 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>🛠️ Tools</strong></td><td>The 16 tools the prompts orchestrate.</td><td><a href="/pages/LEWpOYJSpmIuTr20aNe8">/pages/LEWpOYJSpmIuTr20aNe8</a></td><td></td></tr><tr><td><strong>📦 Resources</strong></td><td>The 7 resource templates prompts can reference.</td><td><a href="/pages/L2DEzPLxdznDACZBKJEr">/pages/L2DEzPLxdznDACZBKJEr</a></td><td></td></tr></tbody></table>


# MCP Authentication

External agents authenticate to Brain's MCP server with a **JWT** that anchors back to an on-chain registration in `BrainMCPAgentRegistry`. There are two layers of verification: the JWT itself, and the cryptographic match between the JWT's `scope_hash` claim and the on-chain hash.

### The Auth Chain

```
┌─────────────────────────────────────────────────┐
│  External agent                              │
│  signs JWT with agent's signing key             │
└────────────────┬────────────────────────────────┘
                 │  Authorization: Bearer <jwt>
                 ▼
┌─────────────────────────────────────────────────┐
│  Brain edge                                     │
│  - Validates JWT signature                      │
│  - Resolves principal (tenant + scopes)         │
└────────────────┬────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────┐
│  MCP dispatcher                                 │
│  Three pre-call checks:                         │
│  1. Agent record in `agents` is `active`        │
│  2. JWT `scope_hash` claim matches on-chain     │
│     hash in BrainMCPAgentRegistry               │
│     (verified once, cached 60 s per agent)      │
│  3. JWT `tenant_id` equals agent's tenant       │
└────────────────┬────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────┐
│  Per-tool scope enforcement                     │
│  Method dispatcher checks the called tool's     │
│  scope against the agent's granted scopes       │
└─────────────────────────────────────────────────┘
```

### JWT Structure

The JWT is signed by the agent's signing key (the same key registered in `BrainMCPAgentRegistry`).

```json
{
  "iss": "agent:0xAgentAddress",
  "sub": "tenant:acme",
  "iat": 1735689600,
  "exp": 1735693200,
  "agent_id": "ag_8231",
  "tenant_id": "acme",
  "scope_hash": "0xabc123..."
}
```

| Claim        | Purpose                                                             |
| ------------ | ------------------------------------------------------------------- |
| `iss`        | Agent's on-chain address                                            |
| `sub`        | Tenant the call is on behalf of                                     |
| `iat`, `exp` | Issued / expiry, max 1-hour TTL                                     |
| `agent_id`   | Brain-internal agent id                                             |
| `tenant_id`  | Tenant id, must match the agent's tenant in `BrainMCPAgentRegistry` |
| `scope_hash` | Hash of the canonical scope document; must match on-chain           |

### On-Chain Scope Verification

This is the move that makes Brain's agent surface different from a typical OAuth integration: **scope is anchored on-chain**.

When the tenant authorized the agent, they signed an EIP-712 message that registered the agent with a `scopeHash` in `BrainMCPAgentRegistry`. The scope document itself stays off-chain; only its hash is on-chain.

```solidity
struct AgentRegistration {
  bytes32 agentId;
  address agentAddress;
  bytes32 tenantId;
  bytes32 scopeHash;     // keccak-256 of canonical scope set
  bytes32 behaviorHash;  // optional behaviour pin; bytes32(0) if unused
  uint256 registeredAt;
  uint256 revokedAt;     // 0 if active
}
```

When an agent makes an MCP call, the JWT presents a `scope_hash` claim. The MCP server verifies that this claim equals the `scopeHash` stored on-chain at the agent's registration record:

| Step | Check                                                    |
| ---- | -------------------------------------------------------- |
| 1    | Read `BrainMCPAgentRegistry.getAgent(agentId)`           |
| 2    | Compare on-chain `scopeHash` to JWT's `scope_hash` claim |
| 3    | Verify `revokedAt == 0` (agent not revoked)              |
| 4    | Verify on-chain `tenantId` matches JWT's `tenant_id`     |

The on-chain read is **cached for 60 seconds per agent**. This balances on-chain verification cost against revocation latency: a revoked agent is rejected within at most 60 seconds.

{% hint style="warning" %}
**Revocation is immediate and on-chain.** A tenant can revoke an agent's authorization at any time by calling `revokeAgent` on `BrainMCPAgentRegistry` with their EIP-712 signature. Within the cache window (<= 60 seconds), the MCP server rejects all subsequent calls.
{% endhint %}

### The Capability Scopes

The canonical scope document enumerates which of these the tenant has granted to the agent.

| Scope                    | Allows                                                                                                   |
| ------------------------ | -------------------------------------------------------------------------------------------------------- |
| `ledger:read`            | All `ledger.*` read tools and `brain://ledger/...` resources                                             |
| `wiki:read`              | All `wiki.*` read tools and `brain://wiki/pages/...` resources                                           |
| `raw:write`              | The `raw.contribute` tool                                                                                |
| `payment_intent:propose` | The `payment_intent.propose` tool and the `brain://payments/action_types` resource                       |
| `payment_intent:approve` | Accepted at the `proposals.decide` call boundary; member approval authority is enforced downstream       |
| `execution:read`         | The `proposals.list`, `proposals.get`, and `evidence.resolve` tools; also accepted by `proposals.decide` |
| `execution:propose`      | The `agent.action.propose` tool                                                                          |
| `audit:read`             | The `brain://proofs/{action_id}` resource                                                                |

A tenant can grant any subset. Unused scopes do not appear in the canonical document. The `scopeHash` is the **keccak-256** of the canonical, lexicographically-sorted scope set (`computeAgentScopeHash` in `shared/src/agents/capability.ts`); it is the same hash the registration tooling writes on-chain, so the seed, JWT claim, and registry agree byte-for-byte.

### Per-Call Scope Enforcement

Even after the three pre-call checks pass, each tool invocation is scope-checked. Calling `wiki.question` with a JWT that lacks `wiki:read` returns:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32002,
    "message": "tool 'wiki.question' requires scope 'wiki:read'",
    "data": {
      "brain_code": "auth_scope_insufficient",
      "details": { "required": ["wiki:read"], "held": ["ledger:read"] }
    }
  }
}
```

Per-call scope enforcement runs **after** authentication, inside JSON-RPC dispatch, so it surfaces as a JSON-RPC `error` (HTTP `200`), unlike the pre-dispatch auth checks below.

### Error Codes: two surfaces

Brain's MCP surface fails in two distinct places, and the shape of the error differs:

**1. Pre-dispatch auth failures → HTTP `401`/`403` Brain error envelope.** The route guard (`services/mcp/src/transport/http.ts`) checks the JWT and principal type; the auth verifier (`services/mcp/src/auth.ts`, run at the top of `server.handle`) checks on-chain registration, scope-hash, and tenant. All of these throw a `BrainError` that propagates out of the handler **before** any method is dispatched, so the client sees an HTTP `401`/`403` with a Brain error envelope (`{ "error_code": ..., "message": ... }`), **not** a JSON-RPC response.

| `error_code`                         | HTTP | Meaning                                               |
| ------------------------------------ | ---- | ----------------------------------------------------- |
| `auth_token_missing/invalid/expired` | 401  | JWT absent, malformed, or expired                     |
| `auth_scope_insufficient`            | 401  | Principal is not `principal_type=agent`               |
| `auth_tenant_mismatch`               | 401  | JWT `tenant_id` != agent's registered tenant          |
| `agent_not_registered`               | 401  | Agent row missing in `agents`, or not `active`        |
| `agent_not_registered_onchain`       | 401  | Agent has no record in `BrainMCPAgentRegistry`        |
| `agent_scope_hash_missing`           | 401  | Agent row has no on-chain scope attestation           |
| `agent_scope_hash_mismatch`          | 401  | DB `scope_hash` differs from the on-chain `scopeHash` |

**2. Post-auth JSON-RPC errors → HTTP `200` with a JSON-RPC `error`.** Once the call has authenticated, failures inside method dispatch (scope, gate, params) are carried in the JSON-RPC `error` field with `data.brain_code` set:

| Code     | Meaning                                                                                   |
| -------- | ----------------------------------------------------------------------------------------- |
| `-32001` | JWT invalid/expired (`auth_token_*`), in-dispatch only                                    |
| `-32002` | Scope insufficient / tenant mismatch (`auth_scope_insufficient`, `auth_tenant_mismatch`)  |
| `-32003` | Agent not registered or inactive (`agent_not_registered`, `agent_not_registered_onchain`) |
| `-32004` | Pre-execution gate failed (`payment_intent_gate_failed`, every `gate_*`)                  |
| `-32005` | On-chain `scope_hash` mismatch (`agent_scope_hash_mismatch`)                              |
| `-32600` | Standard JSON-RPC: invalid request                                                        |
| `-32601` | Standard JSON-RPC: method not found                                                       |
| `-32602` | Standard JSON-RPC: invalid params                                                         |
| `-32603` | Standard JSON-RPC: internal error                                                         |

The numeric mapping lives in `services/mcp/src/dispatcher.ts`. In practice the auth-class codes (`-32001`/`-32002`/`-32003`/`-32005`) are reached via the HTTP envelope above, since auth runs before dispatch; the JSON-RPC codes you will actually observe in a `200` body are `-32004` (gate), `-32002` (per-tool scope), and the standard `-326xx` family.

### Token Lifetimes

| Token                         | TTL           | Refreshable                 |
| ----------------------------- | ------------- | --------------------------- |
| **Agent JWT**                 | Max 1 hour    | Yes; agent signs a new JWT  |
| **Cached scope verification** | 60 seconds    | Auto-refreshes on next call |
| **On-chain registration**     | Until revoked | N/A; on-chain               |

### Revoking an Agent

Two paths:

| Path                  | Effect                                                                            |
| --------------------- | --------------------------------------------------------------------------------- |
| **Tenant in Console** | Generates EIP-712 revocation signature, calls `BrainMCPAgentRegistry.revokeAgent` |
| **Tenant via API**    | `POST /v1/agents/{agent_id}/revoke` with the tenant's signature                   |

After revocation, the on-chain `revokedAt` becomes non-zero, so the scope read returns null and, within the 60-second cache window, all calls are rejected pre-dispatch with an HTTP `401` `agent_not_registered_onchain` envelope.

### 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>🛠️ Tools</strong></td><td>The 16 tools and their per-tool scope requirements.</td><td><a href="/pages/LEWpOYJSpmIuTr20aNe8">/pages/LEWpOYJSpmIuTr20aNe8</a></td><td></td></tr><tr><td><strong>🪪 BrainMCPAgentRegistry</strong></td><td>The on-chain contract this all anchors to.</td><td><a href="/pages/7cGQBqLnTUZjyofcuHlm">/pages/7cGQBqLnTUZjyofcuHlm</a></td><td></td></tr></tbody></table>


# Overview

Brain's on-chain surface is intentionally small. Most logic lives off-chain. On-chain contracts exist to anchor state, register identity, enforce session-key scope and spend caps, and route agent execution.

| Property            | Value                                                                                                                                                                 |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Network**         | Base L2                                                                                                                                                               |
| **Language**        | Solidity 0.8.x                                                                                                                                                        |
| **Toolchain**       | Foundry                                                                                                                                                               |
| **Upgrade pattern** | Immutable. No upgrade path in MVP; changes ship as audited redeploys                                                                                                  |
| **Audits**          | External security audit required before mainnet. The escrow + reputation contracts are **UNAUDITED** and run on **Base Sepolia testnet** only until that audit clears |
| **Bug bounty**      | Public coverage                                                                                                                                                       |

### Core Contracts

The six deployed contracts. All are Base Sepolia today; mainnet remains blocked on the external smart-contract audit.

<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>BrainAuditAnchor</strong></td><td>Stores Merkle roots of per-tenant audit batches. Immutable after submission.</td><td><a href="/pages/5njwTjZlypdSt55BbRDG">/pages/5njwTjZlypdSt55BbRDG</a></td><td></td></tr><tr><td><strong>BrainPolicyRegistry</strong></td><td>Registers policy version hashes per tenant, signed via EIP-712.</td><td><a href="/pages/Qk74oUUATzrLis1xhGcb">/pages/Qk74oUUATzrLis1xhGcb</a></td><td></td></tr><tr><td><strong>BrainSmartAccount</strong></td><td>Per-tenant session-key smart account; <code>executeViaSessionKey</code> enforces scope, spend caps, and the bound <code>policyVersion</code> on-chain. Immutable.</td><td><a href="/pages/2xFXIKlbOlKKY8V47AgE">/pages/2xFXIKlbOlKKY8V47AgE</a></td><td></td></tr><tr><td><strong>BrainMCPAgentRegistry</strong></td><td>Stores agent identity and scope as <code>agentId</code>/<code>tenantId</code>/<code>scopeHash</code>/<code>behaviorHash</code> hashes. Reputation lives in a separate contract.</td><td><a href="/pages/7cGQBqLnTUZjyofcuHlm">/pages/7cGQBqLnTUZjyofcuHlm</a></td><td></td></tr></tbody></table>

### Settlement and Reputation (UNAUDITED. Base Sepolia testnet reference contracts)

<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>BrainEscrow</strong></td><td>Custodial USDC escrow for conditional M2M settlement: lock against a hashed job commitment, then <strong>incremental</strong> release/refund. UNAUDITED, testnet only.</td><td><a href="/pages/pdclcSITK44863badHKU">/pages/pdclcSITK44863badHKU</a></td><td></td></tr><tr><td><strong>BrainReputationRegistry</strong></td><td>ERC-8004-style per-agent reputation <strong>pointer</strong> (Merkle root); read by Policy as a tighten-only threshold input. Never a money gate. Non-custodial. UNAUDITED, testnet only.</td><td><a href="/pages/0TL0aQXn4l3eiEqu51uS">/pages/0TL0aQXn4l3eiEqu51uS</a></td><td></td></tr><tr><td><strong>x402 Settlement</strong></td><td>HTTP-native machine payments (USDC on Base) for per-call API access, settled through the §6 gate.</td><td><a href="/pages/pdclcSITK44863badHKU">/pages/pdclcSITK44863badHKU</a></td><td></td></tr></tbody></table>

### Deployed Addresses

All six contracts are deployed on **Base Sepolia (chain `84532`)**. There is **no mainnet deployment**; mainnet is blocked on the external smart-contract audit. A `brain.proof()` result anchors to `BrainAuditAnchor`; look the anchor tx up on the explorer to verify it independently.

| Contract                  | Base Sepolia address                                                                                                            | Base mainnet                               |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| `BrainAuditAnchor`        | [`0xb900add824064098342c869ff83efdeb05eb95ce`](https://sepolia.basescan.org/address/0xb900add824064098342c869ff83efdeb05eb95ce) | pending external audit                     |
| `BrainPolicyRegistry`     | [`0x92d1CC5c46eAE229C8A9dD95a334cec0cE33CAD9`](https://sepolia.basescan.org/address/0x92d1CC5c46eAE229C8A9dD95a334cec0cE33CAD9) | pending external audit                     |
| `BrainSmartAccount`       | [`0x8cC094d03676d29c8cE0267480f58188E7F1E23D`](https://sepolia.basescan.org/address/0x8cC094d03676d29c8cE0267480f58188E7F1E23D) | pending external audit                     |
| `BrainMCPAgentRegistry`   | [`0xcE7Ce9dd95c17E1F4E27D49249b6fdb015f3A7e0`](https://sepolia.basescan.org/address/0xcE7Ce9dd95c17E1F4E27D49249b6fdb015f3A7e0) | pending external audit                     |
| `BrainEscrow`             | [`0x5924BD26Bc827FB3cAd6f3a0DBDC793562555Cc0`](https://sepolia.basescan.org/address/0x5924BD26Bc827FB3cAd6f3a0DBDC793562555Cc0) | pending external audit (UNAUDITED testnet) |
| `BrainReputationRegistry` | [`0xcEf6C25aE3DF9c5cfC0B3E11D031eAAa2c26026C`](https://sepolia.basescan.org/address/0xcEf6C25aE3DF9c5cfC0B3E11D031eAAa2c26026C) | pending external audit (UNAUDITED testnet) |

(Authoritative copy lives in `SECURITY.md`.)

### Standards Composed

| Standard                      | Role in Brain                                                                                                                                                       |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Session-key smart account** | Owner-granted scoped, spend-capped, policyVersion-bound keys; `executeViaSessionKey` enforces the bounds on-chain                                                   |
| **EIP-7702**                  | *Planned (RFC 0001)*. Delegated execution for EOAs (single-session lifetime); not shipped in MVP                                                                    |
| **ERC-8004**                  | *ERC-8004-style*. `BrainReputationRegistry` (RFC 0001, **UNAUDITED testnet**): a per-agent reputation pointer/Merkle root, read by Policy as a threshold input only |
| **BrainEscrow**               | Custodial USDC escrow for conditional M2M settlement. Incremental release/refund (RFC 0001, **UNAUDITED testnet**). A custom hash-only contract, not ERC-8183.      |
| **EIP-712**                   | Typed-data signatures for policies, scopes, approvals                                                                                                               |
| **EIP-4361 (SIWX)**           | Sign-In With X for agent authentication                                                                                                                             |
| **x402**                      | HTTP-native machine settlement                                                                                                                                      |

### Operational Safety

| Mechanism                      | Purpose                                                                             |
| ------------------------------ | ----------------------------------------------------------------------------------- |
| **Immutable contracts**        | No upgrade path in MVP; any change ships as a separately audited redeploy           |
| **Anchorer key hardening**     | Current testnet publisher is a single EOA; HSM-backed signing is a pre-mainnet TODO |
| **Root-uniqueness per tenant** | A published root cannot be re-anchored, so history cannot be silently rewritten     |
| **External audit**             | Required before any mainnet deployment, plus a public bug bounty                    |

{% hint style="info" %}
Most logic is off-chain by design. The on-chain surface is the smallest possible footprint required to anchor truth, register identity, and enforce session-key scope and spend caps.
{% endhint %}

### Threat Model

| Trusted                                   | Untrusted                           |
| ----------------------------------------- | ----------------------------------- |
| The user's smart account contract code    | Any off-chain backend               |
| `BrainPolicyRegistry`, `BrainAuditAnchor` | Any RPC endpoint                    |
| The user's owner key                      | Any UI or hosted service            |
| Future HSM-protected anchorer keys        | Any individual extractor or service |

Even if Brain's backend were fully compromised, an attacker would still need to produce a valid EIP-712 signature from a key the on-chain contracts recognize. Stale verdicts expire. Reused nonces are rejected.

### 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>BrainAuditAnchor</strong></td><td>Merkle anchoring for audit history.</td><td><a href="/pages/5njwTjZlypdSt55BbRDG">/pages/5njwTjZlypdSt55BbRDG</a></td><td></td></tr><tr><td><strong>BrainPolicyRegistry</strong></td><td>Policy version hashes on-chain.</td><td><a href="/pages/Qk74oUUATzrLis1xhGcb">/pages/Qk74oUUATzrLis1xhGcb</a></td><td></td></tr><tr><td><strong>BrainSmartAccount</strong></td><td>Session-key account with policy and scope checks.</td><td><a href="/pages/2xFXIKlbOlKKY8V47AgE">/pages/2xFXIKlbOlKKY8V47AgE</a></td><td></td></tr></tbody></table>


# BrainAuditAnchor

`BrainAuditAnchor` stores Merkle roots of per-tenant audit batches. Anchors are immutable after submission.

| Property         | Value                                                                                 |
| ---------------- | ------------------------------------------------------------------------------------- |
| **Network**      | Base Sepolia only                                                                     |
| **Solidity**     | 0.8.x                                                                                 |
| **Pattern**      | Immutable. No upgrade path in MVP; changes require a redeploy.                        |
| **Audit status** | Unaudited. Base mainnet is fenced pending an external smart-contract audit.           |
| **Publisher**    | Single EOA at `0x41d4ce9d9fe968ca1230bdc296b28fdc9aa6ff6e`, verified on Base Sepolia. |

### Interface

```solidity
interface IBrainAuditAnchor {
    event AnchorPublished(
        bytes32 indexed tenantId,
        bytes32 root,
        uint256 eventCount,
        uint256 periodStart,
        uint256 periodEnd
    );

    function anchor(
        bytes32 tenantId,
        bytes32 root,
        uint256 eventCount,
        uint256 periodStart,
        uint256 periodEnd
    ) external;  // onlyPublisher

    function anchorBatch(
        bytes32[] calldata tenantIds,
        bytes32[] calldata roots,
        uint256[] calldata eventCounts,
        uint256[] calldata periodStarts,
        uint256[] calldata periodEnds
    ) external;  // onlyPublisher, maximum 50 entries

    function publisher() external view returns (address);

    function MAX_BATCH() external view returns (uint256);

    function latestAnchor(bytes32 tenantId)
        external view returns (bytes32 root, uint256 blockNumber);

    function latestAnchorFull(bytes32 tenantId)
        external view
        returns (bytes32 root, uint256 blockNumber, uint256 eventCount, uint256 periodEnd);

    function verifyInclusion(
        bytes32 root,
        bytes32 leaf,
        bytes32[] calldata proof
    ) external pure returns (bool);

    function isPublished(bytes32 tenantId, bytes32 root)
        external view returns (bool);
}
```

Publication is authorized by the caller, not by a per-call signature. `anchor` and `anchorBatch` are `onlyPublisher`. The current Base Sepolia publisher is a single EOA, not a Safe multisig. `setPublisher` and `acceptPublisher` provide a two-step rotation path.

### How Anchoring Works

```
Off-chain audit log
   │
   ├─ events batched per tenant over a period window
   │
   ├─ Merkle tree built per batch
   │
   └─ publisher calls anchor() or anchorBatch() on Base Sepolia
```

| Step | Detail                                                                              |
| ---- | ----------------------------------------------------------------------------------- |
| 1    | Audit events batch into a Merkle tree per tenant over a period window               |
| 2    | The publisher submits root, event count, and period bounds                          |
| 3    | `anchor()` publishes one root, or `anchorBatch()` publishes up to `MAX_BATCH` roots |
| 4    | Contract emits `AnchorPublished`; the root becomes immutably retrievable            |

### Replay Protection

The contract records every published `(tenantId, root)` pair and rejects a repeat.

| Behavior                           | Detail                                   |
| ---------------------------------- | ---------------------------------------- |
| **First time a root is published** | Stored as the tenant's latest anchor     |
| **Re-publishing the same root**    | Reverts with `RootAlreadyPublished`      |
| **Period bounds**                  | `periodEnd` before `periodStart` reverts |

Root-uniqueness per tenant is the replay guard: a published root cannot be re-anchored for the same tenant. There is no batch-index sequence to maintain, so anchoring never depends on submission order.

`anchorBatch()` has the same period validation as `anchor()` and a hard `MAX_BATCH` cap of 50. Unlike single-root `anchor()`, it skips an already published `(tenantId, root)` pair. This makes a batch retry safe after a prior partial success.

### Verification by Counterparties

A counterparty does not need a Brain account to verify an audit event. They just need:

| Input     | Source                                                            |
| --------- | ----------------------------------------------------------------- |
| `root`    | The published Merkle root (read via `latestAnchor` or event logs) |
| `leaf`    | Hash of the event being verified                                  |
| `proof[]` | Merkle path supplied by Brain                                     |

```solidity
bool valid = anchor.verifyInclusion(root, leaf, proof);
```

If `valid` is true, the event is provably part of the anchored history under that root. `verifyInclusion` uses domain-separated hashing: leaf nodes are `keccak256(0x00 ++ leaf)` and internal nodes are `keccak256(0x01 ++ sort(left, right))`.

{% hint style="success" %}
The verifier does not need to trust Brain. They only need to call a public view function on Base L2.
{% endhint %}

### Reorg Tolerance

Base L2 has fast finality, but small reorgs are possible.

| Mitigation                                 | Detail                                                                          |
| ------------------------------------------ | ------------------------------------------------------------------------------- |
| **Confirmation depth**                     | Reads wait for a configurable depth before treating an anchor as final          |
| **Retryable publication**                  | The publisher retains pending anchors and retries them after transient failures |
| **Off-chain log canonical until anchored** | A record remains pending until its on-chain transaction is confirmed            |

### Publisher Rotation

The publisher is rotated through a two-step handoff so a mistyped or uncontrolled address can never brick anchoring. The current publisher proposes the next address with `setPublisher(next)` (publisher-only), and the rotation takes effect only when that address calls `acceptPublisher()`. The contract itself is immutable, so there is no upgrade path. Only the publisher address changes.

```solidity
event PublisherTransferStarted(address indexed currentPublisher, address indexed pendingPublisher);
event PublisherChanged(address indexed oldPublisher, address indexed newPublisher);

function setPublisher(address next) external;  // onlyPublisher, proposes the handoff
function acceptPublisher() external;           // called by the pending publisher to complete it
```

### Privacy

Only Merkle roots and hashed `tenantId` values are on-chain. Everything underneath stays off-chain in tenant-prefixed storage and tenant-scoped database rows. Source credentials use the global AES-256-GCM credential key described in `shared/src/crypto/credential-key-provider.ts`.

| On-chain                                 | Off-chain                           |
| ---------------------------------------- | ----------------------------------- |
| `tenantId` (hashed)                      | Tenant raw identifier               |
| `root` (Merkle root)                     | Individual audit events             |
| `eventCount`, `periodStart`, `periodEnd` | Event content, citations, decisions |

### 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>Audit and Proof</strong></td><td>The conceptual model.</td><td><a href="/pages/PIgNXssgtEUZDLnC4b4d">/pages/PIgNXssgtEUZDLnC4b4d</a></td><td></td></tr><tr><td><strong>Audit API</strong></td><td>Retrieve events and proofs over HTTP.</td><td><a href="/pages/BqeKz3FmbRDmRILK0zaA">/pages/BqeKz3FmbRDmRILK0zaA</a></td><td></td></tr></tbody></table>


# BrainPolicyRegistry

`BrainPolicyRegistry` registers policy version hashes per tenant. The policy text and compiled rules live off-chain. Only the canonical hash is on-chain, signed by the tenant.

| Property           | Value                                                                |
| ------------------ | -------------------------------------------------------------------- |
| **Network**        | Base L2                                                              |
| **Solidity**       | 0.8.x                                                                |
| **Pattern**        | Immutable. No upgrade path in MVP; changes ship as audited redeploys |
| **Tenant signing** | EIP-712 `PolicyRegistration`                                         |

### Interface

```solidity
interface IBrainPolicyRegistry {
    event PolicyRegistered(
        bytes32 indexed tenantId,
        uint256 indexed version,
        bytes32 policyHash,
        address[] signers,
        uint256 activatedAt
    );

    function registerPolicy(
        bytes32 tenantId,
        uint256 version,
        bytes32 policyHash,
        address[] calldata signers,      // pre-authorized tenant signers, ascending order
        bytes[]   calldata signatures    // EIP-712 PolicyRegistration, one per signer
    ) external;

    function getPolicy(bytes32 tenantId, uint256 version)
        external view
        returns (bytes32 hash, address[] memory signers, uint256 activatedAt);
}
```

A policy version is registered by one or more EIP-712 signatures from addresses that are already authorized as tenant signers. The registry does not store policy bodies, only the hash, the signer set, and the activation timestamp.

### EIP-712 Type

Tenants sign the canonical hash, not the prose.

```
PolicyRegistration(
  bytes32 tenantId,
  uint256 version,
  bytes32 policyHash
)
```

| Field        | Purpose                                            |
| ------------ | -------------------------------------------------- |
| `tenantId`   | The tenant the policy belongs to                   |
| `version`    | Monotonically increasing version number            |
| `policyHash` | SHA-256 hash of the canonical compiled policy JSON |

### Lifecycle

```
draft → compile → review → sign (EIP-712) → registerPolicy() → active
```

| Phase        | Where                                         |
| ------------ | --------------------------------------------- |
| **Draft**    | Console or API                                |
| **Compile**  | Off-chain Policy compiler                     |
| **Review**   | Tenant reviews compiled JSON plus explanation |
| **Sign**     | Tenant signs `PolicyRegistration`             |
| **Register** | `registerPolicy()` called on Base             |
| **Active**   | Until superseded by a newer version           |

### What Is on-Chain vs Off-Chain

| On-chain            | Off-chain                 |
| ------------------- | ------------------------- |
| `tenantId` (hashed) | Tenant raw identifier     |
| `version`           | Plain-English policy text |
| `policyHash`        | Compiled JSON rules       |
| `activatedAt`       | Compiler explanation      |
| Signer addresses    | Diff between versions     |

{% hint style="info" %}
The policy text is private to the tenant. Only its hash is anchored. A counterparty verifying a policy verdict checks that the verdict references a hash registered on-chain, not the policy text itself.
{% endhint %}

### Policy Lookup

Registered policies are read by explicit version. There is no revocation and no implicit "active" pointer: a policy is superseded when a higher version is registered, and the highest registered version per tenant is tracked in the public `latestVersion` mapping.

```solidity
(bytes32 hash, address[] memory signers, uint256 activatedAt) =
    registry.getPolicy(tenantId, version);

uint256 latest = registry.latestVersion(tenantId);
```

A verifier reads `getPolicy` for the version a verdict references and confirms the on-chain hash matches the compiled policy it was shown.

### Versioning Rules

| Rule                                          | Detail                                        |
| --------------------------------------------- | --------------------------------------------- |
| `version` must increase                       | A version at or below `latestVersion` reverts |
| Each `(tenantId, version)` is write-once      | Re-registering the same version reverts       |
| Signers must be pre-authorized for the tenant | An unknown signer reverts                     |
| Signers supplied in ascending address order   | Enforces uniqueness across the signer set     |

### Privacy

The on-chain footprint is intentionally minimal. The hash commits to the policy without revealing it.

| Mechanism                                        | Effect                                                    |
| ------------------------------------------------ | --------------------------------------------------------- |
| `tenantId` is hashed before storage              | Cross-tenant correlation is hard                          |
| `policyHash` is SHA-256 of compiled JSON         | The structure is hidden                                   |
| Off-chain logic enforces canonical serialization | Two compilations of the same policy produce the same hash |

### 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>Policy and Permissioning</strong></td><td>The conceptual model.</td><td><a href="/pages/GSe2ntE9CLqxoQAiQuzG">/pages/GSe2ntE9CLqxoQAiQuzG</a></td><td></td></tr><tr><td><strong>Policy API</strong></td><td>HTTP reference for policy operations.</td><td><a href="/pages/Syg5AYinEpRFdxOeaGYb">/pages/Syg5AYinEpRFdxOeaGYb</a></td><td></td></tr><tr><td><strong>BrainSmartAccount</strong></td><td>How policy versions are validated on session-key calls.</td><td><a href="/pages/2xFXIKlbOlKKY8V47AgE">/pages/2xFXIKlbOlKKY8V47AgE</a></td><td></td></tr></tbody></table>


# BrainSmartAccount

`BrainSmartAccount` is a per-tenant **session-key smart account**. The tenant's root key owns the account; Brain receives a scoped, spend-capped, revocable **session key**. The owner calls `grantSessionKey` to issue a key, and the session-key holder calls `executeViaSessionKey(nonce, target, value, data)` to dispatch a call. The account enforces every bound on-chain and reverts on anything out of scope.

A session-key call succeeds if and only if **all** of the following hold, checked inside `executeViaSessionKey`:

| Check                                                                                | Mechanism                   |
| ------------------------------------------------------------------------------------ | --------------------------- |
| 1. Caller is the granted holder, and the key is not paused                           | `holder` match + pause flag |
| 2. The call is within the key's validity window (`validAfter`/`validUntil`)          | Per-key timestamps          |
| 3. The supplied nonce equals the holder's current replay nonce                       | Per-holder `nonce(holder)`  |
| 4. `target` is on the key's `allowedTargets` allowlist                               | Per-key target allowlist    |
| 5. The calldata selector is on the key's `allowedSelectors` allowlist                | Per-key selector allowlist  |
| 6. The amount is within the per-tx (`maxPerTx`) and per-window (`maxPerPeriod`) caps | On-chain spend caps         |

The key's `policyVersion` is bound at grant time (a zero value is rejected by `grantSessionKey`), so a stored key always carries the policy digest it was authorized under.

### Implementation

```solidity
contract BrainSmartAccount {
    struct SessionKey {
        address holder;
        uint256 validAfter;
        uint256 validUntil;
        address[] allowedTargets;
        bytes4[] allowedSelectors;
        address capToken;       // address(0) = NATIVE (caps in wei); else ERC20-mode
        uint256 maxPerTx;       // per-call cap in capToken units (or wei in NATIVE mode)
        uint256 maxPerPeriod;   // cumulative cap per periodSeconds window (same units)
        uint256 periodSeconds;  // e.g. 86400 for daily; 0 disables period accounting
        bytes32 policyVersion;  // bound at grant; must be non-zero
    }

    address public owner;             // tenant root key (hardware/custody)
    bytes32 public immutable tenantId;
    address public immutable policyRegistry;

    // Owner-only: issue a scoped, spend-capped, policyVersion-bound key.
    function grantSessionKey(SessionKey calldata key) external onlyOwner;

    // Holder-authenticated: execute within the key's bounds, or revert.
    function executeViaSessionKey(
        uint256 nonceSupplied,
        address target,
        uint256 value,
        bytes calldata data
    ) external returns (bytes memory result);

    // Kill-switch / lifecycle, all owner-only.
    function pauseSessionKey(address holder) external;
    function unpauseSessionKey(address holder) external;
    function revokeSessionKey(address holder) external;
}
```

`executeViaSessionKey` walks the target and selector allowlists, derives the cap-relevant amount according to the key's **cap mode**, enforces the per-tx and per-window caps, checks-effects-interactions the external call, increments the replay nonce, and emits `AgentActionExecuted`. There is no off-chain verdict signature on the call path; the policy decision is made off-chain and reflected in the key's `policyVersion` binding and scope.

### Cap modes

A session key is denominated in exactly one of two modes, set at grant time via `capToken`:

| Mode       | When                     | Caps mean                                          | Constraints                                                                                                                                                                 |
| ---------- | ------------------------ | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **NATIVE** | `capToken == address(0)` | wei of ETH (or the chain's native gas token)       | Caps apply to `msg.value`. A `value == 0` call to a non-token target passes un-metered by design                                                                            |
| **ERC20**  | `capToken != address(0)` | Raw units of `capToken` (USDC=6dp, DAI=18dp, etc.) | `allowedTargets` MUST be exactly `[capToken]`, `allowedSelectors` MUST be a subset of `{transfer, approve, transferFrom}`, `value` MUST be 0. Enforced in `grantSessionKey` |

The ERC20 mode constraints close two finding classes the external audit would otherwise catch:

* a USDC-denominated cap can't be misread against an 18-decimal token (unit-blindness, R-06)
* a session key can't be granted with a non-decodable selector that silently bypasses caps (unmetered call, R-07)
* a token-transfer selector can't be granted in NATIVE mode, where `msg.value == 0` would leave token amounts unmetered

BrainSmartAccount still supports `approve` as a decodable ERC20 selector for general scoped keys. The payment-key issuance script does not grant `approve`; payment keys are limited to `transfer` and `transferFrom`. Revoking or pausing a session key stops future `executeViaSessionKey` calls but cannot claw back any ERC20 allowance that already exists at the token contract.

### EIP-712 ScopeAttestation

The agent's signature includes a tenant-signed scope attestation.

```
ScopeAttestation(
  bytes32 tenantId,
  address agent,
  bytes32 capability,        // e.g. keccak256("pay_invoice")
  uint128 maxAmount,
  bytes32 resourceScope,     // e.g. counterparty allowlist root
  uint64  notBefore,
  uint64  notAfter,
  uint256 nonce
)
```

| Field                   | Purpose                                         |
| ----------------------- | ----------------------------------------------- |
| `capability`            | The action class the agent may take             |
| `maxAmount`             | Per-action ceiling (denominated per token)      |
| `resourceScope`         | Allowlist root (counterparties, accounts, etc.) |
| `notBefore`, `notAfter` | Validity window                                 |
| `nonce`                 | Per-tenant, per-agent replay protection         |

### Policy Binding

The off-chain Policy decision is reflected on-chain by the key's `policyVersion`, fixed when the owner grants the key.

| Property          | Value                                                                 |
| ----------------- | --------------------------------------------------------------------- |
| **Bound at**      | Grant time, in `grantSessionKey`                                      |
| **Bound to**      | The key holder, via the stored `policyVersion` digest                 |
| **Zero allowed?** | No, `grantSessionKey` reverts `PolicyVersionMismatch` on `bytes32(0)` |
| **Anchored in**   | `BrainPolicyRegistry` (the digest the off-chain decision used)        |

{% hint style="warning" %}
A session key carries exactly the `policyVersion` it was granted under. Rotating the active policy means granting a fresh key; the old key keeps its original binding until revoked or expired.
{% endhint %}

### Spend Caps

Per-key caps bound blast radius even within the key's allowlists.

| Field           | Purpose                                                                 |
| --------------- | ----------------------------------------------------------------------- |
| `maxPerTx`      | Maximum value of a single call                                          |
| `maxPerPeriod`  | Maximum cumulative value per `periodSeconds` window                     |
| `periodSeconds` | Window length for the cumulative cap (e.g. `86400` daily; `0` disables) |

`executeViaSessionKey` reverts `ExceedsPerTxCap` or `ExceedsPerPeriodCap` on a call that would breach either ceiling, tracking spend per holder per window.

### Belt-and-Braces Enforcement

Policy is enforced **twice** by design.

| Layer                       | When                      | Catches                                                                   |
| --------------------------- | ------------------------- | ------------------------------------------------------------------------- |
| **Off-chain Policy Engine** | At proposal time          | Most violations, fast feedback, dynamic conditions                        |
| **`BrainSmartAccount`**     | At `executeViaSessionKey` | Anything the off-chain engine missed; protects against backend compromise |

Even if the off-chain backend is compromised, on-chain enforcement rejects any call outside the granted session key's policyVersion-bound scope, allowlists, and spend caps.

### Threat Scenarios

| Scenario                                          | Outcome                                            |
| ------------------------------------------------- | -------------------------------------------------- |
| Holder calls a target outside the allowlist       | Reverts: `TargetNotAllowed`                        |
| Holder calls a selector outside the allowlist     | Reverts: `SelectorNotAllowed`                      |
| Call exceeds the per-tx or per-window cap         | Reverts: `ExceedsPerTxCap` / `ExceedsPerPeriodCap` |
| Call made before/after the key's validity window  | Reverts: `KeyNotActive`                            |
| Call made while the key is paused                 | Reverts: `KeyPaused`                               |
| Replays a session-key call with a consumed nonce  | Reverts: `BadNonce`                                |
| Malicious target re-enters `executeViaSessionKey` | Reverts: `ReentrantCall`                           |
| Owner grants a key with an empty allowlist        | Reverts: `TargetsRequired` / `SelectorsRequired`   |
| Owner grants a key with a zero `policyVersion`    | Reverts: `PolicyVersionMismatch`                   |
| ERC20 selector granted in NATIVE mode             | Reverts: `Erc20SelectorRequiresTokenCap`           |

## Kill-Switch: Pause vs Revoke

| Function                    | Effect                                                                                                                                                                                                           |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pauseSessionKey(holder)`   | Immediately disables execution by this session key **without** deleting its record, window spend, limits, or metadata. So `unpauseSessionKey(holder)` resumes with no fresh attestation. Idempotent, owner-only. |
| `unpauseSessionKey(holder)` | Re-enables execution under the key's existing scope and accumulated window spend.                                                                                                                                |
| `revokeSessionKey(holder)`  | **Permanent** removal. Deletes the key record entirely (and clears any pause flag).                                                                                                                              |

`executeViaSessionKey` reverts with `KeyPaused` while a key is paused. This backs the off-chain `/v1/agents/{id}/halt` and `/v1/payment-intents/{id}/pause` flows.

### Per-Task Minimum-Privilege Keys

A one-time child key is granted per approved PaymentIntent, bounded to the **exact** counterparty (`allowedTargets`), **exact** amount (`maxPerTx == maxPerPeriod`), and a \~10-minute `validUntil`. A compromised worker can spend at most one in-flight intent's authority.

### Session-Key Hardening (pre-audit)

`executeViaSessionKey` carries three defenses closing pre-audit weaknesses:

| Defense                   | Mechanism                                                                                                                                                                            |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Non-empty scope**       | `grantSessionKey` reverts (`TargetsRequired` / `SelectorsRequired`) on an empty target or selector allowlist. An empty list no longer means "any"                                    |
| **Policy bound at grant** | A zero `policyVersion` is rejected at grant (`PolicyVersionMismatch`), so a stored key can never have a missing policy binding                                                       |
| **Replay nonce**          | `executeViaSessionKey(nonceSupplied, target, value, data)` reverts `BadNonce(expected, supplied)` unless `nonceSupplied == nonce(holder)`, then increments. Every call is single-use |
| **Re-entrancy guard**     | A per-holder `_locked` flag is set before the external call and cleared after; a target that calls back in reverts `ReentrantCall`                                                   |

The off-chain rail reads the current `nonce(holder)` and threads it into the call (see the on-chain Base rail). Caps and allowlists are still enforced on every call as before.

### 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>🪪 BrainMCPAgentRegistry</strong></td><td>Where agent identity is checked.</td><td><a href="/pages/7cGQBqLnTUZjyofcuHlm">/pages/7cGQBqLnTUZjyofcuHlm</a></td><td></td></tr><tr><td><strong>📋 BrainPolicyRegistry</strong></td><td>Where the active policy hash is anchored.</td><td><a href="/pages/Qk74oUUATzrLis1xhGcb">/pages/Qk74oUUATzrLis1xhGcb</a></td><td></td></tr><tr><td><strong>🤖 Agents</strong></td><td>The conceptual model.</td><td><a href="/pages/SKNJp6HT7EckCBOhNHMl">/pages/SKNJp6HT7EckCBOhNHMl</a></td><td></td></tr></tbody></table>


# BrainMCPAgentRegistry

`BrainMCPAgentRegistry` registers agents as a compact on-chain record: `agentId`, `agentAddress`, `tenantId`, `scopeHash`, and `behaviorHash`, each registration authorized by an EIP-712 signature from a tenant-allowlisted signer. On-chain reputation is planned, not yet implemented. See RFC 0001.

| Property     | Value                                                                   |
| ------------ | ----------------------------------------------------------------------- |
| **Network**  | Base L2                                                                 |
| **Solidity** | 0.8.x                                                                   |
| **Pattern**  | Immutable. No upgrade path in MVP; changes ship as audited redeploys    |
| **Standard** | EIP-712 signed registration (ERC-8004 reputation planned. See RFC 0001) |

### Interface

This is the **deployed MVP surface**. Each lifecycle call carries an EIP-712 signature from a signer the tenant has allowlisted.

```solidity
contract BrainMCPAgentRegistry {
    struct AgentRegistration {
        bytes32 agentId;
        address agentAddress;
        bytes32 tenantId;
        bytes32 scopeHash;
        bytes32 behaviorHash;   // keccak256(model_id, model_version, prompt_template_hash, tool_manifest_hash)
        uint256 registeredAt;
        uint256 revokedAt;      // 0 while active
    }

    event AgentRegistered(
        bytes32 indexed agentId,
        address indexed agentAddress,
        bytes32 indexed tenantId,
        bytes32 scopeHash,
        bytes32 behaviorHash
    );
    event AgentRevoked(bytes32 indexed agentId, bytes32 indexed tenantId);
    event AgentBehaviorUpdated(bytes32 indexed agentId, bytes32 indexed tenantId, bytes32 behaviorHash);
    event TenantSignerSet(bytes32 indexed tenantId, address indexed signer, bool allowed);

    // A tenant must configure ≥1 allowlisted signer before any agent can be
    // registered for it; the first signer is bootstrapped by initialAdmin.
    function setTenantSigner(
        bytes32 tenantId, address signer, bool allowed,
        address authSigner, bytes calldata signature
    ) external;

    function registerAgent(
        bytes32 agentId, address agentAddress, bytes32 tenantId,
        bytes32 scopeHash, bytes32 behaviorHash, bytes calldata tenantSignature
    ) external;

    function updateBehaviorHash(
        bytes32 agentId, bytes32 behaviorHash, bytes calldata tenantSignature
    ) external;

    function revokeAgent(bytes32 agentId, bytes calldata tenantSignature) external;

    // Views
    function isAuthorized(bytes32 agentId, bytes32 tenantId) external view returns (bool);
    function getAgent(bytes32 agentId) external view returns (AgentRegistration memory);
    function isTenantSigner(bytes32 tenantId, address a) external view returns (bool);
}
```

The fuller ERC-8004 identity record (identity Merkle root, `mcpEndpoint`, capability-hash array) and per-capability scope grants are the **planned** target. See RFC 0001. Reputation is out of scope here and lives in [`BrainReputationRegistry`](/smart-contracts/brainreputationregistry).

### Agent Record

The deployed `AgentRegistration` struct stores exactly these fields:

| Field          | Purpose                                                                                                                |
| -------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `agentId`      | Global agent identifier. The registry's primary key                                                                    |
| `agentAddress` | The agent's on-chain address                                                                                           |
| `tenantId`     | The tenant this registration is bound to                                                                               |
| `scopeHash`    | Hash of the agent's granted scope set; the agent's JWT `scope_hash` must equal this                                    |
| `behaviorHash` | `keccak256(model_id, model_version, prompt_template_hash, tool_manifest_hash)`. Pins model/prompt/tools (§6 check 1.5) |
| `registeredAt` | Block timestamp at registration                                                                                        |
| `revokedAt`    | Block timestamp at revocation; `0` while active (`isAuthorized` reads this)                                            |

The fuller record below is the **planned** target. See RFC 0001. None of these fields exist in the deployed struct today:

| Planned field (RFC 0001) | Purpose                                                                |
| ------------------------ | ---------------------------------------------------------------------- |
| `identityRoot`           | ERC-8004 identity Merkle root (planned. RFC 0001)                      |
| `mcpEndpoint`            | URL where Brain can reach the agent over MCP                           |
| `capabilities[]`         | Hashes of capability identifiers (e.g., `keccak256("pay_invoice")`)    |
| `reputationRoot`         | Reputation pointer. Now a separate contract, `BrainReputationRegistry` |

### Registration

Agents cannot self-register. A registration is authorized by a **tenant-allowlisted signer** who signs this EIP-712 message:

```
AgentRegistration(
  bytes32 agentId,
  address agentAddress,
  bytes32 tenantId,
  bytes32 scopeHash,
  bytes32 behaviorHash
)
```

```solidity
registry.registerAgent(agentId, agentAddress, tenantId, scopeHash, behaviorHash, tenantSignature);
```

The contract recovers the signer, rejects it unless it is on the tenant's allowlist (`isTenantSigner`), stores the record, and emits `AgentRegistered`.

### Per-Tenant Scoping

A tenant must first configure at least one allowlisted signer with `setTenantSigner`. The very first signer for a tenant is bootstrapped by `initialAdmin`, after which signers manage each other. Only an allowlisted signer can register, re-attest, or revoke an agent for that tenant.

Each registration binds the agent to exactly one `tenantId` plus a single `scopeHash` that encodes the whole granted scope set; the agent's JWT `scope_hash` must equal it. The predicate Brain consults before granting a session key is:

```solidity
registry.isAuthorized(agentId, tenantId); // true while registered, not revoked, and tenant matches
```

Finer-grained, per-capability scope grants (`grantScope`/`isScoped`) are the **planned** target. See RFC 0001. The MVP collapses scope into one signed `scopeHash`.

### Deactivation

```solidity
registry.revokeAgent(agentId, tenantSignature);
```

Revocation (signed by a tenant signer) sets `revokedAt` to the current block timestamp. `isAuthorized` then returns false, and Brain refuses to grant or use session keys for the agent. Revocation is permanent for that `agentId`; promoting a new model/prompt/tools instead uses `updateBehaviorHash`.

Behavior updates and revocations are replay-protected. Their EIP-712 payloads include per-agent nonces (`behaviorNonce(agentId)` and `revocationNonce(agentId)`) that increment on accepted updates, so a previously observed signature cannot later roll an agent back to an older behavior hash or replay a stale lifecycle action.

### Reputation lives in a separate contract

Reputation is **not** stored in this registry. It lives in [`BrainReputationRegistry`](/smart-contracts/brainreputationregistry). An *ERC-8004-style* per-agent pointer / Merkle root (RFC 0001, **UNAUDITED testnet**). This registry's deployed `AgentRegistration` struct stores only `agentId`, `agentAddress`, `tenantId`, `scopeHash`, and `behaviorHash`. There is **no** `reputationRoot` field here. Policy reads the reputation pointer as a **tighten-only threshold input**; it is never a money gate or a §6 precondition.

### Discovery

On-chain, the deployed registry answers per-id queries. `getAgent(agentId)` and `isAuthorized(agentId, tenantId)`. Richer discovery (by capability, by reputation standing) is resolved off-chain today; on-chain capability indexing is the planned target. See RFC 0001.

| Query                | Result                                                                                                 |
| -------------------- | ------------------------------------------------------------------------------------------------------ |
| Authorization        | `isAuthorized(agentId, tenantId)`. Registered, not revoked, tenant matches                             |
| Capability filter    | Active agents declaring a capability (resolved off-chain; on-chain index planned. RFC 0001)            |
| Reputation threshold | Resolved via `BrainReputationRegistry` (testnet); Policy maps the pointer to a minimum score off-chain |

{% hint style="success" %}
Discovery is itself audited. Brain logs every selection event so a tenant can later verify why a particular agent was chosen.
{% endhint %}

### ERC-8004 Alignment (RFC 0001)

{% hint style="info" %}
The deployed registry stores identity + scope as `agentId`/`tenantId`/`scopeHash`/`behaviorHash` hashes. The *reputation* half of ERC-8004 alignment is now a separate (RFC 0001, **UNAUDITED testnet**) contract, `BrainReputationRegistry`.
{% endhint %}

| ERC-8004 concept (RFC 0001) | Brain Implementation                                                           |
| --------------------------- | ------------------------------------------------------------------------------ |
| **Identity record**         | `BrainMCPAgentRegistry`. `agentId` / `tenantId` / `scopeHash` / `behaviorHash` |
| **Reputation root**         | `BrainReputationRegistry.scoreRoot` per agent (testnet)                        |
| **Validation records**      | Committed off-chain under the reputation `scoreRoot` (testnet)                 |
| **Discovery**               | View functions across both registries                                          |

## behaviorHash Pinning

`registerAgent` now also takes a `behaviorHash = keccak256(model_id, model_version, prompt_template_hash, tool_manifest_hash)`, emitted on `AgentRegistered` and stored on the registration. This freezes the agent's behavior at a known version. Enterprise security teams get a "the agent cannot silently change its model/prompt/tools" guarantee.

* The §6 gate adds **check 1.5**: the runtime `behaviorHash` must equal the registered value, or the action is rejected regardless of every other signal.
* Promotion to a new behavior requires fresh tenant re-attestation via `updateBehaviorHash(agentId, behaviorHash, tenantSignature)` (EIP-712 signed by a tenant signer) with the current `behaviorNonce(agentId)`. The on-chain analogue of re-signing the ScopeAttestation.

### 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>🤖 Agents</strong></td><td>The conceptual model.</td><td><a href="/pages/SKNJp6HT7EckCBOhNHMl">/pages/SKNJp6HT7EckCBOhNHMl</a></td><td></td></tr><tr><td><strong>🔐 BrainSmartAccount</strong></td><td>How `isAuthorized()` gates session-key grants.</td><td><a href="/pages/2xFXIKlbOlKKY8V47AgE">/pages/2xFXIKlbOlKKY8V47AgE</a></td><td></td></tr><tr><td><strong>🌐 Agents API</strong></td><td>Register and scope agents over HTTP.</td><td><a href="/pages/6zFwU1VB8lTyM59Se4l7">/pages/6zFwU1VB8lTyM59Se4l7</a></td><td></td></tr></tbody></table>


# BrainReputationRegistry

An *ERC-8004-style* on-chain home for **agent reputation** (RFC 0001 §7.7). For each agent it stores a single **reputation pointer**. A `bytes32` Merkle root committing to the agent's off-chain reputation dataset. Versioned by a monotonically increasing `epoch`. The chain holds the pointer **only**: no raw history, no score, no PII.

{% hint style="warning" %}
**UNAUDITED. Base Sepolia testnet only.** A pre-audit reference implementation. **Non-custodial** (no funds, no value path), so an unaudited deploy risks no money. But it is batched into the external audit and stays testnet-only until that clears. Immutable: no admin, no upgrade, no pause.
{% endhint %}

### What it is. And isn't

| It is                                                  | It is **not**                                        |
| ------------------------------------------------------ | ---------------------------------------------------- |
| A per-agent `bytes32` reputation pointer (Merkle root) | A store of raw feedback / scores / history           |
| A **Policy threshold input** (read off-chain)          | A money gate or a §6 pre-execution-gate precondition |
| Attestor-written, monotonic, tamper-evident            | A contract that holds or moves any funds             |

### Data model (hash-only, RFC 0001 §3)

| Field       | Type      | Notes                                                      |
| ----------- | --------- | ---------------------------------------------------------- |
| `scoreRoot` | `bytes32` | Merkle root committing to the off-chain reputation dataset |
| `epoch`     | `uint64`  | Monotonic version; strictly increases on each publish      |
| `updatedAt` | `uint64`  | Unix seconds of the latest publication                     |

The ABI is `bytes32` / `address` / `uint` only. No `string`, no PII (enforced by `scripts/check-no-onchain-pii.mjs`).

### How it's used

```
attestor ──publishReputation(agentId, scoreRoot, epoch)──►  BrainReputationRegistry
                                                                   │
Policy ◄── reputationOf(agentId) ──────────────────────────────────┘
   │  derives a score off-chain from the dataset the root commits to
   ▼
tighten-only adjustment (more approvers / lower cap). NEVER loosens, NEVER a §6 gate
```

* **Attestor**. Brain's reputation oracle (a Safe multi-sig in production) is the only writer, rotatable only by itself. It has **no fund-moving power**: a compromised attestor can at worst publish a bad pointer, which. Via Policy's tighten-only rule. Can only make payments *stricter*, never authorize one.
* **Anti-replay**. Each publish must strictly increase the agent's `epoch`; a stale or equal epoch reverts, so an old pointer can never overwrite a newer one.
* **Policy input only**. Reputation may *raise or lower a policy threshold* but is never the precondition itself. The §6 gate never sees a reputation value (LLM/reputation judgment never replaces a deterministic gate check).

{% hint style="info" %}
`reputationOf` is a public read, so other Base-ecosystem participants can fetch an agent's reputation pointer. The cross-ecosystem interop surface ERC-8004 envisions (RFC 0001 §7.7). Without exposing Brain's private reputation data.
{% endhint %}

### 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>🪪 BrainMCPAgentRegistry</strong></td><td>Agent identity + scope attestation.</td><td><a href="/pages/7cGQBqLnTUZjyofcuHlm">/pages/7cGQBqLnTUZjyofcuHlm</a></td><td></td></tr><tr><td><strong>📋 Policy</strong></td><td>How thresholds (incl. reputation) are evaluated.</td><td><a href="/pages/pKaCvQpKvk0xKd6TcXzR">/pages/pKaCvQpKvk0xKd6TcXzR</a></td><td></td></tr><tr><td><strong>📦 Escrow and X402</strong></td><td>The settlement contracts.</td><td><a href="/pages/pdclcSITK44863badHKU">/pages/pdclcSITK44863badHKU</a></td><td></td></tr></tbody></table>


# Escrow and X402

For agent-to-agent (M2M) commerce where a payment must be **conditioned on job completion**, Brain provides `BrainEscrow`. A custodial USDC escrow on Base. For per-call API access, Brain integrates **x402**. Both terminate in the same `PaymentIntent → §6 gate → audit` flow; neither is a second money path.

{% hint style="warning" %}
**`BrainEscrow` is UNAUDITED and Base Sepolia testnet only.** It is a pre-audit reference implementation (RFC 0001 §7.6). Immutable (no admin, no upgrade, no pause), and it must clear an external security audit before any mainnet address is funded.

The api enforces this in code: at boot, if `BRAIN_ESCROW_ADDRESS` is set on any chain outside the explicit testnet allowlist, the api refuses to start unless **both** of the following hold. (1) The committed audit record `contracts/audit-status.json` has status `approved` for that chain. This is the reviewed source of truth, and `scripts/check-audit-status.mjs` will not let it be marked `approved` without an auditor, the audited commit, a report reference, and zero unresolved critical/high findings. (2) An explicit operator attestation in the environment: `BRAIN_ESCROW_AUDIT_RECEIPT="<url/filepath/hash pointing at the audit report>"` (preferred. Carries the diligence metadata) or the legacy `BRAIN_ESCROW_AUDIT_APPROVED="true"` bare-boolean. Requiring both means a bare env flag can no longer bypass a pending audit. The api also checks explicit `BASE_RPC_URL` with `eth_chainId` against `BRAIN_BASE_CHAIN_ID` before wiring EVM rails. Either audit signal must only be set after the external audit signs off and the deployed bytecode is verified to match the audited bytecode. See `contracts/audit-status.json` and `services/api/src/composition/escrow-audit-gate.ts`.
{% endhint %}

| Mechanism       | Use case                                                                |
| --------------- | ----------------------------------------------------------------------- |
| **BrainEscrow** | Job-style work where USDC releases incrementally as milestones complete |
| **x402**        | Per-call API access where payment settles inline with each HTTP request |

### BrainEscrow. Custodial, hash-only, incremental

A payer locks USDC **into the contract** against a `jobTermsHash` (a keccak256 commitment of the off-chain terms. No PII, RFC 0001 §3). Funds then **release** to the payee or **refund** to the payer. Settlement is **incremental**: `release(amount)` and `refund(amount)` each move a partial sum, supporting **milestone payments** and **arbiter dispute-splits**. The escrow stays `Locked` until `released + refunded` reaches the full amount, then becomes `Settled`.

```
lock(escrowId, payee, USDC, amount, jobTermsHash, deadline)   ← payer deposits USDC
        │
        ▼   release(amount)  (payer confirms delivery, or arbiter attests)
   Locked ───────────────────────────────────────────────►  pays the payee
        │   refund(amount)   (arbiter any time, or payer after deadline)
        └───────────────────────────────────────────────►  returns to the payer
        │
        ▼  when released + refunded == amount
     Settled (terminal)
```

| Action      | Who                                                                                                                       |
| ----------- | ------------------------------------------------------------------------------------------------------------------------- |
| **lock**    | The payer (buyer/agent). Deposits USDC against the job commitment                                                         |
| **release** | The payer (confirming delivery, incl. per-milestone) **or** the arbiter (attesting / resolving a dispute). Pays the payee |
| **refund**  | The **arbiter** any time (dispute), **or** the payer once the `deadline` passes (job not delivered). Returns to the payer |

{% hint style="info" %}
**The contract custodies the USDC; Brain (the operator) cannot redirect it.** The `arbiter` is immutable (a Safe multi-sig in production) and can only ever **release to the designated payee** or **refund to the designated payer**. Never to an arbitrary address. There is no admin/drain path. A dispute is resolved by a partial release to the payee plus a partial refund to the payer on the same lock.
{% endhint %}

### Gate binding (§6 check 6.6)

Before a release is gated through, the §6 pre-execution gate reads `getEscrow(escrowId)` and binds the PaymentIntent to the on-chain lock: still `Locked`, enough **remaining** balance (`amount − released − refunded`) to cover this release, same payee, same `jobTermsHash`. Binding against `remaining` (not the full `amount`) is what lets each milestone after the first through.

### X402 Machine-Native Payments

For HTTP-native settlement, Brain integrates **x402**: the resource server returns `402 Payment Required` with payment instructions; the calling agent retries with an x402 payment header backed by the tenant's smart account; settlement and audit happen in the same flow.

{% hint style="warning" %}
x402 is outside the `BrainEscrow` audit fence because it does not call the escrow contract. It is still a production money rail and is boot-fenced as a live rail. `x402_settle` may execute without per-action human approval only when the matched signed policy rule explicitly sets `onchain_settlement_permitted: true` and an `x402_autonomous_max_amount` cap that covers the amount. Missing, malformed, wrong-currency, or over-cap policy data fails closed to the human approval path.
{% endhint %}

```
Agent                      Resource Server
  │                              │
  │  GET /api/data               │
  │ ──────────────────────────►  │
  │                              │
  │  402 Payment Required        │
  │  X-Payment-Required: ...     │
  │ ◄──────────────────────────  │
  │                              │
  │  GET /api/data               │
  │  X-Payment: ...              │
  │ ──────────────────────────►  │
  │                              │
  │  200 OK + result             │
  │ ◄──────────────────────────  │
```

| Step                     | Detail                                                                                  |
| ------------------------ | --------------------------------------------------------------------------------------- |
| **Initial request**      | Agent calls a resource without payment                                                  |
| **402 response**         | Server returns required amount, recipient address, currency                             |
| **Retry with X-Payment** | Agent attaches a payment authorization signed against its smart account                 |
| **Validate and settle**  | Server validates the payment (or accepts a verifiable promise) and returns the resource |
| **Audit**                | The full flow is logged as an audit event linked to the action                          |

### When to Use Which

| Scenario                                | Mechanism                                  |
| --------------------------------------- | ------------------------------------------ |
| Agent paid on completion of a job       | BrainEscrow                                |
| Agent pays per-call for an API or tool  | x402                                       |
| Agent pays another agent for a sub-task | x402 (immediate) or BrainEscrow (deferred) |

### Where Settlement Is **Not** Brain's Job

| Boundary                          | Who Handles                                    |
| --------------------------------- | ---------------------------------------------- |
| Immediate (x402) funds in transit | The tenant's smart account / rail; not Brain   |
| Final settlement                  | The settlement layer (Base, an off-chain rail) |
| Tax and reporting                 | Tenant and tenant's accounting tooling         |

Brain (the operator) never holds or redirects funds. For *conditional* settlement the immutable `BrainEscrow` contract escrows USDC. But it can only ever release to the designated payee or refund the designated payer; there is no path for Brain to redirect it.

### 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>🔐 BrainSmartAccount</strong></td><td>The smart account that locks and releases.</td><td><a href="/pages/2xFXIKlbOlKKY8V47AgE">/pages/2xFXIKlbOlKKY8V47AgE</a></td><td></td></tr><tr><td><strong>🏅 BrainReputationRegistry</strong></td><td>How agent reputation is referenced on-chain.</td><td><a href="/pages/0TL0aQXn4l3eiEqu51uS">/pages/0TL0aQXn4l3eiEqu51uS</a></td><td></td></tr><tr><td><strong>🤖 Agents</strong></td><td>The conceptual model.</td><td><a href="/pages/SKNJp6HT7EckCBOhNHMl">/pages/SKNJp6HT7EckCBOhNHMl</a></td><td></td></tr></tbody></table>


# Errors

Every error Brain returns uses a single envelope: a stable `snake_case` `code`, a human-readable `message`, optional structured `details`, a `request_id` for cross-system correlation, and a `docs_url`. Codes are stable forever once shipped (format: `{domain}_{condition}`). The full registry lives in `shared/src/errors.ts`.

{% hint style="warning" %}
Brain **never** returns HTTP 200 with an error in the body. A non-2xx status always carries this envelope.
{% endhint %}

### Shape

```json
{
  "error": {
    "code": "policy_denied",
    "message": "Counterparty not in the approved allowlist",
    "details": { "counterparty_id": "cp_x", "policy_version": 3 },
    "request_id": "req_8f3a92...",
    "docs_url": "https://docs.brain.fi/resources/errors#policy_denied"
  }
}
```

In the SDK:

```typescript
try {
  await brain.pay("acme", { invoiceId: "inv_8231" });
} catch (err) {
  if (err instanceof BrainError) {
    console.log(err.code, err.message, err.requestId);
  }
}
```

### Auth (401) and Authorization (403)

| Code                       | Meaning                                                                               | Fix                                              |
| -------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------ |
| `auth_token_missing`       | No bearer token on a protected route                                                  | Send `Authorization: Bearer <token>`             |
| `auth_token_invalid`       | Token malformed or signature failed                                                   | Re-issue the token                               |
| `auth_token_expired`       | Access token past its expiry                                                          | Log in / re-sign and retry                       |
| `auth_invalid_key`         | API key malformed, revoked, or for the wrong environment                              | Check `.env`; sandbox and production keys differ |
| `auth_invalid_credentials` | Email/password login failed (also returned for an unknown email. No user enumeration) | Check the credentials                            |
| `auth_email_unverified`    | The owner's email has not been verified                                               | Complete `POST /v1/auth/verify-email`            |
| `auth_siwx_invalid`        | SIWX signature did not verify                                                         | Re-sign with the registered key                  |
| `auth_scope_insufficient`  | Token lacks the required scope                                                        | Re-issue with the right scope                    |

### Self-serve onboarding

| Code                    | Meaning                                                         | Fix                                     |
| ----------------------- | --------------------------------------------------------------- | --------------------------------------- |
| `signup_email_taken`    | An account with this email already exists (409)                 | Log in instead, or use another email    |
| `signup_token_invalid`  | The email-verification token is invalid, expired, or used (400) | Request a new verification token        |
| `wallet_already_linked` | The wallet is already linked to an account (409)                | Use a different wallet, or unlink first |

### Tenant

| Code                   | Meaning                                           | Fix                                                |
| ---------------------- | ------------------------------------------------- | -------------------------------------------------- |
| `tenant_not_found`     | The `tenant_id` doesn't exist in this environment | Verify the id; sandbox and production are separate |
| `tenant_suspended`     | The tenant is suspended (403)                     | Contact support                                    |
| `tenant_access_denied` | Authenticated, but not for this tenant (403)      | Check the token's tenant                           |

### Source and Raw

| Code                            | Meaning                                                                                                                                                                                                                                                         | Fix                                                                    |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `source_not_found`              | Source id doesn't match a connected source                                                                                                                                                                                                                      | List sources to find the right id                                      |
| `source_credential_invalid`     | Upstream-source credentials were rejected (401)                                                                                                                                                                                                                 | Reconnect the source                                                   |
| `raw_artifact_not_found`        | No raw artifact for that id (404)                                                                                                                                                                                                                               | Check the id                                                           |
| `raw_source_unsupported`        | Unknown `source_type` (400)                                                                                                                                                                                                                                     | Use a supported source type                                            |
| `raw_source_reserved`           | A high-trust provider `source_type` (`plaid`/`stripe`) was sent to the generic `/raw/ingest` route (403). Those may only arrive via the authenticated `/raw/webhooks/{provider}` path, so a generic caller can't mint high-trust evidence by choosing the label | Ingest via the provider webhook, or use `upload` / `agent_contributed` |
| `raw_webhook_signature_invalid` | A provider webhook's HMAC didn't verify (401)                                                                                                                                                                                                                   | Check the signing secret                                               |

### Policy

Policy **decisions** are `allow` / `confirm` / `reject` (returned on the decision, not as errors). These codes are the error conditions:

| Code                       | Meaning                                                | Fix                                    |
| -------------------------- | ------------------------------------------------------ | -------------------------------------- |
| `policy_not_found`         | No policy for the tenant (404)                         | Create + activate a policy             |
| `policy_not_active`        | The tenant has no active policy version (409)          | Activate a policy version              |
| `policy_denied`            | The action violates the active policy (422)            | Read `details` for which rule fired    |
| `policy_quorum_not_met`    | Required approver quorum not reached (409)             | Collect the remaining approvals        |
| `policy_version_mismatch`  | The decision was for a superseded policy version (409) | Re-evaluate against the active version |
| `policy_signature_invalid` | The signed policy attestation didn't verify (401)      | Re-sign the policy                     |

### Agent and scope

| Code                        | Meaning                                                | Fix                                          |
| --------------------------- | ------------------------------------------------------ | -------------------------------------------- |
| `agent_not_found`           | Agent id doesn't match a registered agent (404)        | List agents                                  |
| `agent_not_registered`      | Agent has no on-chain registration (401)               | Register in `BrainMCPAgentRegistry`          |
| `agent_inactive`            | Agent record is not `active` (409)                     | Reactivate / re-register                     |
| `agent_scope_hash_mismatch` | JWT `scope_hash` ≠ the on-chain hash (401)             | Re-sign with current scope (or it's revoked) |
| `scope_expired`             | The scope grant's window has passed (403)              | Renew the grant                              |
| `agent_proposal_duplicate`  | This run already produced an equivalent proposal (409) | Reuse the existing proposal                  |

### PaymentIntent / Action

| Code                           | Meaning                                                                                                                        | Fix                                                                                    |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
| `payment_intent_not_found`     | No PaymentIntent for that id (404)                                                                                             | Check the id                                                                           |
| `payment_intent_invalid_state` | The intent isn't in a state that allows this op (409)                                                                          | Read its current `status`                                                              |
| `payment_intent_gate_failed`   | The §6 pre-execution gate rejected the intent (409)                                                                            | Read `details` for the failed check                                                    |
| `obligation_not_found`         | The linked `obligation_id` doesn't resolve in the Ledger (404)                                                                 | Reference an existing obligation                                                       |
| `obligation_direction_invalid` | A new obligation-linked intent must target a known `payable` obligation; its direction is `null`/unknown or `receivable` (422) | Pay a payable (vendor) obligation; classify the counterparty so its direction resolves |
| `action_already_executed`      | Already settled (409)                                                                                                          | No retry needed                                                                        |
| `idempotency_key_reused`       | Same idempotency key, different request body (409)                                                                             | Generate a new key                                                                     |

### Pre-execution gate failures

When the §6 gate rejects an intent it surfaces as `payment_intent_gate_failed`, with `details` naming the failed check (e.g. behavior-hash pinned 1.5, balance, counterparty, approval, escrow-state binding 6.6). Standalone gate codes:

| Code                           | Meaning                                                |
| ------------------------------ | ------------------------------------------------------ |
| `gate_no_policy_decision`      | No PolicyDecision linked to the intent                 |
| `gate_policy_version_stale`    | The active policy superseded the one Policy evaluated  |
| `gate_counterparty_unverified` | Counterparty `verified_status` doesn't meet the policy |
| `gate_counterparty_sanctioned` | Counterparty is sanctioned per latest screening        |
| `gate_balance_insufficient`    | Source balance < amount at gate time                   |
| `gate_approval_incomplete`     | Required approver signatures missing or invalid        |
| `gate_session_key_invalid`     | On-chain session key expired or out-of-scope           |
| `gate_audit_chain_stale`       | Audit anchor too stale for the configured threshold    |

[**→ The pre-execution gate**](/protocol/the-pre-execution-gate)

### Validation (400)

| Code                     | Meaning                                                    |
| ------------------------ | ---------------------------------------------------------- |
| `request_body_invalid`   | Request body failed validation; `details` lists the issues |
| `request_params_invalid` | Path/query params failed validation                        |
| `validation_failed`      | Generic schema validation failure                          |
| `missing_required_field` | A required field is absent                                 |
| `invalid_cursor`         | The pagination cursor is malformed or expired              |

### Conflict (409)

| Code                             | Meaning                                                                                                      |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `tenant_identity_already_linked` | The platform identity is already linked to a tenant; use the returned `error.details.tenant_id` to reattach. |

### Rate limiting and server

| Code               | Status | Meaning                                                          |
| ------------------ | ------ | ---------------------------------------------------------------- |
| `rate_limited`     | 429    | Per-minute limit for your tier; honour the `Retry-After` header  |
| `internal_error`   | 500    | Unexpected error; retry, and include the `request_id` in support |
| `upstream_timeout` | 504    | A downstream source timed out; retry with backoff                |
| `maintenance_mode` | 503    | Scheduled maintenance                                            |

### MCP-Specific JSON-RPC Codes

The MCP surface (`POST /v1/agents/mcp`) returns JSON-RPC error codes:

| Code     | Meaning                                                                                              |
| -------- | ---------------------------------------------------------------------------------------------------- |
| `-32001` | Auth token missing, invalid, or expired (covers `auth_token_missing/invalid/expired`)                |
| `-32002` | Scope insufficient. Also tenant mismatch (covers `auth_scope_insufficient` / `auth_tenant_mismatch`) |
| `-32003` | Agent not registered or inactive (`agent_not_registered`)                                            |
| `-32004` | Pre-execution gate failed. Covers every `gate_*` sub-code (`payment_intent_gate_failed`)             |
| `-32005` | Agent `scope_hash` does not match on-chain registration (`agent_scope_hash_mismatch`)                |
| `-32600` | Invalid request (standard JSON-RPC)                                                                  |
| `-32601` | Method not found                                                                                     |
| `-32602` | Invalid params                                                                                       |
| `-32603` | Internal error                                                                                       |

### Getting Help

Include the `request_id` from the error envelope when contacting support. Brain can resolve the exact request from it.


# Changelog

User-visible changes to the Brain protocol, HTTP API, MCP surface, and SDK. Internal refactors, performance work, and bug fixes that don't change behaviour are omitted unless they affect integrators.

### v0.5.18 (obligation scenario filtering)

* **`GET /v1/ledger/obligations` now supports `scenario=ap|ar`.** The filter is validated and applied in the Ledger query rather than silently ignored. When direction is omitted, `scenario=ar` selects receivable rows and `scenario=ap` selects payable rows.
* **Invoices are documented as the complete receivables inventory.** The receivable-obligation filter is limited to rows that have an obligation projection and is not interchangeable with the invoice list.

### v0.5.17 (production tenancy identity conflicts)

* **Production tenant creation now handles an already-linked platform identity.** `POST /v1/tenants` and `POST /v1/orgs/{orgId}/tenants` return `409` `tenant_identity_already_linked` with the existing tenant id instead of surfacing a PostgreSQL unique-constraint error. Clients can reattach through the documented session and agent-token routes.

### v0.5.16 (explicit accounts receivable classification)

* **AR-sourced Ledger invoices and obligations now carry `metadata.scenario: "ar"`.** Consumers can identify receivables positively instead of classifying every non-AP row as AR.
* **Ledger obligation metadata is now part of the public response contract.** `GET /v1/ledger/obligations` returns each row's structured metadata, matching invoice responses.
* **Invoice and obligation pagination is documented.** Both list endpoints return an opaque `next_cursor`; receivables can additionally be selected with `GET /v1/ledger/obligations?direction=receivable`.

### v0.5.15 (tenant-aware Wiki question suggestions)

* **`GET /v1/wiki/suggested-questions` returns eligible deterministic questions for the calling tenant.** Suggestions derive from the same intent registry as deterministic Wiki question execution, so unavailable or generative-only questions are never suggested.
* **Suggestions are ranked by tenant-local deterministic question use.** The `usage_rank_score` is an all-time invocation count scoped by tenant and is never shared across tenants.
* **`GET /v1/assistant/questions` remains a separate persisted-record feed.** It does not evaluate deterministic eligibility; clients needing suggestions must call `GET /v1/wiki/suggested-questions`.

### v0.5.14 (grounded Wiki listing answers)

* **Bounded transaction, cash-flow, and invoice listings use deterministic Ledger queries.** Questions such as `Show last 10 transactions`, `Show recent cash flow`, and `List this month's invoices` return the matching records and cited Ledger evidence without generative synthesis.
* **Invoice records are now a Wiki-question evidence type.** Listing responses can cite the Ledger invoice rows they return.

### v0.5.13 (grounded Wiki aggregation answers)

* **Wiki question answers now carry `answered`.** This boolean is `true` only for grounded or deterministic answers. Clients no longer need to infer a refusal from prose.
* **Transaction counts, totals, and averages use a deterministic Ledger query when the transaction scope is unambiguous.** Month-scoped and direction-scoped questions return the exact computed value and matching transaction evidence without relying on an LLM to perform arithmetic.

### v0.5.12 (counterparty trust contract correction)

Documentation and integration-contract correction. No route, service, gate, or authentication behavior changed.

* **BrainMVB's API surface now includes all four mounted counterparty trust transitions.** Grant, pause, restore, and acknowledge require a user bearer JWT with `ledger:write`; platform shared-secret callers and API keys cannot call them.
* **Counterparty trust terminology now matches the live state machine.** The state vocabulary is `unreviewed`, `trusted`, `paused`, and `acknowledged`.
* **Trust-state docs now state the current enforcement boundary.** Trust state is advisory only. Payment execution is enforced through `verified_status`, sanctions screening, and policy checks.

### v0.5.11 (production launch documentation reconciliation)

Documentation-only correction. No API or contract behavior changed.

* **Audit proof documentation now matches `BrainAuditAnchor`.** Verification uses `isPublished(tenantId, root)` and `verifyInclusion(root, leaf, proof)`. `latestAnchor` returns the current tenant root. The contract has no batch index or `rootAt` method.
* **Batch publication is documented.** `anchorBatch` publishes up to 50 tenant roots in one Base Sepolia transaction and skips already-published pairs on a retry. Single-root `anchor` still rejects a duplicate pair.
* **Launch network and publisher statements are corrected.** Brain's APIs are production available. The anchor contract is unaudited, runs on Base Sepolia, has no Base mainnet deployment, and currently uses a single EOA publisher with two-step rotation.
* **API-key availability is stated per environment.** At launch, first-class `brain_sk_` authentication is enabled for sandbox integration and disabled on the production API. The only issuable scopes are `ledger:read`, `audit:read`, and `governance:read`.

### v0.5.10 (proposal read model expansion)

Backward-compatible API read-model expansion. No new route or API version was introduced because existing compact fields remain in place and all new fields are additive.

#### Changed. Proposals and evidence

* **`GET /v1/proposals` and `GET /v1/proposals/{id}` now return richer proposal cards.** In addition to the compact fields, each proposal includes `stored_action_type`, `details`, `policy`, `presentation`, and `available_decisions`. `presentation.technical_detail` uses stable six-layer keys: `1_ingest`, `2_extract`, `3_classify`, `4_score`, `5_policy`, and `6_propose`.
* **Public proposal types are now complete across first-party agents and payment intents.** The full public set is `vendor_risk`, `payment`, `collections`, `treasury`, `cash_forecast`, `dispute`, `compliance`, `revenue_intel`, `reconciliation`, `subscription`, `fraud_anomaly`, `personal_budget`, `financial_health`, `purchase_advisor`, `tax_prep`, `travel_finance`, `bill_management`, `debt_optimization`, and `savings`.
* **Stored action names map deterministically to public proposal types.** Direct public types win first, then agent role or kind, then the explicit stored-action map. Examples: `flag_transaction -> fraud_anomaly`, `block_payment -> vendor_risk`, `propose_match -> reconciliation`, `recommend_card -> travel_finance`, `tag_tax_item -> tax_prep`, `remind -> bill_management`, and `recommend_savings_transfer -> savings`. Ambiguous names such as `notify`, `escalate`, `create_task`, and `recommend_action` resolve through agent role or kind.
* **High-risk unmatched non-money actions surface for review.** Unmatched `collections`, `fraud_anomaly`, and `vendor_risk` `agent_action` proposals now fall back to `confirm` with a signer requirement. Low-stakes proposal types and `payment` remain fail-closed when no policy rule matches.

### v0.5.9 (surface onboarding admin auth)

* Surface onboarding endpoints for Slack OAuth install, Teams install and revoke, and email recipients, routes, and domains now require a Brain bearer JWT with `surfaces:admin`. The gateway derives the tenant from the principal instead of trusting tenant ids in request bodies.
* Email custom sender domains are verified by Brain-side DNS checks for SPF, DKIM, and DMARC before activation. A reverify endpoint can refresh the result.
* Slack OAuth install state is signed with `SLACK_INSTALL_STATE_SECRET` instead of reusing the OAuth client secret.

### v0.5.8 (surface approval audit ordering + OpenAPI cleanup)

Safety and contract-polish release. No endpoint behavior is loosened.

#### Fixed. Surface approval ordering

* **Surface approval signatures now happen after the decision audit record.** Slack, Teams, and email approval flows re-check policy first, write the audit record, then record the approval signature that contributes to quorum. This keeps quorum-changing approval writes from preceding the audit evidence for the same human decision.
* **Dual approval still records the first approver.** The post-audit approval hook runs for both awaiting-second-approval and terminal approvals, so quorum can build without moving signing into the execution handoff.
* **Approver role checks are stricter.** A roleless actor no longer satisfies a `signer` sentinel, and disabled users no longer count as active approvers.

#### Changed. OpenAPI contract quality

* **OpenAPI lint now runs clean with zero warnings.** The contract now includes proprietary license metadata, explicit documented error responses for operations that only listed success responses, and regenerated SDK types.
* **Intentional route-shape exceptions are documented in Redocly config.** The implemented Fastify routes retain their existing paths, and the disabled legacy `POST /v1/execution/execute` route remains documented as returning `422` rather than a fake success response.

### v0.5.7 (money-path reservation lifecycle)

Safety hardening for the PaymentIntent execution path. No API, MCP, or SDK surface changed.

#### Fixed. Reservation-backed execution handoff

* **Balance reservations now have a live lifecycle.** For ledger-account-backed payments, `execute()` locks the source account, locks the latest balance snapshot, rechecks available balance net of active reservations, then creates a reservation in the same transaction that moves the PaymentIntent from `approved` to `dispatching` and enqueues the durable outbox row. The outbox row stores `reservation_id`; settlement consumes the reservation, and deterministic rail failure releases it. Check #8 already subtracted active reservations, and the locked recheck makes the handoff race-free.
* **Outbox and PaymentIntent state races fail closed.** Settlement now verifies that `dispatching -> executed` actually updated the PaymentIntent before appending the execution receipt, consuming the reservation, or recording spend. Deterministic failure similarly verifies `dispatching -> failed` before releasing a reservation. A lost race routes the worker to retry/reconcile instead of producing a mismatched outbox/PaymentIntent state.
* **Outbox idempotency fallback is tenant-scoped.** The conflict lookup now selects by both `tenant_id` and `idempotency_key`, matching the unique index and preserving correctness outside strict RLS test environments.
* **Readiness evidence is profile-gated.** `production-readiness --profile` now treats evidence strength as a release gate, not only a display field. Staging requires exercised core safety rows such as Base Sepolia on-chain E2E; mainnet requires exercised money-path, rail, audit, and contract evidence. A new `pnpm run readiness:evidence -- --profile staging` command emits a diligence-ready report with row status, evidence state, blockers, and known limitations.

#### Changed. Boot composition

* Production DB role expectations moved out of `services/api/src/main.ts` into a focused composition module. Runtime behavior is unchanged; the boot binary is smaller and the role matrix is easier to review.

### v0.5.6 (docs-accuracy remediation + error `docs_url`)

Documentation-accuracy pass across the published docs, reconciling them to the code as the source of truth: the §6 gate-check count (13 numbered + 10 hardening = 23), the MCP scope error code (`-32002`, not `-32004`), the route migration table (`/execution/*` propose/register stay live; `/agents/{id}/propose` and `/agents/register` 404), the MCP surface size (12 tools / 7 resources / 5 prompts), the self-serve signup path (`POST /v1/signup`), the credential model (`brain_sk_` is the bearer service token), the decision/status vocabularies (`allow|confirm|reject` and their SDK aliases), the webhook event catalog (`payment_intent.*`), and the deployed Base Sepolia contract addresses. No API / MCP / SDK behavior changed except the item below.

#### Changed. Error envelope `docs_url`

* **`error.docs_url` now points at the published error reference.** It previously emitted `https://docs.brain.fi/errors/{code}`, a path with no page (every self-help link 404'd). It now emits `https://docs.brain.fi/resources/errors#{code}`, which lands on the canonical error registry.

### v0.5.5 (GDPR erasure hardening)

Second-pass review remediation of the tenant blob-purge workflow (RFC 0003). Compliance- and diligence-facing; the internal worker/test detail is omitted.

#### Changed. Tenant erasure is now crash-safe and audit-atomic

* **A crashed purge worker can no longer silently strand a GDPR Article 17 erasure.** A job claimed by a worker that then dies (left in `purging`) is now reclaimed by another worker once its lease expires, and every status write is fenced on a unique lock token so a resurrected stale worker cannot overwrite the new owner's result.
* **A transient cloud error no longer masquerades as a permanent legal hold.** Per-object delete failures are classified: throttling / 5xx / network / authorization errors are retried, and only a confirmed WORM / object-lock response makes the job terminal (`blocked_legal_hold`). Previously any failed delete was treated as a legal hold, so a 503 could permanently stop an erasure.
* **Purge-lifecycle audit events are now written transactionally.** Each state transition records its audit intent in the same database transaction as the status change (via an outbox), and a publisher delivers it to the audit service idempotently. This removes the prior ordering where an audit event could be emitted before the state write (orphaning a "completed" record) and the `audit-emit-failed` sentinel that let a job complete with no real audit event. A tenant deletion now returns a truthful committed result even if the audit service is momentarily unavailable.

### v0.5.4 (audit-build binding, GDPR erasure, money-path CI)

Review-remediation batch. The internal CI/test work is omitted; the items below are API-, compliance-, or diligence-facing.

#### Fixed. Proof API

* **`GET /v1/proof/{action_id}` no longer 500s for an in-flight payment.** A PaymentIntent that is executed but still `dispatching` (settlement async, not yet anchored) now returns a **200 partial proof** (gate checks present, `merkle_root` empty until anchored) instead of an internal error. Root cause was a never-run-on-Postgres evidence query referencing non-existent columns. `policy_hash` is now hex-encoded (was a raw byte buffer).

#### Added. GDPR Article 17 erasure (RFC 0003)

* **Tenant deletion now durably and permanently erases Raw blob bytes.** `DELETE /v1/tenants/{id}` enqueues a `tenant_blob_purge_jobs` row in the deletion transaction; a privileged worker drains it via `BlobAdapter.purgeTenant` with bounded retries, a dead-letter state, legal-hold surfacing, and per-lifecycle audit events (`tenant_blob.purge_requested` / `_completed` / `_blocked_legal_hold` / `_retried` / `_exhausted`). `purgeTenant` does version-aware deletion (S3 every object version + delete marker; Azure versions + snapshots), so erasure is permanent in a versioned bucket, closing the gap where a "deleted" response left the user's PII recoverable.

#### Changed. Mainnet escrow audit binding (diligence)

* **Audit approval binds to the audited build, not just a commit.** `contracts/audit-status.json` approval now additionally requires the compiler settings, contract-source-tree hash, and creation/runtime bytecode hashes, plus an explicit `approved_chain_ids`. A CI step recomputes them from the working tree + Foundry artifact and fails the build on drift; the mainnet escrow boot fence also requires the booting chain to be in `approved_chain_ids`. A single shared validator now backs the runtime fence, the CI guard, and the readiness command (previously the runtime path was a bare `status === "approved"` check).
* **The mainnet escrow boot fence now also verifies the DEPLOYED on-chain bytecode.** With an escrow address configured on Base mainnet, the api reads the live contract code via `eth_getCode` and refuses to boot unless it matches the audited runtime bytecode. Because Solidity writes `immutable` values (the escrow arbiter) into the deployed code at construction, the audited hash and the on-chain code are compared with those immutable byte ranges masked; an approved `contracts/audit-status.json` now carries the masked `runtime_bytecode_sha256` plus the `immutable_references` ranges. A wrong, unaudited, or tampered deployment becomes a refused boot rather than a silent funds-custody risk.

### v0.5.3 (CI + demo integrity)

Internal hardening. No API, protocol, MCP, or SDK surface change. Recorded here because it restores an end-to-end proof artifact integrators rely on, and one item affects anyone bootstrapping the schema.

#### Fixed. CI + golden-path

* **The golden-path smoke runs end-to-end as a post-merge CI gate again.** Its job depends on the unit+integration job, which had been red on an unrelated, masked test-schema failure, so the smoke was silently skipped and the full `seed → ingest → normalize → propose → policy → execute → proof` chain was not actually exercised in CI. The prerequisite suite is green again and the smoke passes front to back (propose → `approved` → execute → `dispatching`).
* **The demo seed no longer trips the §6 duplicate-payment gate (check 11.5).** A freshly-seeded counterparty's payment instructions were stamped `now()`, which the `destination_recently_changed` rule correctly reads as a vendor-account-swap signal. The seed's backdate mitigation had been scoped to the on-chain recipient only; it now backdates every seeded counterparty for the tenant, so the default ACH demo settles cleanly. The §6 gate itself is unchanged and still fail-closed.
* **The migration set is self-contained for `pgcrypto`.** A migrations-only bootstrap (for example a test schema that does not run the `postgres-init` extension script) now creates `pgcrypto` via migration `0031`, so the migration `0027` payment-instruction trigger's `digest()` call resolves instead of failing at first insert. This affects anyone building a schema from the migration set alone.

### v0.5.2 (control-plane hardening)

Follow-up review fixes. All stricter / fail-closed.

#### Changed. Safer defaults + provenance

* **The default tenant policy never auto-executes money.** A freshly provisioned tenant's default now requires human **confirmation** for `outbound_payment` / `onchain_tx` above the confidence floor (with a single-signer approval); non-money actions still auto-allow. The prior blanket `auto` rule (which the repo's own policy linter flags as unsafe-for-money) is gone. A tenant signs a constrained autonomy policy to earn unattended money movement.
* **High-trust provider source types are reserved to authenticated ingestion.** `source_type: "plaid"` / `"stripe"` are refused on the generic `/raw/ingest` route with `raw_source_reserved` (403); they may only arrive via the HMAC-verified `/raw/webhooks/{provider}` path, so a `raw:write` caller can't mint high-trust evidence by choosing the label.

#### Fixed. Audit-control plumbing

* The committed `contracts/audit-status.json` now ships in the production image, so the mainnet escrow boot fence can actually pass once the audit is approved (previously it was excluded and failed closed forever).
* `pnpm run production-readiness` now models the same two-part mainnet-escrow condition as the runtime boot fence (committed approved record **and** env attestation), so the report can no longer show green for a deployment the runtime would reject.

### v0.5.1 (autonomy + provenance hardening)

A safety-hardening batch. All changes are stricter (fail-closed), never more permissive.

#### Changed. Tighter defaults + authorization

* **Default confidence floor raised to `0.6`.** A freshly provisioned tenant's default policy now rejects auto-execution of an intent backed only by an uncorroborated, document-extracted obligation (capped at `0.5`); a corroborated obligation (reconciliation lifts it to \~`0.7`+) still passes. Tenants with their own signed policy are unaffected.
* **Signed per-agent action allowlist (`PolicyDocument.agent_actions`) is now enforced on every action-resolution path** (explicit request, event map, intent-classifier match, and default action), not just explicit requests. A denied action can no longer be smuggled in via an event mapping or a default.
* **Evidence trust derives from the raw artifact's `source_type`**, not the caller-chosen parser label, so a `raw:write` principal can no longer mint high-trust evidence by labelling its parser `plaid`/`stripe`.

#### Added. Obligation-direction safety

* **`obligation_direction_invalid` (422).** A new obligation-linked PaymentIntent must target a known `payable` obligation; a `null`/unknown or `receivable` (wrong-way) direction is refused at creation. The §6 gate's check 6.7 continues to reject `receivable` at execute for already-created intents.

#### Added. Operational / diligence

* **`contracts/audit-status.json`** is the committed source of truth for the external smart-contract audit; mainnet escrow now boots only when that record says `approved` (a bare env flag no longer bypasses a pending audit).
* **`BlobAdapter.purgeTenant`** primitive for GDPR Art. 17 tenant erasure (deletes Raw bytes under a tenant prefix; WORM/legal-hold-protected blobs are surfaced, not force-deleted).

### v0.5 (M2M Commerce + Self-Serve Onboarding)

Two additive tracks: **machine-to-machine (M2M) agent commerce** (RFC 0001) and **self-serve onboarding** (RFC 0002). Everything money-moving here is **shadow-first / fail-closed**. The new settlement rails are unregistered at boot and the new contracts are unaudited testnet/reference code. Self-serve signup is gated behind `BRAIN_SELF_SERVE_SIGNUP` (default off, sandbox-first).

#### Added. Self-serve onboarding (RFC 0002)

* `POST /v1/signup`. Open, sandbox-first tenant + owner creation (email + password). Returns a verification token directly only outside production (no email provider wired yet). Registered only when `BRAIN_SELF_SERVE_SIGNUP` is enabled; returns 404 when the flag is off.
* `POST /v1/auth/verify-email`. Verify the owner's email with the issued token.
* `POST /v1/auth/login`. Email + password login for the human owner; mints an owner JWT.
* `POST /v1/tenants/{tenant_id}/wallets`. Link a wallet to a tenant; a linked wallet can then sign in over SIWX as the owner.
* Agent on-chain registration is async: a newly registered agent starts `pending_onchain` and a relayer submits the `BrainMCPAgentRegistry` registration (the relayer is fail-closed until configured).

#### Added. M2M / x402 settlement (RFC 0001)

* `x402_settle` action type. USDC-on-Base settlement via the `x402_base` rail.
* `escrow_release` action type. Milestone / dispute-split release via the `escrow_base` rail (`BrainEscrow`).
* Both settlement rails are **unregistered at boot and fail closed** until promoted; they throw rather than fake-settle.
* Five dormant-until-wired §6 gate checks (3.5 on-chain-settlement-permitted, 5.5 agent-counterparty-attested, 6.5 x402-payment-context, 6.6 escrow-state-bound, 8.5 micropayment-cap-in-window). Each adds a row only when the intent carries settlement/escrow context **and** its on-chain loader is configured; the canonical path is unchanged for non-settlement payments.
* On-chain-settlement reconciliation matcher; agent counterparties; `chain_tx_hash` on `ledger_transactions`.

#### Added. Smart contracts (Base; unaudited)

* `BrainEscrow`. Custodial escrow with partial release, refund, and dispute splits (**UNAUDITED reference implementation**, testnet only).
* `BrainReputationRegistry`. An ERC-8004-style per-agent reputation pointer / score root (RFC 0001, **UNAUDITED testnet**). Policy reads it as a **tighten-only** threshold input. Never a money gate or a §6 precondition.

#### Errors

* `signup_email_taken` (409), `signup_token_invalid` (400), `wallet_already_linked` (409), `auth_invalid_credentials` (401), `auth_email_unverified` (403).

### v0.4 (Agent Autonomy v3)

Hardens the 19-agent internal library for production autonomous execution. **Money-movers stay shadowed by default**. Going live is a deliberate, per-agent promotion (strict caps + allowlisted rails); no agent moves money until promoted.

#### Added. HTTP API

* `POST /v1/agents/route`. Routing decision only (no run).
* `POST /v1/agents/run`. Route → resolve action → dry-run gate → persist run → propose (shadow-aware; a shadowed agent's financial proposal terminates as `shadow_completed`).
* `POST /v1/agents/events`. Enqueue an event-driven route/run job.
* `GET /v1/agents/runs`, `GET /v1/agents/runs/{run_id}`, `GET /v1/agents/runs/{run_id}/why`. Run history + the structured-reason / trace / gate / receipt bundle.
* `GET /v1/agents/routing-decisions/{id}`. Routing decision detail.
* `POST /v1/agents/{agent_id}/halt`, `POST /v1/agents/halt-category`. Kill-switch: pause an agent's in-flight intents + quarantine it, or emergency-stop a whole category.
* `POST /v1/payment-intents/{id}/pause`, `POST /v1/payment-intents/{id}/resume`. Pause/resume an approved intent (resume re-runs the live §6 gate).
* `GET /v1/payment-intents/{id}/replay-investigation`. Typed forensic record (intent + executions + rail receipts + linking ids).

#### Added. Policy DSL (signed)

* `agent.id`, `tenant.category`, `action.in` / `action.not_in`, `agent.behaviorHash`, `agent.spend_in_window`, `agent.tx_count_in_window`, and rule-level `approval_required_above`. All covered by the policy content hash, so they're signed.

#### Added. Smart contracts

* `BrainSmartAccount.pauseSessionKey(holder)` / `unpauseSessionKey(holder)`. Disable execution while preserving the key record, window spend, limits, and metadata (distinct from `revokeSessionKey`, which is permanent removal).
* `BrainMCPAgentRegistry.registerAgent` now takes a `behaviorHash`; `updateBehaviorHash(...)` re-attests on a model/prompt/tool change. The §6 gate adds check 1.5 (runtime `behaviorHash` must match the registered value).

#### Changed

* `Agent.state` adds `quarantined` (additive enum widening).
* Typed rail receipts (`ach` / `wire` / `erp` / `onchain`): the audit-after step refuses to commit unless the receipt validates against the rail's schema.

#### Errors

* `agent_proposal_duplicate` (409). Proposal-layer idempotency collision.

#### SDK (`@brain/sdk`)

* New `agents.route/run/enqueueEvent/listRuns/getRun/why/getRoutingDecision/halt/haltCategory` and `payments.pause/resume/replayInvestigation`. Generated types regenerated from the OpenAPI spec.

### v0.3.1 (poc-investor-demo)

#### Breaking changes

* **`BRAIN_DEMO_MODE` env var now requires literal `"true"` or `"false"`.** Previously `z.coerce.boolean()` silently coerced `"false"`, `"0"`, `"no"` to `true`. Update any `.env` or CI config using those forms.
* **`Brain.getMaskedApiKey()` renamed to `getMaskedToken()`.** Follows the `apiKey → token` rename in this release.

#### Added

* `Dockerfile`. Multi-stage build for the `brain-server` single-process boot binary.
* `GET /v1/demo/token`. Mints a 15-minute read-heavy JWT for the golden demo tenant (requires `BRAIN_DEMO_MODE=true`, refused in `NODE_ENV=production`).
* `POST /v1/audit/anchor/publish`. On-demand anchor trigger (requires `audit:admin`, 60s per-tenant cooldown).
* Live viem anchor broadcaster. `AUDIT_PUBLISHER_KEY` + `AUDIT_ANCHOR_ADDRESS` wires on-chain anchoring to Base Sepolia.
* `CORS_ALLOWED_ORIGINS` config variable. Replaces the previous reflect-any-origin behaviour.
* `tools/demo-reset`. Wipes and re-seeds golden-path demo-tenant business entities; audit log preserved.

## Current: Six-Layer Protocol with MCP

The current release introduces a Normalized Ledger between Raw and Wiki, splits Execution into a dedicated Agent layer, and adds the MCP server.

### Added

* **Normalized Ledger layer.** Eleven entities: accounts, balances, transactions, counterparties, obligations, documents, categories, transfers, invoices, payment intents, reconciliation matches.
* **Payment Intents.** Agent-proposed financial actions live as Ledger rows, queryable like any other entity.
* **Pre-execution gate.** Deterministic check against live Ledger state before any payment executes: 13 numbered checks plus 10 hardening additions (23 entries total; several record `not_applicable` until their loaders are wired). See [the pre-execution gate](/protocol/the-pre-execution-gate).
* **MCP server.** `POST /v1/agents/mcp`, JSON-RPC 2.0 over single-shot HTTP. 12 tools, 7 resource templates, 5 canned prompts.
* **Agent contributions.** External agents with `raw:write` scope can push artifacts into the Raw layer with cryptographic attribution.
* **`/v1/audit/entity/{type}/{id}` endpoint.** Pull every audit event that touched a specific Ledger row.

### Changed

* **Ledger is now the source of truth.** Wiki is downstream of Ledger and regenerable from Ledger plus Raw at any time.
* **Wiki no longer authoritative for financial state.** Wiki holds human-readable memory only; balances, transactions, and obligations come from Ledger.
* **Execution renamed to Agent.** The Agent layer covers proposal, scope enforcement, and the propose-only MCP surface.
* **Routes added.** `/payment-intents/*`, `/agents/run`, and `/agents/mcp` are the v0.3 paths that are mounted today. The legacy `/execution/*` routes remain **live and fully supported**: the generic propose/approve flow (`/execution/propose`, `/execution/approve`) and external-agent registration (`/execution/agents/register`) still run through them and have no v0.3 replacement. The reserved `/agents/{id}/propose` and `/agents/register` paths appear in the OpenAPI spec but are **not yet implemented and return 404**; do not migrate to them.

### Six Layers (Was Five)

* The previous protocol had five layers: Raw, Wiki, Policy, Execution, Audit.
* The current protocol has six: Raw, **Ledger**, Wiki, Policy, **Agent** (renamed from Execution), Audit.

## Migration from the Previous Version

| If you were using              | Use instead                                                              |
| ------------------------------ | ------------------------------------------------------------------------ |
| `/execution/propose`           | Still live (no replacement); or `/agents/run` for routed agent proposals |
| `/execution/execute`           | `/payment-intents/{id}/execute`                                          |
| `/execution/agents/register`   | Still live (no replacement); `/agents/register` is not yet implemented   |
| `/execution/mcp`               | `/agents/mcp`                                                            |
| Wiki for current balances      | `brain.accounts.list` (Ledger)                                           |
| Wiki for transaction filtering | `brain.transactions.list` (Ledger)                                       |

Only `/execution/execute` and `/execution/mcp` have v0.3 replacements; both carry `Deprecation`/`Sunset` headers. The propose/approve and agent-registration routes under `/execution/*` are **not deprecated** and remain the supported path.

## Earlier: Five-Layer Protocol

* Five layers: Raw, Wiki, Policy, Execution, Audit.
* Wiki was the source of truth for financial state.
* MCP surface lived under `/execution/mcp`.


# Support

| Channel                | Best for                           | Where                                                                                                |
| ---------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------- |
| **Status page**        | "Is Brain up?"                     | [status.brain.fi](https://status.brain.fi)                                                           |
| **Documentation**      | Reference and guides               | [docs.brain.fi](https://docs.brain.fi)                                                               |
| **GitHub Discussions** | Public questions, integration help | [github.com/braindotfi/brain-core/discussions](https://github.com/braindotfi/brain-core/discussions) |
| **Discord**            | Real-time community help           | [discord.brain.fi](https://discord.brain.fi)                                                         |
| **Email**              | Specific failed requests           | <support@brain.fi>                                                                                   |
| **Security**           | Vulnerabilities only               | <security@brain.fi>                                                                                  |

### When Opening a Ticket

The single most useful thing you can include is a **request ID** (the `request_id` field on every error envelope; `err.requestId` in the SDK). Every API and MCP response carries one. Pasting a request ID lets the support team pull the exact request, the policy version that evaluated it, and the audit event that recorded it.

```typescript
try {
  await brain.pay("acme", { invoiceId: "inv_8231" });
} catch (err) {
  console.log(err.requestId); // include this in the ticket
}
```

### SLAs

| Plan           | First response   | Channels                                 |
| -------------- | ---------------- | ---------------------------------------- |
| **Sandbox**    | Best effort      | Discord, GitHub                          |
| **Developer**  | 1 business day   | Email, Discord                           |
| **Production** | 4 business hours | Email, Discord, dedicated Slack          |
| **Enterprise** | 1 hour, 24/7     | All of the above plus on-call escalation |

### Reporting a Security Issue

Please email <security@brain.fi> directly. Do not file public issues for security vulnerabilities. Brain runs a public bug bounty; details are on the security page.


# Privacy Policy

**Effective Date: January 1, 2026**

Brain Finance Inc. ("Brain," "we," "our," or "us"), incorporated in the United States, is committed to protecting your privacy. This Privacy Policy outlines how we collect, use, disclose, and protect your personal information when you access our services, website, or products ("Services").

{% hint style="info" %}
Questions about this policy? Contact us at <legal@brain.fi>
{% endhint %}

### Information We Collect

We may collect the following categories of information:

<table><thead><tr><th width="199.5859375">Category</th><th>Examples</th></tr></thead><tbody><tr><td><strong>Personal identifiers</strong></td><td>Name, email address, contact details, wallet address</td></tr><tr><td><strong>Technical data</strong></td><td>IP address, browser type, device information, access times</td></tr><tr><td><strong>Usage data</strong></td><td>Pages viewed, time spent on site, and other analytical data</td></tr><tr><td><strong>Communications</strong></td><td>Correspondence and related information if you contact us directly</td></tr></tbody></table>

### How We Use Your Information

We use the information we collect to:

* Provide and improve our Services
* Respond to inquiries and support requests
* Send service-related and promotional communications
* Comply with legal obligations and prevent fraud

### Sharing and Disclosure

We do not sell your personal data. We may share your information with third parties only in the following circumstances:

**Service Providers**\
Third parties that support our infrastructure, analytics, or communication services.

**Legal Authorities**\
When required to comply with applicable laws or to respond to lawful requests from public authorities.

**Business Transfers**\
In the event of a merger, acquisition, or sale of assets, your information may be transferred as part of that transaction.

### Data Storage and Security

Your data is stored using secure cloud providers. We implement industry-standard measures to protect your information against unauthorised access, disclosure, or loss.

{% hint style="warning" %}
No method of transmission over the internet is 100% secure. While we take reasonable steps to protect your data, we cannot guarantee absolute security.
{% endhint %}

### Your Rights

Depending on your jurisdiction, you may have the right to:

* **Access**: request a copy of the personal data we hold about you
* **Update or delete**: correct inaccurate data or request its deletion
* **Object to processing**: restrict how we use your data in certain circumstances
* **Data portability**: receive your data in a structured, machine-readable format
* **Lodge a complaint**: raise concerns with your local data protection authority

To exercise any of these rights, contact us at <legal@brain.fi>.

### International Transfers

Brain Finance Inc. is incorporated in the United States. As a result, your data may be processed and stored in countries other than your own. By using our Services, you consent to this transfer.

### Changes to This Policy

We may update this Privacy Policy at any time. We will communicate material changes via email or a prominent notice on our website before they take effect.

### Contact

For any questions about this Privacy Policy, please contact us at <legal@brain.fi>.


# Terms of Service

**Effective Date: January 1, 2026**

These Terms of Service ("Terms") govern your access and use of the products, software, services, and website provided by Brain Finance Inc. ("Brain," "we," "us," or "our"). By using our Services, you agree to these Terms.

{% hint style="info" %}
Questions about these Terms? Contact us at <legal@brain.fi>
{% endhint %}

### Eligibility

You must be at least 18 years old and legally capable of entering into binding contracts. By using our Services, you represent and warrant that you meet these criteria.

### Use of Services

You agree not to use our Services in any of the following ways:

* **Unlawful use**: violate any applicable laws or regulations
* **Fraud or abuse**: use our Services for fraudulent, deceptive, or illegal purposes
* **Infrastructure interference**: disrupt, degrade, or interfere with our network, systems, or infrastructure
* **Reverse engineering**: attempt to reverse-engineer, decompile, or replicate our proprietary technology

### Intellectual Property

All content, trademarks, and technology on our platform are owned by Brain Finance Inc. or its licensors. You may not copy, modify, or distribute any part of our Services without our prior written permission.

### User Content

If you submit content to us including feedback, ideas, or suggestions, you grant Brain Finance Inc. a non-exclusive, royalty-free, perpetual license to use that content for any purpose.

### Third-Party Services

Our Services may link to or integrate with third-party services, protocols, or platforms. Brain Finance Inc. is not responsible for the practices, availability, or content of those third-party services.

### No Financial Advice

{% hint style="warning" %}
Our Services, including any automated insights or agent outputs, are for informational purposes only. Nothing on the Brain platform constitutes financial, legal, or investment advice. Always consult a qualified professional before making financial decisions.
{% endhint %}

### Disclaimers

The Services are provided **"as is"** and **"as available"** without warranties of any kind. Brain Finance Inc. expressly disclaims all warranties, whether express or implied, including implied warranties of merchantability, fitness for a particular purpose, and non-infringement.

### Limitation of Liability

To the fullest extent permitted by applicable law, Brain Finance Inc. shall not be liable for any indirect, incidental, special, or consequential damages arising from:

* Your use of, or inability to use, our Services
* Any unauthorised access to or alteration of your data
* Any conduct or content of third parties on or via our Services

### Indemnity

You agree to indemnify, defend, and hold harmless Brain Finance Inc. and its officers, directors, employees, and agents from any claims, damages, losses, or expenses, including reasonable legal fees arising out of your use of the Services or your violation of these Terms.

### Governing Law

These Terms are governed by the laws of the United States. Any disputes arising under or in connection with these Terms shall be resolved in the courts of the United States, unless otherwise agreed in writing by both parties.

### Termination

We reserve the right to suspend or terminate your access to our Services at our sole discretion, at any time and without notice, for any reason including, but not limited to, breach of these Terms.

### Changes to These Terms

We may update these Terms at any time. Material changes will be communicated via email or a prominent notice on our website before they take effect. Continued use of our Services after any changes constitutes your acceptance of the revised Terms.

### Contact

For any questions about these Terms, please contact us at <legal@brain.fi>.


# Cookies Policy

**Effective Date: January 1, 2026**

Brain Finance Inc. ("Brain," "we," "our," or "us"), incorporated in the United States, is committed to protecting your data and information. This Cookies Policy outlines how we use cookies and similar technologies to operate our websites, documentation, applications, APIs, and related services.

This Cookie Policy explains what cookies are, how we use them, and the choices you have. It should be read together with our [Privacy Policy](/legal/privacy-policy).

{% hint style="info" %}
Questions about this policy? Contact us at <legal@brain.fi>
{% endhint %}

### What Are Cookies?

Cookies are small text files placed on your device when you visit a website. They help websites remember information about your visit, such as your preferences, session status, browser type, or usage activity.

We may also use similar technologies such as pixels, local storage, software development kits, tags, and analytics tools.

### How We Use Cookies

Brain may use cookies and similar technologies for the following purposes:

#### Essential Cookies

These cookies are necessary for our websites and services to work properly. They may support security, authentication, session management, fraud prevention, load balancing, and basic site functionality.

You cannot disable essential cookies through our cookie tools because the services may not function correctly without them.

#### Functional Cookies

These cookies help us remember user preferences, such as interface settings, documentation preferences, region, language, or other settings that improve your experience.

#### Analytics Cookies

These cookies help us understand how users interact with our websites, documentation, and services. For example, they may help us measure page visits, traffic sources, feature usage, error patterns, and overall performance.

We use this information to improve Brain’s product experience, content, documentation, and service reliability.

#### Security and Fraud Prevention Cookies

We may use cookies and similar technologies to detect abuse, protect accounts, prevent unauthorized access, monitor suspicious activity, and maintain the integrity of our platform.

#### Marketing and Measurement Cookies

Where permitted, we may use cookies to understand the effectiveness of our marketing, improve messaging, measure campaigns, and show more relevant content. We do not use these cookies to sell your personal information.

### Third-Party Cookies

Some cookies may be placed by third-party service providers that help us operate, secure, analyze, or improve our websites and services.

These providers may include hosting, analytics, infrastructure, security, customer support, payment, CRM, or marketing tools. Their use of cookies is governed by their own policies.

We do not control third-party cookies once they are placed by those providers, but we choose service providers that support our business, security, and compliance needs.

### Cookies and Personal Information

Cookies may collect or be linked to information such as:

* Device type
* Browser type
* IP address
* Approximate location
* Pages viewed
* Referring URLs
* Session identifiers
* Usage events
* Account or login status, where applicable

Where cookie data identifies or can reasonably be linked to you, we treat it in accordance with our Privacy Policy.

### Your Choices

You can control cookies in several ways:

#### Browser Settings

Most browsers allow you to block, delete, or manage cookies. Blocking certain cookies may affect how our websites or services function.

#### Cookie Banner or Preferences Tool

Where required, we may provide a cookie banner or preference center that allows you to accept, reject, or customize certain non-essential cookies.

#### Analytics and Advertising Controls

Some third-party analytics or advertising providers offer their own opt-out tools. You may also be able to manage tracking preferences through your browser, device, or platform settings.

### Do Not Track

Some browsers offer “Do Not Track” signals. Because there is no consistent industry standard for responding to these signals, our websites may not respond to them in a uniform way. Where legally required, we will honor applicable privacy choices and opt-out signals.

### Data Retention

Cookies may remain on your device for different periods of time.

Session cookies are deleted when you close your browser. Persistent cookies remain for a set period or until you delete them. The duration depends on the cookie’s purpose and the settings of Brain or the applicable third-party provider.

### International Users

Brain may operate across different jurisdictions. By using our websites or services, your cookie-related information may be processed in the United States or other locations where Brain or its service providers operate.

### Updates to This Policy

We may update this Cookie Policy from time to time to reflect changes in our technology, services, legal obligations, or business practices. When we make material changes, we will update the effective date or provide additional notice where required.

### Contact

For any questions about these Terms, please contact us at <legal@brain.fi>


# Disclaimer

The contents of all documentation and materials related to Brain Finance Inc. are subject to change at any time by the team or participating network members.

{% hint style="warning" %}
Nothing in Brain Finance Inc.'s documentation, marketing materials, or platform constitutes an offer to buy or sell any security, or financial, legal, or investment advice. Always consult a qualified professional before making any financial decision.
{% endhint %}

### Forward-Looking Statements

This documentation may contain forward-looking statements reflecting Brain Finance Inc.'s current expectations regarding product development, execution timelines, financial outlook, strategy, and future plans.

Statements that use phrases such as "expects," "plans," "believes," "intends," "may," "will," "targets," or similar expressions are forward-looking in nature. They involve inherent risks and uncertainties, and actual outcomes may differ materially from those anticipated.

These statements are current as of the date of publication. Brain Finance Inc. has no obligation to revise or update them in light of new information or future developments.

### No Guarantee of Performance

This documentation does not constitute a profit forecast or a guarantee of future performance, nor does it imply any specific earnings or returns.

| What this documentation is                              | What this documentation is not                   |
| ------------------------------------------------------- | ------------------------------------------------ |
| General information about Brain's products and protocol | A profit forecast or performance guarantee       |
| A description of Brain's current plans and direction    | A solicitation to buy or sell any security       |
| Subject to change without notice                        | Reviewed or approved by any regulatory authority |

The information provided has not been reviewed or approved by any regulatory authority. Its distribution does not indicate compliance with any legal or regulatory framework.

### Not Financial Advice

Nothing contained in Brain Finance Inc.'s documentation or marketing materials constitutes an offer to buy or sell any security. These materials do not take into account any individual's specific financial situation, objectives, or needs.

{% hint style="warning" %}
All prospective participants are strongly encouraged to consult with a qualified financial advisor, legal counsel, and/or tax advisor before making any investment or financial decision.
{% endhint %}

### No Warranty

Brain Finance Inc. does not warrant the completeness, accuracy, or adequacy of the information provided in any of its documentation or materials. Any views, projections, or forward-looking statements expressed are subject to change without notice.

### Your Agreement

By accessing or using Brain Finance Inc.'s documentation, platform, or materials, you agree to our Terms of Service, Privacy Policy, and this Disclaimer.

Before proceeding with any investment or financial decision, carefully review all associated materials, risk factors, investor requirements, and applicable resale restrictions.

### Contact

For any questions about this Disclaimer, please contact us at <legal@brain.fi>.


