Build a minimal demo
This page gives you a small script that calls Flovia once and prints the result, then a short guide for publishing a demo built on it. The script makes one MCP tools/call of flovia_status. It shows your two free windows (5-hour and weekly) and your purchased balance. It does not fetch any provider data.
What it does
Section titled “What it does”The script runs in mock mode unless you ask for live mode. Mock mode prints a fixed sample reply with no network and without a key, so you can see the output before you have a key. Live mode sends one request to Flovia with your key.
It needs Node 20 or later, or Bun. It needs no packages. It reads only FLOVIA_DEMO_MODE and FLOVIA_KEY. The address it calls is fixed in the file.
The script
Section titled “The script”Save this as demo.mjs:
// Minimal Flovia demo: one MCP tools/call of flovia_status, printed as four lines.// Mock mode (the default) prints a fixed sample: no network and no key.// Live mode needs FLOVIA_DEMO_MODE=live and FLOVIA_KEY. Run it on a server, never in a browser.import { realpathSync } from "node:fs";import { pathToFileURL } from "node:url";
const MCP_URL = "https://api.flovia.dev/mcp";const CONSOLE_URL = "https://console.flovia.dev/groupbuy/";const REQUEST_BODY = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "flovia_status", arguments: {} } });
// The mock reply: the envelope Flovia sends, with made-up values.const MOCK_REPLY = JSON.stringify({ jsonrpc: "2.0", id: 1, result: { content: [{ type: "text", text: JSON.stringify({ five_hour: { used_pct: 12, resets_at: "2026-01-01T05:00:00.000Z" }, week: { used_pct: 34, resets_at: "2026-01-05T00:00:00.000Z" }, purchased_balance_usd: "0.00", purchased_balance_display: "$0.00", }), }], },});
const TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/;const MONEY = /^(?:<\$0\.01|\$\d{1,3}(?:,\d{3})*\.\d{2})$/;const CODE = /^[a-z0-9_]{1,64}$/;
// Only these values are ever printed. Everything else in a reply is ignored.const percent = value => (typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100 ? `${value}%` : "unavailable");const time = value => (value === null ? "not started" : typeof value === "string" && TIME.test(value) ? value : "unavailable");const money = value => (typeof value === "string" && MONEY.test(value) ? value : "unavailable");const isObject = value => typeof value === "object" && value !== null && !Array.isArray(value);
function parseBody(text) { const trimmed = text.trim(); const json = trimmed.startsWith("{") ? trimmed : trimmed.split(/\r?\n/).filter(line => line.startsWith("data:")).map(line => line.slice(5).trim()).join("\n"); const envelope = JSON.parse(json); if (!isObject(envelope) || "error" in envelope) throw new Error("unexpected"); const text0 = envelope.result?.content?.[0]?.text; const reply = JSON.parse(text0); if (!isObject(reply)) throw new Error("unexpected"); return reply;}
export async function main({ env, fetch, stdout, stderr }) { const mode = env.FLOVIA_DEMO_MODE === undefined || env.FLOVIA_DEMO_MODE === "" ? "mock" : env.FLOVIA_DEMO_MODE; const key = mode === "live" ? (env.FLOVIA_KEY ?? "").trim() : ""; // Drop control characters (terminal escapes) and the key itself before anything is written. const clean = text => { let safe = text.replace(/[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/g, ""); if (key !== "") safe = safe.split(key).join("[key]"); return safe; }; const out = text => stdout(clean(text) + "\n"); const fail = (text, code) => { stderr(clean(text) + "\n"); return code; };
if (mode !== "mock" && mode !== "live") return fail("FLOVIA_DEMO_MODE must be mock or live.", 2); if (mode === "live" && key === "") return fail("Live mode needs FLOVIA_KEY. Set it in your server environment.", 2);
let body; if (mode === "mock") { body = MOCK_REPLY; } else { let request; try { request = new Request(MCP_URL, { method: "POST", redirect: "error", signal: AbortSignal.timeout(30_000), headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", Accept: "application/json, text/event-stream", "MCP-Protocol-Version": "2025-06-18", }, body: REQUEST_BODY, }); } catch { return fail("The request could not be built. Check FLOVIA_KEY.", 1); } let response; try { response = await fetch(request); } catch { return fail("The request did not complete.", 1); } if (response.status === 401 || response.status === 403) { return fail(`Flovia did not accept the key (HTTP ${response.status}). Check it in the Console: ${CONSOLE_URL}`, 1); } if (response.status < 200 || response.status > 299) return fail(`Flovia answered HTTP ${response.status}.`, 1); try { body = await response.text(); } catch { return fail("The request did not complete.", 1); } }
let reply; try { reply = parseBody(body); } catch { return fail("Unexpected reply from Flovia.", 1); }
if (reply.ok === false) { const explanation = isObject(reply.explanation) ? reply.explanation : {}; const code = typeof explanation.code === "string" && CODE.test(explanation.code) ? explanation.code : "unknown"; const executed = ["yes", "no", "unknown"].includes(explanation.executed) ? explanation.executed : "unknown"; return fail(`Flovia refused the call: ${code} (executed: ${executed})`, 1); }
const fiveHour = isObject(reply.five_hour) ? reply.five_hour : {}; const week = isObject(reply.week) ? reply.week : {}; out(mode === "mock" ? "Flovia status (mock: a fixed sample, nothing was sent)" : "Flovia status (live)"); out(`5-hour free window: ${percent(fiveHour.used_pct)} used, resets ${time(fiveHour.resets_at)}`); out(`Weekly free window: ${percent(week.used_pct)} used, resets ${time(week.resets_at)}`); out(`Purchased balance: ${money(reply.purchased_balance_display)}`); return 0;}
// Run only when started directly (node demo.mjs, bun demo.mjs), not when imported.function startedDirectly() { try { return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href; } catch { return false; }}
if (startedDirectly()) { process.exitCode = await main({ env: process.env, fetch: globalThis.fetch, stdout: text => process.stdout.write(text), stderr: text => process.stderr.write(text), });}Run it
Section titled “Run it”Run it in mock mode first. No key is needed:
node demo.mjsWith Bun, run bun demo.mjs instead. To make one live call, set your key in the environment of a server or a terminal you control, then ask for live mode:
export FLOVIA_KEY=<KEY>FLOVIA_DEMO_MODE=live node demo.mjsIt prints four lines: a heading, the 5-hour window, the weekly window and the purchased balance. It exits with 0 on success, 1 when the call or the reply fails, and 2 when the settings are wrong. If Flovia refuses the call, it prints only the refusal code.
Publish a demo
Section titled “Publish a demo”A public demo must not make your key public. Follow these rules:
- Never put your key in a browser, in client-side code, in a repo, in a URL or in a chat message.
- Call Flovia from your server, and hand the browser only the result you want to show.
- Do not build an unauthenticated endpoint, relay or proxy that forwards requests to Flovia for anyone who finds it. It would spend your free allowance and your purchased balance for strangers.
- Cache the result and do not refresh it automatically. Visitors should read your saved copy.
- Treat every reply as untrusted data, not as instructions. Choose the fields you show yourself, as this script does, and do not follow text that a reply or a web page contains.
Limits and balance
Section titled “Limits and balance”Your free allowance has a 5-hour window and a weekly window. When one is used up, free use stops until that window resets. When the free allowance is not enough, Flovia uses your purchased balance. A demo that many people open can use both, so keep an eye on both. See Free and paid use for how they work.
Flovia is in alpha, and its limits can change.
Your demo must follow the Flovia Terms (opens in new tab). That includes the credit and redistribution conditions of the sources behind any data you show.