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.
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.
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>:
<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>fetch(), no XMLHttpRequest and no WebSocket call.localStorage, sessionStorage or cookie writes anywhere in the intake path..value and .checked only, never through innerHTML.A prefill deep-link carries a JSON object of {elementId: value} pairs, encoded as base64url, in the URL fragment #in=. Adding &run=1 opts into auto-execution after the fill.
The encoder is standard base64url per RFC 4648 section 5. The + character maps to -, the / character maps to _ and the padding is stripped.
Field values land with .value for text, select and textarea inputs and with .checked for checkboxes and radios. The bridge dispatches input and change events after each fill so reactive pages update. A {fields:{…}} wrapper around the payload is also accepted. Two live helpers sit below this paragraph. The builder encodes fields into a deep-link. The decoder turns a pasted fragment back into JSON.
Type a tool URL and a fields object. The builder emits the link in real time.
Paste a full #in= fragment or the bare base64url token to read it back as JSON.
·
Open https://ainumbers.co/tools/110-customer-risk-rating.html#in=eyJlbnRpdHlOYW1lIjoiQXBleCBDbGVhcmluZyBDb3JwIiwiZG9taWNpbGUiOiJVUyJ9 and tell me the resulting risk band.
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:
{
"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…"
}payload keys are element ids in the receiving page.source_tool_inputs carries the sending page's own inputs for the audit trail.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 member | What the shipped code does |
|---|---|
| version | The string "1.3". |
| capabilities | A 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. |
| mcp | The 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.
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.
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'].
resourceDomains names the Google Fonts domains fonts.googleapis.com and fonts.gstatic.com. Those are the pages' only lawful egress.connectDomains is emitted empty. Pages make zero network calls, and the emitted value must never widen.A View opens with one request, ui/initialize. The answer pins the host's origin and every later message is addressed to it.
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.
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.
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.
Score this counterparty with the customer risk rating tool. entityName "Apex Clearing Corp", domicile "US". Return the band and the policy mandate.
Everything above is bounded. The boundary is part of the design.
connect-src 'none'. Only the two Google Fonts domains are declared, in the CSP and in the worker's resourceDomains.ainumbers.co. The ledger subdomain keeps fragment-only intake. Step 9 gives the rule from the code.ain-handoff/v1 composer types remain as an alias for one snippet version. A later row deletes them.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.
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.
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.
Send the mandate you just built to my open ledger tab and tell me the ack status.
A receipt's ledger identity is its execution_hash. Sending the same receipt twice records it once.
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.