# Wallet provider (/docs/protocol/wallet-provider)

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 [#discovery]

### window.aiwallet [#windowaiwallet]

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 [#aiwalletannounce]

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 [#aiwalletrequest]

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 [#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.

```ts
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 [#the-provider-object]

```ts
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](#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 [#shared-types]

The methods exchange the types of `@aiconnect/core`, documented in the [core reference](/docs/reference/core#chat-types).

```ts
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 [#methods]

| Method | Params | Result | Asks the visitor |
| --- | --- | --- | --- |
| `status` | none | `WalletLink` | No |
| `connect` | none | `WalletLink` | The first time, and to unlock |
| `disconnect` | none | `{ connected: false }` | No |
| `chat` | `{ messages, tools, options }` | `ChatResponse` | To unlock |

### status [#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.

```ts
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 [#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.

```ts
try {
  const link = await window.aiwallet.request('connect');
} catch (error) {
  // 'The connection was declined', or the reason the wallet could not ask.
}
```

### disconnect [#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".

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

### chat [#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.

```ts
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](/docs/guides/existing-sdks#what-the-wallet-takes-from-a-request) 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](/docs/providers#auto) describes.

## Streaming events [#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.

```ts
{ 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 [#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.

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

### After a wallet update [#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.

## Link notices [#link-notices]

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`.

```ts
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 [#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 [#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 [#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.
