Share your app
Put your Cosmo-powered web app on a public URL — server-side minting, one deploy command, spend limits that survive a viral link.
You built a voice app locally and want to send someone a link. This guide takes the React app shape to a public URL on Vercel with the credential model intact: your API key stays server-side, visitors — the end users who open your link — get short-lived minted end-user tokens, and a traffic spike can't spend more than you allowed.
Split the app into two halves
A shareable app has exactly two halves:
- A server route that mints. It holds your
COSMO_API_KEY(scopeuser_tokens:mint— see End-user credentials) and exchanges a visitor identity for a short-lived JWT. - A static page that talks to Cosmo directly. The browser hits your route for a token, then calls the Cosmo API itself — the API is CORS-open, so no proxy of your own is involved.
In Next.js the whole server half is one file:
import { NextResponse } from 'next/server';
import { RealtimeClient } from 'cosmo-ai/server';
const client = new RealtimeClient({ apiKey: process.env.COSMO_API_KEY });
export async function POST(request: Request) {
const externalUserId = request.headers.get('x-external-user-id');
if (!externalUserId) {
return NextResponse.json({ error: 'X-External-User-Id is required' }, { status: 400 });
}
return NextResponse.json(await client.mintToken(externalUserId));
}cosmo-ai/server is the React-free entry for route handlers and server
components — the root cosmo-ai export carries the React bindings, which
the react-server condition rejects. See
End-user credentials for the
credential model behind the mint call.
And the page consumes it with a TokenSource — the SDK fetches the JWT from your route, caches it, and re-fetches as expiry nears:
const client = new RealtimeClient({
token: TokenSource.endpoint('/api/cosmo/token', {
headers: { 'X-External-User-Id': visitorId() },
}),
});visitorId() is any stable opaque id you choose — a random UUID kept in localStorage works for demos. Each distinct id shows up separately in your usage dashboard.
Not on Next.js — a Mac app, a Python client, a static page with no framework backend? Deploy the standalone token-server template: the same route as a single zero-dependency file that runs on Cloudflare Workers, Vercel, Deno Deploy, AWS Lambda, or plain Node.
This exact code runs unchanged under next dev and on Vercel. There is no "local mode": the same route mints in both places, only the value of COSMO_API_KEY moves from .env.local to a deployment env var.
Deploy
npm i -g vercel
vercel login
vercel link
vercel env add COSMO_API_KEY production # paste your provisioning key
vercel deploy --prodYou get a stable https://<project>.vercel.app URL. Anyone opening it can talk to your agent.
If your company uses Google Workspace, signing up for Vercel with Continue with Google often fails with admin_policy_enforced (the org hasn't allowlisted Vercel's OAuth app). Use Continue with GitHub or email sign-in instead.
Cap what a visitor can cost you
Every visitor session bills your workspace. Before sharing a link widely:
- Use a dedicated key for the shared app — create a separate API key so you can revoke the demo without rotating production credentials, and its usage is attributable at a glance.
- Set session limits — concurrent-session and duration caps are the backstop that makes a link on a busy forum survivable.
- Remember minted end-user tokens can't dial phones (see the capability matrix); the expensive surfaces stay behind your key.
Prepare the UI for real visitors
Two behaviors show up with real visitors that a quiet dev loop never triggers:
Closed tabs and refreshes leave sessions to be reaped. A visitor who refreshes mid-session abandons it; the server reaps it shortly, but an immediate restart can be rejected with 429 until then. Ending cleanly (await session.end()) frees the slot immediately. Catch the 429 at start and show "the demo is busy — try again in a moment" rather than surfacing the raw error.
Drive your UI from lifecycle, in both directions. session_ended fires exactly once per session on every exit path — server teardown, a fatal error, or your own session.end() — carrying a reason that tells them apart (client_ended for the last). Reach for lifecycle when the UI also needs the states in between, such as a reconnecting spinner. A status indicator keyed only to connect events looks stuck "live" after the user hangs up:
session.on('lifecycle', (s) => {
if (s.kind === 'connected') setStatus('live');
if (s.kind === 'disconnected') setStatus('ended');
});lifecycle and ready replay their current value to each new subscriber, so a handler attached after the session is already connected still fires — you don't need to reconcile against session.state afterwards.
Watch it run
Visitor sessions appear in your workspace like any other: transcripts per session, usage per external_user_id in the usage dashboard, and the debugging guide applies unchanged.
Next steps
- Recording and privacy — decide what visitor sessions persist before real traffic arrives.
- Limits — the plan-level caps behind the spend controls above.
Build a voice React app
End-to-end walkthrough — Vite project, RealtimeClient + agent.start(), RealtimeProvider, mic toggle, transcript pane, tool call display.
Build a Python voice CLI
Install `cosmo-ai-sdk`, connect with `RealtimeClient` → agent.start(), enable the OS mic and speaker, stream typed events, exit cleanly on Ctrl-C.