OpenChainGraph · Explainer 10 steps in three parts

From a page in a tab to a receipt in your ledger

The AIN Bridge is the client-side script inside every AINumbers tool page. This page walks the bridge end to end. A link or a dropped file fills a form. An AI host renders the same page inline as an MCP Apps View and drives it with the ui/ JSON-RPC 2.0 dialect. The Policy Mandate the page exports can then travel to an open ledger tab over a same-origin BroadcastChannel and be recorded live with an acknowledgement.

Presenter mode shows one step per screen. Arrow keys move, A toggles autoplay, Esc exits.
Every method name, constant and rule quoted on this page comes from the merged code in the site repo and the worker repo as of 29 September 2026. The bridge snippet is version 1.3. The worker served 626 node Views on that date. Protocol claims are quoted against that code, never against a row description.
Part one · The page in the tab

Everything starts with one page

An AINumbers tool is a single HTML page with its inputs and its math written inline. Nothing else is required for the three intake channels below to work.

1
Step 1 · Page

A self-contained page computes in the tab

Once loaded, a tool page computes without a server and without a network call. Its only output is what it draws and the Policy Mandate it can export on demand.

The bridge gives the page three intake channels. A URL hash fragment named #in= can fill the form. A dropped or pasted .policy.json file can fill the next page from the previous one. A message from a same-origin parent frame can drive it too, and since bridge version 1.3 that message channel speaks JSON-RPC 2.0 in the MCP Apps ui/ dialect. The integration is one config line plus one inline script before </body>:

Integration referenceper tool page
<script>window.AIN_BRIDGE_CFG = {
  runFn:        "runMyTool",      // global function name; null means no auto-run
  intake:       true,             // render the Policy Mandate import zone
  intakeTarget: "mandateInput",   // textarea id for a full JSON paste (optional)
  intakeAnchor: ".pii-notice"     // the import zone inserts before this element
};</script>
<script>/* inline copy of scripts/ain-bridge-v1.snippet.html, version 1.3 */</script>
3
Step 3 · File

A file can fill the next page

A page configured with intake: true renders an import zone. Drop a .policy.json Policy Mandate onto it or paste the raw JSON and the matching inputs fill.

The bridge validates the shape before it touches the DOM. The check is one line of the shipped code: looksLikeMandate requires mandate_id, tool_id and a payload key. Anything else is refused with a warning and nothing is applied. The resolution order is the intakeTarget textarea first, then payload, then source_tool_inputs. The minimum shape looks like this:

Policy Mandate · minimum shapechecked by the bridge
{
  "mandate_id": "mnd_20260618_T503_abc123",
  "tool_id":    "503-canton-tokenization-readiness-diagnostic",
  "issued_at":  "2026-06-18T14:30:00Z",
  "payload":           { "entityName": "Apex Clearing Corp", "domicile": "US" },
  "source_tool_inputs":{ "entityType": "clearing_member" },
  "execution_hash":    "a3f8c2…"
}
4
Step 4 · API

The page exposes an API called AINBridge

After the bridge boots, the page holds one global object, window.AINBridge. That object is the page's API. Each row below is one member of it, and a script or an agent reaches every leg of the bridge through these members.

AINBridge memberWhat the shipped code does
versionThe string "1.3".
capabilitiesA derived feature-detect object with flags for prefill, mandate, handoff, sendToVerify, sessionRoot, liveLedger, mcpApps and share. Most flags are read from the routine's presence, so an older page reports the truth about itself. The share flag follows the page's own CFG.share setting.
apply(fields)Applies an {id: value} map to DOM inputs and returns the matched count.
run()Calls CFG.runFn and returns a boolean.
intake(json)Validates and applies a Policy Mandate JSON string. Returns the applied count.
getMandate()Returns window._lastMandate, then AIN_BUILD_MANDATE(), then window._currentMandate.
makeLink(fields, run)Builds origin + pathname + '#in=' plus the encoded fields and the optional &run=1.
mcpThe View legs: ready(), host(), send(), message(text) and openLink(url). Every send passes the method allowlist and the pinned origin.
sessionRoot(hashes)Async. Takes a non-empty array of execution_hash values and returns the session receipt Merkle root, the same rule the worker applies.
ledgerSend(receipt)Posts a receipt to an open ledger tab on this origin and resolves with the acknowledgement within 2000 ms, or null.
ledgerTarget(origin)Returns <origin>/ledger/ for ainumbers.co, localhost and 127.0.0.1 over http or https. Any other origin returns null.
sendToVerify()Takes the current Policy Mandate, hands it to the ledger leg and opens the ledger page. The notice reports the ack.

The API table replaces the shorter six-row table the previous version of this page carried. sendToVerify, sessionRoot, ledgerSend, ledgerTarget and the whole mcp object are additions of the 2026-09 bridge rows, and version moved from "1.0" to "1.3". The twelve members above are the complete list of what the object holds.

Part two · The View inside your AI host

The host can drive the same page

An MCP Apps host renders a tool page inline as a View. The worker hands the page to the host. The host and the page then speak one JSON-RPC 2.0 dialect.

5
Step 5 · View served

The worker serves each page as a View

The MCP worker vendors every bridged node page verbatim and registers it as the resource ui://ainumbers/node/<tool_id>. On 29 September 2026 that register held 626 views.

The vendor step copies each chaingraph/<tool_id>.html file byte for byte into the worker's data tree and writes a sha256 sentinel into data/mcp/node-views.json tying the served bytes to the site file at the vendored commit. A page above the 1 MiB guard is skipped and listed. The largest page at that vendor measured 298,239 bytes. Resources ride the existing resources/read path and stay off the tools/call single-tool fast path, so serving a page never widens the compute budget of a tool call. The tool definition itself gains _meta.ui metadata with the resource URI and the visibility pair ['model','app'].

6
Step 6 · Handshake

The host and the page shake hands

A View opens with one request, ui/initialize. The answer pins the host's origin and every later message is addressed to it.

AI hostframes the View the pagerendered as a View ui/initialize the answer pins the host origin the answer ui/notifications/initialized every later send is addressedto the pinned origin

One send before the handshake carries nothing but the page's capability declaration. A sandboxed View is not told who framed it, so that single message is addressed with a wildcard and the code marks the exception. The version constant in the request is 2026-01-26.

The shipped code holds two allowlists. The page accepts ui/notifications/tool-input, ui/notifications/tool-input-partial, ui/notifications/tool-result, ui/notifications/tool-cancelled, ui/notifications/host-context-changed and ui/resource-teardown from the host. The page may emit ui/initialize, ui/notifications/initialized, ui/message, ui/open-link, ui/request-display-mode, ui/update-model-context, ui/notifications/size-changed and tools/call. A request outside the accepted list is answered with error code -32601, Method not found. A notification outside the list is dropped because a notification carries no reply channel. One legacy listener remains so a page rolled to 1.3 and a composer still speaking ain-handoff/v1 interoperate for this one snippet version: ain:prefill and ain:run alias to ui/notifications/tool-input, and ain:getMandate has no MCP twin at all because the spec defines no Host-to-View state pull.

7
Step 7 · Tool call

The host drives the tool

Inside a View the same form filling works through notifications. ui/notifications/tool-input brings the arguments, the page computes in the tab, and ui/notifications/tool-result returns the answer.

AI hostproxies the tool call the pagecomputes in the tab ui/notifications/tool-input arguments applied to the form ui/notifications/tool-result structuredContent holds the mandate

A result whose structuredContent looks like a Policy Mandate goes through the same intake routine as a dropped .policy.json file. Anything else is reported in the notice bar. A cancellation arrives as ui/notifications/tool-cancelled.

The arguments ride the notification at most once, and a host may send ui/notifications/tool-input-partial zero or more times before it. The page exposes the legs as AINBridge.mcp, so a page script never hand-rolls an envelope. Text sent with ui/message lands in the host's chat transcript, so the shipped helper puts identifiers only in that channel and never the mandate body, because the body can carry a user's own inputs.

Who asks at this stepan agent inside a host
Score this counterparty with the customer risk rating tool. entityName "Apex Clearing Corp", domicile "US". Return the band and the policy mandate.
8
Step 8 · Claims

The claims this page makes

Everything above is bounded. The boundary is part of the design.

Part three · The receipt in your ledger

The receipt outlives the session

An exported Policy Mandate carries an execution_hash. The bridge can hand that receipt to an open ledger tab and the ledger records it live.

9
Step 9 · Live receipt

A receipt can reach the ledger live

The transport is a BroadcastChannel named ain-ledger-v1. A message on it only ever reaches a tab on the same origin, so the channel carries nothing across an origin boundary.

tool tabainumbers.co tool page ledger tabopen at ainumbers.co/ledger/ BroadcastChannel 'ain-ledger-v1' ui/notifications/receipt validated like a #a=v1 fragment ui/notifications/receipt-ack ack added

The page waits 2000 ms for the acknowledgement. With no answer the send stays fire-and-forget and nothing on the page changes. The receipt is stored in IndexedDB with execution_hash as the key.

Both directions are JSON-RPC 2.0 notifications in the same ui/ dialect the postMessage leg carries, and neither carries an id. The params are {v: 1, receipt} on the way out and {v: 1, execution_hash, status} on the way back, where status is added, duplicate or rejected. Validation reuses the same extractArtifact and isArtifact pair a #a=v1 fragment goes through, and a receipt that fails it acks rejected. The channel carries a receipt only, never a mandate body. The addressing rule lives in ainLedgerTarget: the origin must be https or http, the hostname must be exactly ainumbers.co, localhost or 127.0.0.1, and the target becomes <origin>/ledger/. The ledger at the ledger.ainumbers.co subdomain therefore never receives a live receipt and keeps its fragment-only intake.

Who asks at this stepafter a run
Send the mandate you just built to my open ledger tab and tell me the ack status.
10
Step 10 · Duplicate

A duplicate is already recorded

A receipt's ledger identity is its execution_hash. Sending the same receipt twice records it once.

tool tabsends the receipt again ledger tabexecution_hash already in the store ui/notifications/receipt ack duplicate ui/notifications/receipt-ack Already recorded: sha256:9f2c…

One decision function sits behind every ingress path. It returns duplicate when the store already holds the hash and added when it does not. The stored record is never rewritten by a duplicate.

On a duplicate the ledger still answers, still renders and still highlights the row the receipt resolved to, so the person watching the tab sees where their receipt lives. A record view or the evidence tab is never yanked out from under the reader. The list view refreshes and marks the row. A browser without BroadcastChannel keeps exactly the fragment-only intake the ledger had before this leg existed.

🔒 All inputs are processed locally in your browser. No data is transmitted. Do not enter real personal data — use synthetic or anonymised inputs only.