Make your site voice-accessible. Serve a static manifest and handle actions — on your server or directly in the browser — and your users can interact with your service entirely through natural conversation.
A WWWAND-compatible site exposes a manifest under the /.wwwand/ path. When a user asks Wanda about a registered site, the AI reads the manifest to understand what's possible, then executes the appropriate action. Actions can be handled in two ways: server mode (default) POSTs to your server's action endpoint, while client mode relays the action to your site's open browser tab via a lightweight script — no server required.
Returns a JSON description of your site's identity, version compatibility, and available actions. Must be publicly accessible — no auth required.
{
"wwwand": {
"standard": "wwwand-alpha",
"version": "0.1.18",
"minClientVersion": "0.1.18",
"actionMode": "server" // "server" (default) | "client" — see Client-Side Actions below
},
"site": {
"name": "My App",
"description": "A brief description Wanda reads to understand context",
"url": "https://myapp.com"
},
"auth": {
"required": true,
"type": "bearer" // "bearer" | "apiKey" | "none"
},
"intents": {
"media": {
"itemTerms": ["product", "item"],
"actions": {
"search": "search",
"add": "addToCart"
}
}
},
"actions": [
{
"name": "search",
"description": "Search for products by keyword",
"type": "read", // "read" | "write"
"params": [
{ "name": "query", "type": "string", "required": true, "description": "Search term" },
{ "name": "limit", "type": "number", "required": false, "description": "Max results" }
]
},
{
"name": "addToCart",
"description": "Add a product to the user's cart",
"type": "write",
"params": [
{ "name": "productId", "type": "string", "required": true, "description": "Product identifier" }
]
}
]
}
| Field | Required | Description |
|---|---|---|
wwwand.standard | req | Must be "wwwand-alpha" — identifies this as a WWWAND-compatible manifest |
wwwand.version | req | Your implementation's semver version |
wwwand.minClientVersion | req | Minimum WWWAND client version required to use this site |
wwwand.actionMode | opt | "server" (default) or "client" — "client" routes actions to the site's open browser tab instead of POSTing to a server endpoint |
wwwand.handleUndispatched | opt | Set to true for client-mode sites that want to receive user text when the AI doesn't include a dispatch block. Requires "actionMode": "client". See Intent Resolution below. |
site.name | req | Human-readable site name |
site.description | req | Concise description — Wanda uses this to decide when your site is relevant |
site.url | req | Canonical URL of your site — must match the URL the user registers in WWWAND settings |
auth.required | opt | Whether requests need a user token |
auth.type | opt | "bearer", "apiKey", or "none" |
actions[] | req | Array of available actions. At least one required. |
intents | opt | Declares how user intents map to your actions. Wanda expands these into the AI prompt so it knows which action to dispatch for common requests (e.g. "stop" → stopSong). See Intent Resolution below. |
intents.<category>.itemTerms | opt | Array of terms your site uses for its items (e.g. ["song", "track"]). Used by client-side intent matching in onUndispatched. |
intents.<category>.actions | opt | Maps intent words to action names (e.g. { "play": "playSong", "stop": "stopSong" }). Expanded into the LLM prompt so it dispatches the right action. |
When actionMode is "server" (the default), Wanda POSTs action requests to this endpoint. The JSON body contains the action name and parameters extracted from the conversation. Note: the userText parameter (the user's original words) is only available in client mode — server endpoints receive { action, params }. For client-side alternatives, see Client-Side Actions below.
// Request body { "action": "search", "params": { "query": "wireless headphones", "limit": 3 } } // Success response { "success": true, "result": { // Any JSON — Wanda reads this and summarises it for the user "items": [ { "id": "p1", "name": "Sony WH-1000XM5", "price": "$349" } ] }, "message": "Found 1 result" // Optional human-readable summary } // Error response { "success": false, "error": "Product not found" }
If your manifest declares auth.required: true, users will be prompted to enter a token when they register your site in WWWAND. That token is stored securely in their data2link account and passed with every server-mode action request. Client-mode sites handle authentication in the browser — the data is already local.
// bearer token — passed as Authorization header Authorization: Bearer <user_token> // apiKey — passed as X-API-Key header X-API-Key: <user_token>
Your /.wwwand/manifest.json endpoint must allow cross-origin requests. In server mode, the /.wwwand/action endpoint also needs CORS. Client-mode sites only need CORS on the manifest — actions are relayed through the browser, not via HTTP.
// Server mode — manifest + action endpoint Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Key // Client mode — manifest only (actions go through the browser) Access-Control-Allow-Origin: *
A complete Node.js / Express implementation of a server-mode WWWAND-compatible endpoint:
const express = require('express') const app = express() app.use(express.json()) const CORS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-Key' } app.options('/.wwwand/*', (req, res) => res.set(CORS).sendStatus(204)) app.get('/.wwwand/manifest.json', (req, res) => { res.set(CORS).json({ wwwand: { standard: 'wwwand-alpha', version: '0.1.18', minClientVersion: '0.1.18', actionMode: 'server' }, site: { name: 'My App', description: 'Does something useful', url: 'https://myapp.com' }, auth: { required: false }, actions: [ { name: 'getStatus', description: 'Get current app status', type: 'read', params: [] } ] }) }) app.post('/.wwwand/action', (req, res) => { const { action, params } = req.body if (action === 'getStatus') { res.set(CORS).json({ success: true, result: { status: 'All systems operational' } }) } else { res.set(CORS).status(400).json({ success: false, error: `Unknown action: ${action}` }) } }) app.listen(3000)
For static sites, client-only apps, or anything where data lives in the browser (IndexedDB, localStorage, playback state), WWWAND can relay actions directly to your open browser tab instead of POSTing to a server.
Your site includes a small client script. When Wanda calls an action, WWWAND relays it to your tab via a cross-origin bridge, your JavaScript handles it, and the result flows back to Wanda. No server, no WebSocket infrastructure, no backend at all.
Add "actionMode": "client" to your manifest's wwwand block. The manifest itself is still a static JSON file served via GET — only the action execution changes.
{
"wwwand": {
"standard": "wwwand-alpha",
"version": "0.1.18",
"minClientVersion": "0.1.18",
"actionMode": "client", // "client" = browser tab handles actions
"handleUndispatched": true // opt in to receive user text when AI skips the dispatch block
},
"site": { "name": "My Music", "description": "A music player with browser-local songs", "url": "https://mymusic.example" },
"intents": {
"media": {
"itemTerms": ["song", "track"],
"actions": {
"play": "playSong",
"stop": "stopSong",
"pause": "pauseSong",
"resume": "resumeSong",
"next": "nextSong",
"previous": "previousSong",
"shuffle": "shuffleSongs"
}
}
},
"actions": [
{ "name": "listSongs", "description": "List all songs in the library", "type": "read", "params": [] },
{ "name": "playSong", "description": "Play a song by ID", "type": "write", "params": [{ "name": "id", "type": "string", "required": true }] },
{ "name": "stopSong", "description": "Stop playback", "type": "write", "params": [] },
{ "name": "pauseSong", "description": "Pause playback", "type": "write", "params": [] },
{ "name": "resumeSong", "description": "Resume playback", "type": "write", "params": [] },
{ "name": "nextSong", "description": "Skip to next song", "type": "write", "params": [] },
{ "name": "previousSong", "description": "Go back to previous song", "type": "write", "params": [] },
{ "name": "shuffleSongs", "description": "Shuffle the song order", "type": "write", "params": [] }
]
}
Add this script tag to your site. It connects your tab to WWWAND automatically — no configuration needed.
<script src="https://wwwand.com/wwwand-client.js"></script>
Handle actions in JavaScript. Return any JSON-serializable value — the client script wraps it in the standard { success, result } response format automatically. Throw an error to signal failure. Async handlers are supported.
<script> WwwandClient.onAction(async (action, params, userText) => { if (action === 'listSongs') { const songs = await getAllSongsFromIndexedDB() return { songs } } if (action === 'playSong') { // userText lets you correct dispatches — e.g. if the AI sent "play" // but the user actually said "next", use userText to detect that: if (/\bnext\b/i.test(userText)) return nextSong() if (/\bprevious\b/i.test(userText)) return previousSong() playSong(params.id) return { ok: true } } throw new Error(`Unknown action: ${action}`) }) </script>
That's it. The manifest (static JSON) handles discovery, the client script handles execution. No server, no infrastructure — any HTML page can become WWWAND-compatible.
| Method | Description |
|---|---|
onAction(handler) | Register a handler called when WWWAND dispatches an action. Receives (action, params, userText). The userText parameter contains the user's original words — useful for dispatch correction (see Intent Resolution below). Return a result object or throw an error. Async handlers are supported. |
onUndispatched(handler) | Register a handler called when the AI didn't include a dispatch block but your site opted in via handleUndispatched: true. Receives (userText) — the user's raw words. Return a result object if your site handled the intent, or null/undefined to decline. See Intent Resolution below. |
setSiteUrl(url) | Override the site URL used for matching. Defaults to window.location.origin + window.location.pathname (trailing slashes stripped). Call this if your manifest URL differs from the page origin — e.g. deploying to /app/ but registering https://mysite.com. |
audioControl(command) | Control audio that WWWAND is playing on your behalf via the autoplay fallback. command is "stop", "pause", or "resume". See Autoplay Fallback below. |
WWWAND uses a three-layer system to match user intent to the right action. Understanding how they interact is key to building responsive client-mode sites.
When you declare intents in your manifest, Wanda expands them into the AI system prompt. This tells the LLM which action to dispatch for common requests — e.g. "stop" → stopSong. The itemTerms field tells the system what words users use for your items (e.g. ["song", "track"]) — these are available for client-side matching but also help the LLM understand context.
Sometimes the LLM decides not to dispatch any action (e.g. the user said something ambiguous or chatty). When your manifest includes "handleUndispatched": true, Wanda sends the raw userText to your client via onUndispatched(). Your handler can match against your own declared itemTerms and intents, then take action directly. Return a result object if you handled it, or null to decline.
// Match against your declared intents when the AI didn't dispatch WwwandClient.onUndispatched((userText) => { const lower = userText.toLowerCase() const terms = ['song', 'track'] if (terms.some(t => lower.includes(t)) && /\b(play|start)\b/.test(lower)) { return playFirstSong() // return a result → handled } if (/\b(stop|halt)\b/.test(lower)) { return stopPlayback() // handled } return null // not handled — other sites may try })
When the LLM does dispatch an action, it might pick the wrong one — e.g. the user said "next" but the LLM dispatched playSong. The userText parameter in onAction() lets you detect and correct these mismatches client-side.
Browser autoplay policies can block audio from site tabs. WWWAND solves this by playing audio itself — the WWWAND tab already has audio permissions from the user's microphone interaction. When your onAction handler returns { audioBlocked: true, dataUrl: "..." }, Wanda plays the audio in its own tab and your site controls playback via WwwandClient.audioControl().
Your onAction handler returns a result with audioBlocked: true and a dataUrl containing the audio data (base64-encoded). Wanda creates an Audio element and plays it. Your site can then send audioControl("stop" | "pause" | "resume") to control playback.
When using the autoplay fallback, include these fields in your action result:
| Field | Type | Description |
|---|---|---|
audioBlocked | boolean | Set to true to signal that your site can't play audio and Wanda should play it instead. |
dataUrl | string | A data: URL (or blob: URL) containing the audio to play. Required when audioBlocked is true. |
name | string | Human-readable name of the track (e.g. "Stairway to Heaven"). Used in Wanda's spoken response. |
id | string | Track identifier. Returned in the cleaned-up result for reference. |
stopped | boolean | Return true when your stop action succeeds. |
paused | boolean | Return true when your pause action succeeds. |
resumed | boolean | Return true when your resume action succeeds. |
rewound | boolean | Return true when your rewind/restart action succeeds. |
shuffled | boolean | Return true when your shuffle action succeeds. |
// Example: play action with autoplay fallback WwwandClient.onAction(async (action, params, userText) => { if (action === 'playSong') { const song = getSong(params.id) try { await playInBrowser(song) // try playing in the site tab return { ok: true } } catch (e) { if (e.name === 'NotAllowedError') { // Autoplay blocked — hand off to WWWAND return { audioBlocked: true, dataUrl: song.dataUrl, name: song.title, id: song.id } } throw e } } if (action === 'stopSong') { // If WWWAND is playing on our behalf, tell it to stop WwwandClient.audioControl('stop') return { stopped: true } } })