Skip to Content
TensorPass
Protocol

Wallet provider

The window.aiwallet provider that AI Wallet places in every page, its discovery events, its four methods, the events a chat streams, errors with their statuses and link notices, written so another wallet or SDK can implement it.

A page talks to the wallet through one object, window.aiwallet. This page specifies that object as AI Wallet implements it and as @aiconnect/core uses it. A wallet that implements it works with sites built on the SDK, and an SDK that follows it works with AI Wallet.

The protocol carries no version number. The surface below is the whole of it in this release.

Discovery

window.aiwallet

The wallet defines window.aiwallet in the page's own JavaScript world before the page's scripts run. AI Wallet does this from a content script that runs at document_start in the page's main world, in the top frame of each tab. It defines the property with Object.defineProperty with writable: false and configurable: false, and freezes the object. A second wallet in the same browser therefore cannot replace it.

AI Wallet runs on pages served over HTTPS, and on http://localhost and http://127.0.0.1 at any port.

aiwallet:announce

The wallet dispatches a CustomEvent named aiwallet:announce on window, with the provider object as its detail. It dispatches it once when it defines the provider, and again each time the page dispatches aiwallet:request.

aiwallet:request

A script that loads after the wallet's announce asks for it again by dispatching a plain Event named aiwallet:request on window.

How the SDK finds the wallet

@aiconnect/core looks for a wallet this way:

  1. When window.aiwallet exists and its isAIWallet is true, it uses it.
  2. Otherwise it listens for aiwallet:announce, dispatches aiwallet:request, and takes the detail of the first announce.
  3. When no announce arrives within 300 milliseconds, it concludes that no wallet is installed.
function findWallet(): Promise<AiWalletProvider | null> {
  if (window.aiwallet?.isAIWallet) {
    return Promise.resolve(window.aiwallet);
  }
  return new Promise((resolve) => {
    const onAnnounce = (event: Event) => {
      window.removeEventListener('aiwallet:announce', onAnnounce);
      resolve((event as CustomEvent<AiWalletProvider>).detail);
    };
    window.addEventListener('aiwallet:announce', onAnnounce);
    window.dispatchEvent(new Event('aiwallet:request'));
    setTimeout(() => {
      window.removeEventListener('aiwallet:announce', onAnnounce);
      resolve(null);
    }, 300);
  });
}

The provider object

interface AiWalletProvider {
  isAIWallet: true;
  // AI Wallet sets 'AI Wallet'. The SDK does not read it.
  name: string;
  request(
    method: string, params?: unknown, onEvent?: (event: ChatEvent) => void, signal?: AbortSignal,
  ): Promise<unknown>;
  // Called when the visitor changes, in the wallet, the account or model this site answers with.
  onLink(listener: (link: WalletLink) => void): () => void;
}
  • isAIWallet. The value true. The SDK checks it before it uses window.aiwallet.
  • request(method, params, onEvent, signal). Sends one request and resolves with its result. onEvent receives the events a chat streams: who answers, then its text (see Streaming events). Aborting signal rejects the request at once with the signal's reason, and the wallet stops the chat it runs for it.
  • onLink(listener). Adds a listener for link notices and returns a function that removes it.

Shared types

The methods exchange the types of @aiconnect/core, documented in the core reference.

type WalletLink =
  | { connected: false }
  | { connected: true; providerName: string; model?: string };

type ChatEvent =
  | { kind: 'start'; providerName: string; model: string }
  | { kind: 'text'; delta: string };

type ChatMessage = SystemMessage | UserMessage | AssistantMessage | ToolMessage;

interface UserMessage {
  role: 'user';
  // An image is a data: URL, or an https URL for the providers that fetch images themselves.
  content: string | ({ type: 'text'; text: string } | { type: 'image'; url: string })[];
}

interface AssistantMessage {
  role: 'assistant';
  content: string;
  toolCalls?: { id: string; name: string; arguments: Record<string, unknown> }[];
}

interface ToolDefinition {
  name: string;
  description: string;
  inputSchema: Record<string, unknown>;
}

interface GenerationOptions {
  maxTokens?: number;
  temperature?: number;
  topP?: number;
  stop?: string[];
  toolChoice?: 'auto' | 'none' | 'required' | { name: string };
  responseFormat?: { type: 'json'; schema?: Record<string, unknown>; name?: string; strict?: boolean };
}

interface ChatResponse {
  message: AssistantMessage;
  finishReason: 'stop' | 'length' | 'toolCalls' | 'contentFilter';
  // The model that answered, as the provider named it.
  model: string;
  // Absent when the provider reports no token counts. inputTokens includes cachedInputTokens.
  usage?: { inputTokens: number; outputTokens: number; cachedInputTokens: number };
}

In a connected WalletLink, providerName is the display name of the account that answers the site, such as ChatGPT or Claude, and Auto when the wallet picks the account for each answer. model is the default model of that account, and is absent with Auto.

Methods

MethodParamsResultAsks the visitor
statusnoneWalletLinkNo
connectnoneWalletLinkThe first time, and to unlock
disconnectnone{ connected: false }No
chat{ messages, tools, options }ChatResponseTo unlock

status

Tells the page whether its origin may use the visitor's AI, without any window or prompt. The SDK calls it when it starts, so a site the visitor approved earlier connects on its own.

const link = await window.aiwallet.request('status');
// { connected: true, providerName: 'ChatGPT', model: 'gpt-6-luna' }

The result is { connected: false } when the origin is not approved, and also while the wallet is locked. A page that reads connected: false because of a lock receives a link notice when the visitor unlocks the wallet.

connect

Asks for the visitor's approval of the origin, then returns its link.

  1. A wallet that is not set up yet rejects with "AI Wallet is not set up yet. Open it from the browser toolbar."
  2. A locked wallet opens its unlock window. When the visitor closes it without unlocking, the request rejects with "AI Wallet is locked. Unlock it to continue." Requests from several pages at the same moment share one unlock window.
  3. An origin the visitor already approved gets its link at once.
  4. Any other origin makes the wallet open its approval window, which shows the origin's host and the account that will answer it. Declining or closing the window rejects with "The connection was declined". A wallet that holds no account yet asks the visitor to connect one first, and its Approve button stays disabled.
  5. On approval the wallet saves the origin with the visitor's default account, and the request resolves with the link.
try {
  const link = await window.aiwallet.request('connect');
} catch (error) {
  // 'The connection was declined', or the reason the wallet could not ask.
}

disconnect

Removes the origin from the visitor's approved sites, sends a link notice of { connected: false } to the origin's open pages, and resolves with { connected: false }. The next connect asks the visitor again. On a locked wallet the request rejects with "The wallet is locked".

await window.aiwallet.request('disconnect');

chat

Runs one model call on the account the origin answers with, and resolves with the answer: the assistant message, why it ended, the model that answered and the token counts.

const message = await window.aiwallet.request(
  'chat',
  {
    messages: [
      { role: 'system', content: 'You recommend books from this site\'s catalog.' },
      { role: 'user', content: 'A calm book for tonight' },
    ],
    tools: [{ name: 'search_books', description: 'Find books by mood.', inputSchema: { type: 'object' } }],
    options: { toolChoice: 'auto' },
  },
  (event) => console.log(event.kind === 'start' ? `${event.providerName} on ${event.model}` : event.delta),
);
// {
//   message: { role: 'assistant', content: '', toolCalls: [{ id: '…', name: 'search_books', arguments: { mood: 'calm' } }] },
//   finishReason: 'toolCalls',
//   model: 'gpt-6-luna',
//   usage: { inputTokens: 412, outputTokens: 21, cachedInputTokens: 0 },
// }
  • params.messages is the whole conversation. The wallet keeps no conversation between calls.
  • params.tools lists the tools the model may call on this step. Without it, the model can only answer in text.
  • params.options holds what the page asks of the answer. The tool choice and the response format always reach the provider. The token limit, temperature, top_p and stop sequences are left out for the accounts that refuse them, as the AI SDK guide lists.
  • The result's message.toolCalls lists the tools the model asked for. The page runs them and calls chat again with tool messages that answer each call's id. One chat request is one model call; the loop around it belongs to the page.
  • A locked wallet opens its unlock window first, as in connect, and the chat goes on once the visitor unlocks.
  • An origin the visitor has not approved gets "This site is not connected to AI Wallet", with status 401.
  • A wallet with no account to answer gets "AI Wallet has no AI account connected", with status 403.

The account that answers is the one the visitor picked for the origin, on the model picked for the origin when there is one, and on the account's default model otherwise. With Auto, the wallet tries its accounts in turn, as the providers page describes.

Streaming events

During a chat, the wallet calls onEvent with each event of the answer, in order, before the promise settles.

  1. One start event names who answers: providerName is the display name of the account, such as Claude, and model is the model the wallet asks that account for. The wallet sends it right before the first text, or right before the result when the answer has no text, which happens on a step that only calls tools. A chat that fails sends no start.
  2. A text event carries each piece of the answer's text, in order. A step that only calls tools may stream no text at all.
{ kind: 'start', providerName: 'Claude', model: 'claude-haiku-5-5' }
{ kind: 'text', delta: 'Piranesi suits ' }
{ kind: 'text', delta: 'a calm evening.' }

With Auto, the wallet picks the account when the answer starts, so the start event is how a page learns which account and model answer: AiConnect.fetch writes that model into the first event of an AI SDK's stream, such as Anthropic's message_start. Every piece of text comes from the account the start names. The wallet moves to the next account only while nothing has streamed, and it sends start once the account that answers is known; an account that failed before it gets no start.

The model of the result is the model as the provider named it, which can be more precise than the start event's, such as a dated version.

Errors

A failed request rejects with a WalletRequestError, exported by @aiconnect/core. Its message is an English sentence meant for the visitor, and its status follows HTTP, set where the failure happens. AiConnect.fetch answers the AI SDKs with that status, so each SDK raises its own error class.

StatusMessageWhen
400A chat request carries its messageschat without messages.
401This site is not connected to AI Walletchat from an origin that is not approved.
403The connection was declinedThe visitor declined the origin, or closed the approval window.
403AI Wallet is not set up yet. Open it from the browser toolbar.connect or chat on a wallet that has no vault.
403AI Wallet is locked. Unlock it to continue.The visitor closed the unlock window without unlocking.
403The wallet is lockeddisconnect on a locked wallet.
403AI Wallet has no AI account connectedchat while the origin's account is gone, or with Auto and no account.
404AI Wallet has no method named methodA method outside the four above.
410AI Wallet was updated. Reload this page to use it again.The wallet was updated or reloaded after the page opened.
the provider'sProvider answered status: detailThe provider refused the call, for example with 429 when a plan is spent.
499The answer was stopped from AI WalletThe visitor pressed Stop in the wallet's side panel.
502The reason the provider could not be reachedA network failure between the wallet and the provider.

After a wallet update

When Chrome updates or reloads the extension, the content scripts of the pages already open lose their connection to the wallet. AI Wallet's content script then answers every request at once with "AI Wallet was updated. Reload this page to use it again." Show that message to the visitor. A reload injects the new provider.

The wallet tells a page when what its origin answers with changes. The page receives the new WalletLink through the listeners it added with onLink.

const stop = window.aiwallet.onLink((link) => {
  console.log(link.connected ? `Now answered by ${link.providerName}` : 'This site was switched off');
});

AI Wallet sends a notice to every open tab whose origin is approved, each time the vault's contents change or the wallet unlocks. The changes include the visitor picking another account or model for a site, adding or removing an account, and changing the default account. A site the visitor switches off, from the wallet or through disconnect, gets a last notice of { connected: false } in its open tabs. A notice can repeat the link a page already holds. The wallet sends nothing when it locks.

Origin binding

The wallet identifies the site by its origin: scheme, host and port. AI Wallet reads the origin from the sender of the runtime port its content script opens, which Chrome sets from the page the content script runs in. Nothing the page sends names the origin, so a page cannot ask on behalf of another site.

Approvals belong to one origin. https://example.com and https://shop.example.com are two sites, each approved on its own.

Inside AI Wallet

This part is informative. Another wallet may carry requests any way it likes, as long as the page sees the surface above.

  1. The provider in the page posts each request to its own window as { source: 'aiwallet-page', request: { id, method, params } }, with a random id. An aborted request posts { source: 'aiwallet-page', cancel: id }.
  2. AI Wallet's content script, in the extension's isolated world, forwards the request on a runtime port named aiwallet-page to the background. A request that arrives after the port closed, for example after the background slept, opens a new port.
  3. The background answers on the port with events that carry the same id: { id, kind: 'start', providerName, model } once, then { id, kind: 'text', delta } for streamed text, then one { id, kind: 'result', result } or { id, kind: 'error', message, status }. A cancel stops the chat the background runs for that id.
  4. The content script posts each event back to the page as { source: 'aiwallet-content', event }, and each link notice as { source: 'aiwallet-content', notice: { kind: 'link', link } }.

Implementing a wallet

A wallet that sites built on @aiconnect/core can use does the following:

  1. Defines window.aiwallet with isAIWallet: true, request and onLink before the page's scripts run, and dispatches aiwallet:announce with the provider as detail, again on each aiwallet:request.
  2. Binds every request to the page's origin as the browser reports it.
  3. Answers status without any prompt, with the origin's link.
  4. On connect, asks the visitor before an origin's first use, keeps the approval, and lets the visitor revoke it.
  5. On chat, accepts the chat format above with tools and options, calls the model on the visitor's account, sends one start event through onEvent before the first text or the result, streams the text through it, and resolves with a ChatResponse whose tool calls carry stable ids and parsed arguments. AiConnect.fetch fails a stream whose answer never names who answers.
  6. Rejects failures with an error whose message the site can show to the visitor and whose status follows the table above.
  7. Calls the onLink listeners of the origin's open pages when the account or model the origin answers with changes, or when the origin loses its approval.
  8. Keeps credentials out of the page. The page receives links, text and answers.

On this page