Brain Documentation Audit
Brain's docs are unusually well-written page by page, but they read like several drafts of the same system stacked on top of each other: the pre-execution gate, the MCP error codes, the route namespace, who is allowed to execute money movement, what an "internal agent" even is, and the policy/status/scope vocabularies each contradict themselves across pages — exactly the class of conflict an AI coding agent cannot resolve with judgment.
1. The pre-execution gate's total is stated five different ways (critical)
Location: /protocol/the-pre-execution-gate, /mcp-server/overview, /mcp-server/tools, /architecture/readiness-summary, /architecture/enterprise-readiness, /resources/changelog
Problem: The single deterministic safety gate — described everywhere as "the only path to financial execution" — resolves to a different total on nearly every page, and the gate's own page contradicts itself in adjacent sections. The totals come out to: 23 (the gate page intro, "13 numbered checks plus 10 hardening additions … = 23 entries total," and Enterprise Readiness, "23 deterministic checks (13 numbered + 10 hardening additions)"); 18 (the gate page's own Hardening Additions subsection, "bringing the non-M2M total to 18 entries"); 17 (MCP Overview and MCP Tools, "13 numbered checks + 4 hardening additions"); 22 (Readiness Summary, "22 checks (13 numbered + 9 hardening)"); and 13 (Changelog, "Deterministic 13-step check"). That is five distinct totals — 13, 17, 18, 22, 23 — across six statements.
Consequence: A developer or compliance reviewer trying to verify what the gate actually enforces (this is a payments-execution boundary) cannot determine the real number of checks, what ships in shared/src/gate/gate.ts, or which checks are dormant. Any reviewer reading two pages gets two answers about a security-critical control.
The fix: Treat shared/src/gate/gate.ts as the single source of truth, publish one canonical table (numbered checks, hardening additions, dormant M2M checks, single total), and have every other page link to it instead of restating a count.
2. MCP error codes -32002 and -32004 are defined as opposites (critical)
Location: /resources/errors vs /mcp-server/tools
Problem: The canonical error registry says -32002 = "Scope insufficient … (auth_scope_insufficient / auth_tenant_mismatch)" and -32004 = "Pre-execution gate failed. Covers every gate_* sub-code." The MCP Tools page says the exact opposite: "The MCP layer rejects scope mismatches with JSON-RPC error -32004 (scope insufficient)."
Consequence: An external agent that branches on JSON-RPC error codes (the entire point of structured codes) will treat a scope-authorization failure as a gate failure, or a gate failure as a scope problem — and retry or escalate incorrectly on a money-movement path. Humans shrug at this; agents act on it.
The fix: Pick the registry as authoritative, correct the MCP Tools sentence to -32002, and render both pages from a single shared code table.
3. Who is allowed to execute money movement is stated four different ways (critical)
Location: /concepts/agents, /api-reference/authentication, /build/let-an-external-agent-in, /api-reference/onboarding-api vs /mcp-server/tools, /build/pay-an-invoice-safely
Problem: The core safety guarantee is contradicted four ways. (1) Concepts, Authentication, and the external-agent build guide say execution is reserved for internal Brain workers and humans never execute: "Execution is reserved for internal Brain workers running under tenant policy," and the owner JWT carries "Never payment_intent:propose / payment_intent:execute." (2) The Onboarding API says "Proposing or executing payments is reserved for registered agents running through the §6 gate." (3) MCP Tools says "humans (or internal Brain workers under an auto policy decision) execute." (4) /build/pay-an-invoice-safely says a human calling brain.approve(actionId, { as: "user_cfo" }) "records the typed signature and immediately attempts execution."
Consequence: This is the load-bearing claim of the whole product ("the safety guarantee that makes external agents safe to authorize"). A developer cannot tell whether a human approval triggers settlement, an internal worker settles, or a registered agent settles — a difference that matters for who is liable when money actually moves.
The fix: State one execution-authority model precisely — who triggers dispatch, what approve does, what role internal workers and registered agents play — and reconcile every "reserved for internal workers," "reserved for registered agents," and "human approval immediately attempts execution" statement against it.
4. "Internal agent" is defined two incompatible ways (significant)
Location: /concepts/agents vs /api-reference/authentication
Problem: Concepts defines an internal agent as the developer's own software: "Internal agent (your backend) | Server API key," and "your code is the agent." Authentication defines the internal agent as a Brain-issued credential holder: "Internal agent | Brain-issued service token | Bearer service token," with a 90-day lifetime "rotated by tenant admin." So the same caller class is credentialed by a "Server API key" on one page and a "Brain-issued service token" on another — and the repeated phrase "execution is reserved for internal Brain workers" (Concepts, Let-an-external-agent-in) implies internal agents are Brain's own first-party workers, not "your backend."
Consequence: The safety guarantee in #3 hinges entirely on what "internal agent / internal Brain worker" means. If it is the tenant's own backend, then "only internal workers execute" means the developer's server moves money; if it is a Brain-operated worker, the developer's code cannot. A developer cannot tell which trust model they are buying, or which credential their backend is supposed to present.
The fix: Define "internal agent" once — state whether it is the tenant's own backend or a Brain-operated worker, which credential it presents (Server API key vs service token vs brain_sk_; see #6), and whether it can trigger execution.
5. The migration table points to routes that the API reference says return 404 (critical)
Location: /resources/changelog vs /api-reference/payment-intents-api, /api-reference/agents-api
Problem: The Changelog declares "/agents/* … are the canonical paths. Legacy /execution/* routes continue to work," and its migration table maps /execution/propose → /agents/{id}/propose. But the Payment Intents API says "POST /v1/agents/{agent_id}/propose … and the /v1/actions/* paths are not implemented … and return 404," and the Agents API says "POST /v1/agents/register … return 404 today. Register external agents via POST /v1/execution/agents/register." So /execution/* is the live path and /agents/* is the dead one — the reverse of the Changelog.
Consequence: A developer following the official migration guide rewrites working /execution/* calls into /agents/* calls that 404, breaking agent registration and proposal in production. An agent doing the same fails silently with no way to know the migration guide is inverted.
The fix: Audit which routes are actually mounted, correct the Changelog's canonical/legacy designation and migration table to match the reference pages, and remove the deprecated-stub routes from any "use this" guidance.
6. The Authentication reference never documents the API key the SDK and Quickstart actually use (significant)
Location: /api-reference/authentication vs /introduction/quickstart, /concepts/agents, /welcome-to-brain
Problem: Authentication says "Brain authenticates three caller types" with credentials: owner JWT, Brain-issued service token, and SIWX access_token. None of these is the brain_sk_test_... / brain_sk_live_... API key that the Quickstart tells you to copy and that every SDK example passes as new Brain({ apiKey }). The API key is the primary credential in practice and is absent from the page whose job is to document credentials. It is plausibly the same thing as the "Server API key" Concepts assigns to internal agents, or the 90-day "service token" Authentication lists — but the docs never say so.
Consequence: A developer reading the auth reference to understand how their key maps to scopes, lifetime, and rotation finds nothing about brain_sk_, and instead finds three credential types that never appear in any quickstart. Rate-limit docs add to the confusion by stating limits are "per API key" — a credential the auth page doesn't acknowledge.
The fix: Add brain_sk_* as a first-class credential in Authentication (issuance, scopes it carries, sandbox vs live, rotation), and state explicitly whether it is the same artifact as the "Server API key" (#4) and/or the service token, or a distinct fourth credential.
7. Quickstart error codes are the wrong case and include a code that doesn't exist (significant)
Location: /introduction/quickstart vs /resources/errors
Problem: The Quickstart "Stuck?" table lists AUTH_INVALID_KEY, TENANT_NOT_FOUND, and SOURCE_RATE_LIMIT (UPPER_SNAKE). The canonical registry states codes are "a stable snake_case code" — auth_invalid_key, rate_limited — and there is no source_rate_limit / SOURCE_RATE_LIMIT code anywhere; rate limiting returns rate_limited (429).
Consequence: A developer (or agent) who string-matches the code Brain actually returns (auth_invalid_key) against the Quickstart's AUTH_INVALID_KEY gets no match, and anyone handling SOURCE_RATE_LIMIT writes a branch that never fires. The first error-handling page a new user reads teaches codes the API never emits.
The fix: Rewrite the Quickstart table using the canonical lowercase codes, replace SOURCE_RATE_LIMIT with rate_limited, and align the "60 rpm" note with the real per-tier limits (see #14).
8. docs_url in the error envelope points to a path that isn't where the errors page lives (significant)
Location: /api-reference/overview vs /resources/errors
Problem: The standard error envelope returns "docs_url": "https://docs.brain.fi/errors/policy_denied", and the registry's stated format is https://docs.brain.fi/errors/{code}. But the actual error reference is published at /resources/errors, and the llms.txt sitemap lists it there. There is no /errors/{code} page.
Consequence: Every error Brain returns hands the developer a self-help link that 404s. The one field designed to get a stuck developer to the right doc sends them nowhere.
The fix: Either serve /errors/{code} (anchored to the registry section) or change docs_url to emit the real /resources/errors#{code} URL.
9. The SDK's payment status value auto is not in the documented status lifecycle (significant)
Location: /build/pay-an-invoice-safely, /introduction/quickstart vs /api-reference/payment-intents-api
Problem: SDK examples show action.status as "auto" | "needs_approval" | "rejected", and /build/pay-an-invoice-safely uses auto as a terminal status meaning "already executed." But the Payment Intents API status lifecycle is proposed | pending_approval | approved | rejected | executed | failed | cancelled — there is no auto, and "already executed" maps to executed.
Consequence: A developer who reads the HTTP status enum and writes if (status === "executed") will never match the SDK's auto, and vice versa. Webhook/state-machine code branches on a status value that only exists in half the docs.
The fix: Document the exact mapping between SDK status strings and the HTTP lifecycle states, or unify them to one set.
10. Policy decisions are described with three irreconcilable vocabularies (significant)
Location: /api-reference/policy-api, /resources/errors vs /build/give-an-agent-a-spending-limit
Problem: Three distinct decision vocabularies appear, none mapped to the others: (1) allow | confirm | reject — the Policy API decision triple and the Errors registry, "Casing is lowercase"; (2) auto | needs_approval | rejected — the SDK's decision.outcome and rule then values in the spending-limit guide; (3) auto | confirm | reject — the rule-level execute field documented in the Policy API itself ("execute is one of auto | confirm | reject"). No page maps allow→auto, confirm→needs_approval/confirm, reject→rejected.
Consequence: A developer switching between the SDK, the rule DSL, and the HTTP/MCP decision layer must guess the translation for a value that gates whether money moves. An agent comparing decision.outcome against the documented allow | confirm | reject set fails every comparison.
The fix: Publish the canonical decision triple once, show the SDK alias mapping and the rule-level execute mapping explicitly in one table, and stop presenting auto/needs_approval/rejected as if it were the protocol vocabulary.
11. Agent permissions are expressed in three incompatible scope vocabularies (significant)
Location: /build/let-an-external-agent-in vs /api-reference/agents-api
Problem: There are at least three incompatible ways to express what an agent may do, none cross-referenced. The SDK register call uses capability strings: capabilities: ["read", "propose_payment", "propose_action"]. The SDK grantScope call uses scope strings: scopes: ["ledger:read", "wiki:read", "payment_intent:propose"]. The HTTP POST /v1/execution/agents/register body uses a third set: allowed_actions: ["read_wiki", "propose_action", "read_audit"] plus action_types_proposable: ["outbound_payment"].
Consequence: To grant an agent the ability to propose a payment, a developer must write propose_payment (SDK capability), payment_intent:propose (SDK scope), or propose_action + action_types_proposable:["outbound_payment"] (HTTP) depending on the surface — and no page maps these to each other. Cross-surface code grants the wrong permission, or none, on a money-movement boundary.
The fix: Publish one canonical agent-permission vocabulary with explicit per-surface aliases in a single table, and have the SDK and HTTP reference render from it.
12. The action_type enum differs between the HTTP API and the MCP propose tool (significant)
Location: /api-reference/payment-intents-api vs /mcp-server/tools
Problem: HTTP action_type is "ach_outbound | ach_inbound | wire | onchain_transfer | erp_writeback | card_payment | x402_settle | escrow_release" — no other. The MCP payment_intent.propose tool lists "ach_outbound, ach_inbound, wire, onchain_transfer, erp_writeback, card_payment, other" — it adds other and drops x402_settle/escrow_release.
Consequence: An agent that proposes an x402_settle over MCP (a real, documented settlement type on the HTTP side) gets rejected, and one that sends other over HTTP gets rejected — for the same logical operation. The enum a caller may use depends on the surface, undocumented.
The fix: Define one action_type enum shared by both surfaces, or explicitly document per-surface differences and why x402/escrow are MCP-unavailable.
13. The MCP surface size disagrees with the changelog (significant)
Location: /mcp-server/overview vs /resources/changelog
Problem: MCP Overview states the surface is "12 tools, 7 resource templates, 5 canned prompts" (and MCP Tools repeats "12 tools"). The Changelog's current MCP entry says "10 tools, 5 resource templates, 5 canned prompts."
Consequence: A developer building against the MCP server can't tell whether two tools and two resource templates exist or not — and can't tell which page is stale, since both are dated current.
The fix: Reconcile the counts against the deployed server, list the exact tool/resource/prompt names once, and reference that list from both pages.
14. Console domains, API base URLs, and rate-limit framing are inconsistent (significant)
Location: /introduction/quickstart vs /api-reference/overview
Problem: Quickstart sends users to "console.brain.dev" for sandbox and says production keys work "against console.brain.fi" — two different TLDs for the console. The API reference gives canonical API hosts https://api.brain.fi and https://api.sandbox.brain.fi. Separately, Quickstart implies a flat "Sandbox limits are 60 rpm," while the API overview documents per-tier limits (Free 60, Developer 600, Production 6,000, Enterprise custom).
Consequence: A developer who bookmarks console.brain.dev may hit the wrong (or non-existent) host, and one who builds backoff around "60 rpm" under-utilizes higher tiers or mishandles the real rate_limited 429. The .dev/.fi split is exactly the kind of typo-class confusion that costs an hour.
The fix: Pick one console domain and use it everywhere; cross-link the API base URLs from the Quickstart; replace the "60 rpm" line with the per-tier table or a link to it.
15. "Production keys work the same way" hides that Brain is staging/Sepolia-only (significant)
Location: /introduction/quickstart, /welcome-to-brain vs /architecture/readiness-summary, /architecture/enterprise-readiness
Problem: The Quickstart says "Production keys (brain_sk_live_...) work the same way," and Welcome frames autonomous execution as a shipped capability. But Readiness Summary states Brain "is not yet 'unrestricted production mainnet'," is "staging / controlled-pilot," with payment rails on Base Sepolia only, and Enterprise Readiness lists "Two items must close before 'production-ready' is an honest claim" (external smart-contract audit pending, Azure deploy chain never exercised).
Consequence: A developer reading the onboarding path reasonably believes they can move real money with a live key, when the architecture pages say settlement runs on a testnet behind unaudited contracts. That gap between the front-door framing and the readiness caveat is material and money-related.
The fix: Put the staging/controlled-pilot caveat (testnet-only, audit pending) in the Quickstart and Welcome where brain_sk_live_ is introduced, not only on deep architecture pages.
16. The apiKey → token rename is shipped in the changelog but not in any example (significant)
Location: /resources/changelog vs /welcome-to-brain, /introduction/quickstart
Problem: The Changelog lists a breaking change: "Brain.getMaskedApiKey() renamed to getMaskedToken(). Follows the apiKey → token rename in this release." Yet Welcome and Quickstart still construct the client with new Brain({ apiKey: process.env.BRAIN_API_KEY }).
Consequence: If the rename actually shipped, every quickstart snippet now constructs the client with a removed option name and breaks on the first call; if it didn't ship, the changelog is wrong. Either way a developer copy-pasting the headline example is exposed to a breaking change the examples ignore.
The fix: Update all SDK examples to the post-rename option (token) or clarify that apiKey remains an accepted alias, and align getMaskedToken() usage everywhere.
17. The SDK agent-registration example passes fields that don't exist in the deployed contract (significant)
Location: /build/let-an-external-agent-in vs /smart-contracts/brainmcpagentregistry
Problem: The build guide calls brain.agents.register({ address, identityRoot, mcpEndpoint, capabilities }). The contract page is explicit that the deployed AgentRegistration struct stores only agentId, agentAddress, tenantId, scopeHash, behaviorHash, and that identityRoot, mcpEndpoint, capabilities[], reputationRoot are "the planned target … None of these fields exist in the deployed struct today."
Consequence: A developer copies the registration call believing identityRoot/mcpEndpoint/capabilities are honored on-chain; in reality they're silently dropped or planned-only, so the agent's capabilities aren't anchored where the developer thinks they are — a security-relevant misunderstanding.
The fix: Mark the planned fields as planned in the SDK example (or remove them), and show the registration shape that maps to the deployed struct, with capabilities expressed via the actual scopeHash mechanism.
18. Webhook event names don't match between the build guide and the Webhooks/Audit surfaces (significant)
Location: /build/pay-an-invoice-safely vs /api-reference/webhooks-api
Problem: The build guide's webhook table uses action.proposed, action.approved, action.executed, action.settled, action.failed. The Webhooks API dead-letter payloads use payment_intent.* event types (e.g. "event_type": "payment_intent.executed"). The two naming schemes never reconcile, and the action.* namespace matches /v1/actions/*, which the Payment Intents API says is "not implemented."
Consequence: A developer who subscribes to or filters on action.executed will never receive an event named payment_intent.executed, so webhook handlers silently miss every event. There is also no action.settled analog shown on the payment_intent.* side, so the settlement signal is ambiguous.
The fix: Publish one canonical event-type catalog (names, when each fires, including settlement) and use it verbatim in both the build guide and the Webhooks API page.
19. The self-serve signup endpoint has two different paths (significant)
Location: /api-reference/onboarding-api vs /resources/changelog
Problem: The Onboarding API documents "Sign up a new tenant | POST /v1/signup," but the Changelog's "Added. Self-serve onboarding" entry says "POST /v1/auth/signup." (Verification is POST /v1/auth/verify-email in both, which makes the bare /v1/signup look even more like an inconsistency.)
Consequence: A developer wiring up self-serve onboarding picks one path and gets a 404 on the other — compounded by the fact that the route is flag-gated (BRAIN_SELF_SERVE_SIGNUP, default off) and already returns 404 when disabled, making the wrong-path failure hard to diagnose.
The fix: Settle on one signup path, correct the Changelog or the Onboarding API to match, and note the flag-gated 404 behavior next to the endpoint so developers can distinguish "wrong path" from "flag off."
20. The "prove it on-chain" promise has no addresses to verify against (significant)
Location: /welcome-to-brain, /smart-contracts/overview, /smart-contracts/brainmcpagentregistry
Problem: Welcome makes verifiable proof a core capability — const proof = await brain.proof(action.id); // Get a verifiable record of what just happened, the "ingest … execute, prove" framing, and the Smart contracts surface listed for "On-chain settlement … scope attests." But the smart-contract pages, as published, show no deployed contract addresses for any network — neither Base Sepolia nor mainnet — for BrainMCPAgentRegistry, BrainEscrow, or BrainReputationRegistry, and state the escrow/reputation contracts are "UNAUDITED, testnet only" on Base Sepolia, with mainnet blocked on a pending external audit.
Consequence: A developer who takes a brain.proof() result and wants to confirm it on a block explorer has no contract address to look up and no pointer to the on-chain anchor — the verifiability the product leads with can't be exercised from the docs.
The fix: Publish the Base Sepolia deployment addresses on the smart-contract overview (with a clear "no mainnet deployment yet" note and explorer links), and link brain.proof() to the specific on-chain record it anchors.
21. The headline natural-language policy feature has no HTTP endpoint (significant)
Location: /build/give-an-agent-a-spending-limit, /welcome-to-brain vs /api-reference/policy-api
Problem: The SDK markets plain-English policy authoring: brain.policy.create("acme", { text: "Allow invoice payments under $5,000 to approved vendors…" }). But the Policy API's compose route states "The DSL is structured JSON, not prose," and no HTTP endpoint for natural-language policy authoring is documented. The SDK uses create/activate; the HTTP layer uses compose/sign over JSON.
Consequence: Any non-TypeScript developer (the HTTP API explicitly targets "any language") cannot reproduce the product's flagship capability, and cannot tell whether the SDK compiles prose client-side or calls an undocumented endpoint. The most-marketed feature is missing from the reference that's supposed to serve every language.
The fix: Document where prose→DSL compilation happens — either expose the natural-language compose endpoint over HTTP or state clearly that NL authoring is SDK-only and show the JSON a non-SDK caller must produce.
22. Support and legal contact addresses are blank placeholders (minor)
Location: /resources/support, /legal/privacy-policy
Problem: The Support page's Email and Security rows have empty destinations, and "Reporting a Security Issue: Please email directly" has no address. The Privacy Policy repeats "Contact us at " and "contact us at ." with no email in four places, including the disclaimer's "please contact us at ." Separately, Support calls the correlation id a "trace ID" surfaced as err.traceId, while the Errors/API pages call the same value request_id / err.requestId.
Consequence: A developer hitting a failed payment or a security researcher with a vulnerability has no address to write to from the pages designed to provide one — and the renamed correlation field means the "include your trace ID" instruction points at a property other pages call requestId.
The fix: Fill in the security and support email addresses on both the Support and Privacy pages, and standardize on one name (request_id / err.requestId) for the correlation id everywhere.
What they do well
- Per-page writing quality is high — the protocol, gate, and readiness pages are honest about pilot status, unaudited contracts, and testnet-only rails where they discuss it.
- Machine-readable surface exists —
llms.txt,.mdpage variants, and the GitBook?ask=query feature make the docs genuinely agent-fetchable. - Error envelope design is sound — a stable
snake_casecode,request_id, structureddetails, and adocs_urlfield is the right shape (it just needs the contradictions in #7/#8 resolved).
Top 3 recommendations
- Establish single sources of truth for the conflict-prone primitives — gate check count (#1), MCP/error codes (#2), route namespace (#5, #19), execution authority and the "internal agent" definition (#3, #4), and the policy/status/scope vocabularies (#9, #10, #11) — and have every page render from them instead of restating. This kills the bulk of the audit at once.
- Reconcile the SDK examples with the deployed reality —
apiKey/token(#16), payment statusauto(#9), agent-registration fields (#17), webhook event names (#18), and the signup path (#19) — so copy-pasted snippets actually run. - Make the on-chain and production claims verifiable — publish Base Sepolia addresses and link
brain.proof()to the record it anchors (#20), surface the staging/testnet caveat at the Quickstart front door (#15), and fill in the empty support/security/legal contact fields (#22).