BYOA Download

Use your visitors' own AI

A site reaches the visitor's agent wallet in one of two ways: through the browser extension's window.byoa, or with a bringyourownagent:// link, after which the wallet connects to your server itself. Both end in the same consent, limits and activity log, and a site allowed one way is allowed the other way too.

The samples are short and complete, and follow the real code of the agent wallet 0.1; the whole design is in docs/wallet-0.1.md. The wallet 0.1 serves two skills: speech to text and text answers.

The manifest

The site says what it needs and why, and names no vendor. The format is the draft rules; the libraries validate it, and the wallet checks every call against it.

The wallet 0.1 has two skills. audio.transcribe takes a WAV clip (16 kHz, mono, 16-bit PCM, up to 15 seconds) as base64, the parameter is the language, the output is { text }. The wallet measures the clip itself; it never trusts a length the page states. text.generate takes a list of chat messages and optional parameters such as maxTokens and choices, and returns { text }. It needs a text source that the visitor already runs (LM Studio, Ollama) or pays for (an OpenRouter key); without one, a site that requires it gets no Allow. The Twenty Questions game at twenty-questions.appshub.net uses it.

The manifest of the voice geography game
{
  "byoa": "0.1-draft",
  "purpose": {
    "summary": "Family geography game: recognise country names spoken aloud",
    "limits": { "maxClipSeconds": 15, "maxCallsPerMinute": 40 }
  },
  "skills": {
    "audio.transcribe": { "languages": ["en", "ru", "sk"], "required": true }
  },
  "privacy": "personal-voice",
  "expectedCost": { "perSession": "0.05 USD" }
}
Check it with the libraries (Node.js; Python and .NET have the same call)sdk/
// npm install bringyourownagent   (the same library: PyPI bringyourownagent, NuGet BringYourOwnAgent)
import { validateManifest } from 'bringyourownagent';

const { ok, errors } = validateManifest(MANIFEST);
if (!ok) console.error(errors);            // [{ code, path, message }]: stable codes, JSON Pointer paths

With the extension: window.byoa

  • The extension adds window.byoa to every https page (and to http://localhost for tests) and fires byoa:ready. Without the extension window.byoa is simply missing, and your page goes its own way.
  • Call request(manifest) from a click handler. Without a click the wallet shows nothing and answers null, unless the visitor let it decide on its own for your site.
  • null from request and { ok: false } from call are the only “no”: declined, blocked, no fitting model, no answer in 120 seconds, no wallet running. Your page cannot tell them apart, and that is intended.
  • The wallet answers request only when its models are loaded, so the first call is as fast as the others; on a fresh computer that can take some seconds. Show that you are waiting.
  • The answer is user input: show it as text, never as HTML, and never let a score, prize or payment depend on it.
In the pagewallet/extension/page.js
// In the page. The extension adds window.byoa at document_start and fires "byoa:ready".
const MANIFEST = { /* the manifest above */ };
let grant = null;

document.querySelector('#voice').addEventListener('click', async () => {
  if (!window.byoa) return useOwnMode();          // no extension: your own path
  grant = await window.byoa.request(MANIFEST);    // in the click handler; the wallet asks the visitor
  if (!grant) return useOwnMode();                // "AI unavailable": declined, blocked, no model, no wallet
  startListening();                               // grant: { grantId, skills, expiresAt }
});

// One clip: a 16 kHz mono 16-bit PCM WAV, up to 15 s, as base64. Resolves with the text, or null.
async function transcribe(wavBase64, language) {
  const r = await window.byoa.call(grant.grantId, 'audio.transcribe',
    { audio: { mime: 'audio/wav', base64: wavBase64 } },
    { language });                                // 'ru', 'sk', 'en', ...
  if (!r.ok) return null;                         // unavailable for this call: use your own path
  return r.output.text;                           // user input: show it as text, never as HTML
}

window.addEventListener('pagehide', () => { if (grant) window.byoa.release(grant.grantId); });
A helper: microphone samples to the WAV the skill takes
// Float32 samples at 16 kHz (from an AudioWorklet, or resampled with an OfflineAudioContext)
// -> base64 of a 16-bit PCM mono WAV, the input of audio.transcribe
function wavBase64(samples) {
  const v = new DataView(new ArrayBuffer(44 + samples.length * 2));
  const tag = (at, text) => [...text].forEach((ch, i) => v.setUint8(at + i, ch.charCodeAt(0)));
  tag(0, 'RIFF'); v.setUint32(4, 36 + samples.length * 2, true); tag(8, 'WAVE');
  tag(12, 'fmt '); v.setUint32(16, 16, true); v.setUint16(20, 1, true); v.setUint16(22, 1, true);
  v.setUint32(24, 16000, true); v.setUint32(28, 32000, true); v.setUint16(32, 2, true); v.setUint16(34, 16, true);
  tag(36, 'data'); v.setUint32(40, samples.length * 2, true);
  samples.forEach((x, i) => v.setInt16(44 + i * 2, Math.max(-1, Math.min(1, x)) * 0x7fff, true));
  let bin = '';
  new Uint8Array(v.buffer).forEach((b) => { bin += String.fromCharCode(b); });
  return btoa(bin);
}

On the visitor's computer

How the wallet 0.1 for Windows catches both ways in. Nothing here is needed to build a site; it is here so that you can see what runs on the visitor's side and why it is safe to rely on.

A link starts the wallet

The installer registers the bringyourownagent scheme for the current Windows user only. The scheme is never byoa://, a generic name another program could take: on Windows the last program to register a scheme gets its links, and a session code is meant for the wallet alone.

Registry keyswallet/install.ps1
# Written by wallet\install.ps1 for the current Windows user only (HKCU), removed by uninstall.ps1
HKCU\Software\Classes\bringyourownagent
    (Default)       URL:Bring Your Own Agent link
    URL Protocol    (empty)
HKCU\Software\Classes\bringyourownagent\shell\open\command
    (Default)       "%LOCALAPPDATA%\BYOA\wallet-app\Byoa.Wallet.exe" --url "%1"
HKCU\Software\Google\Chrome\NativeMessagingHosts\org.bringyourownagent.wallet
    (Default)       %LOCALAPPDATA%\BYOA\wallet-app\org.bringyourownagent.wallet.json
HKCU\Software\Chromium\NativeMessagingHosts\org.bringyourownagent.wallet
    (Default)       the same file

One wallet at a time

A second Byoa.Wallet.exe, started by a link while the wallet runs, finds the first one by a mutex and hands the link over the wallet's named pipe, then exits. The pipe is per user and opened with CurrentUserOnly on both ends; each message is a 4-byte little-endian length and UTF-8 JSON.

Abridged from Program.csProgram.cs
// Byoa.Wallet.exe --url "<link>": one wallet per user and data folder; a second one hands the link over
var url = Array.IndexOf(args, "--url");
if (url >= 0) LinkArgument = url + 1 < args.Length ? args[url + 1] : "";
using var instance = new Mutex(true, WalletPaths.MutexName(), out var first);
Background = args.Contains("--background") || LinkArgument is not null;
if (!first)
{
    // the running wallet takes the link over its per-user pipe (CurrentUserOnly), and this process ends
    if (LinkArgument is not null) LinkHandOff.Send(WalletPaths.PipeName(), LinkArgument);
    else if (!Background) AskRunningWalletToOpen();
    return 0;
}

The extension talks to a native host

Chrome starts byoa-host.exe for the extension through native messaging and lets only the extension whose ID is in allowed_origins use it; the ID is fixed by the key in the extension's manifest. The host relays messages to the wallet's pipe and starts the wallet for a request; it decides nothing. The page's origin comes from Chrome, never from the page.

The host manifest, written by the installerbyoa-host
{
  "name": "org.bringyourownagent.wallet",
  "description": "BYOA agent wallet: lets pages use your own AI within your limits",
  "path": "C:\\Users\\you\\AppData\\Local\\BYOA\\wallet-app\\byoa-host.exe",
  "type": "stdio",
  "allowed_origins": ["chrome-extension://niobopdoiopjilmlonjcgllpmgjemhpk/"]
}

Checks before anything else

  • Only a connect link of version 0.1, with no fragment and no repeated parameter. The endpoint must be wss:// with a public DNS name: no IP address in any spelling, no localhost, no .local, .home.arpa, .internal or .lan, no single-label name.
  • The manifest must be base64url without padding, at most 4096 bytes, a JSON object, and valid under the draft rules (the .NET library). The session code: 22 to 256 URL-safe characters.
  • A link that fails a check, repeats a recent code or comes from a blocked site does nothing but leave an entry in Activity. At most 4 links are handled at once.
  • After Allow, the wallet resolves the host itself, refuses private, loopback, link-local and other special addresses, and connects to the address it checked: no proxy, no cookies, no redirects.

Rules for sites

  • Ask only after the visitor clicks something; otherwise the wallet stays silent and answers null.
  • Always keep your own way: no wallet, a decline, a block and a failed call look the same, and your page must work without the visitor's AI.
  • Treat every answer as something the visitor typed. A visitor can connect any model, or a script, to the wallet.
  • State your purpose honestly and ask only for what you use: the visitor reads it in the consent window, and a site that asks for more later gets a new consent showing only the difference.
  • Sites need no key, no account and no payment to use BYOA; the visitor pays their own provider, or nothing for a model on their own computer.

Questions and ideas: GitHub issues or the Ideas page. Try it as a visitor: get the wallet and open geo.appshub.net.