# Build a minimal demo

Run a one-call Flovia demo with a fixed mock reply, then publish a demo without exposing your key.

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

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

Save this as `demo.mjs`:

```js
// 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

Run it in mock mode first. No key is needed:

```bash
node demo.mjs
```

With 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:

```bash
export FLOVIA_KEY=<KEY>
FLOVIA_DEMO_MODE=live node demo.mjs
```

It 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

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

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](/usage.md) for how they work.

Flovia is in alpha, and its limits can change.

## Terms

Your demo must follow the [Flovia Terms](https://flovia.dev/terms). That includes the credit and redistribution conditions of the sources behind any data you show.
