Appearance
Free token pool (merido/free)
The free token pool lets operators provision a shared set of free-tier provider accounts (Groq, Gemini, Cerebras, DeepSeek, …) and expose them to every user in the org under the single model name merido/free — no per-user provider setup required.
Glossary
Several merido concepts use overlapping words ("key", "pool", "provider"). Keep them straight:
| Term | What it is |
|---|---|
| Pool source | An upstream provider account (Groq, Gemini, DeepSeek, …) the operator marks as feeding the free pool via the Providers tab or the HTTP API. Distinct from your own Providers — pool-source accounts are admin-managed and shared across all users. |
| Starter key | The one-time, org-scoped gateway (proxy) API key a user receives at signup, pre-configured for merido/free. It consumes the pool — the opposite end from a pool source. |
merido/free (system VM) | The system virtual model the operator configures in the Virtual models tab. Its account picker is restricted to pool-source accounts. |
System account (pool_source) | The internal flag marking an account row as a pool source. Pool-source accounts appear in the system VM editor's account picker and are shown with a Pool source badge in the Providers list. |
| Outbound proxy | A network-egress proxy (HTTP/SOCKS) configured under Outbound proxies. Unrelated to the free token pool, despite the historical "proxy pool" name. |
How it works
- The operator marks one or more provider accounts as pool sources in the Providers tab.
- The operator configures the
merido/freesystem virtual model in the Virtual models tab — choosing strategy, targets (model + pool-source account), and enable/disable. - Any user calls
model: "merido/free"and gets served by the pool. - A user who has their own org-level virtual model also named
merido/freeis served by their model instead — giving power users a clean override path.
Discovery (/v1/models)
merido/free appears in GET /v1/models (and GET /api/models) only when a call would actually be accepted for the requesting key — so a coding CLI that builds its model list from the endpoint shows the free pool exactly when it can use it. The listing reuses the same gates as dispatch, so it never advertises a model that would then be rejected:
- The pool is configured — an enabled
merido/freesystem VM with at least one pool-source account backing its target providers (otherwise it is omitted instead of 503-ing on use). - The caller passes every per-request gate: the email-verification gate (when email is configured and required), the global daily cap, and the per-org token windows. If any gate would reject the call (403 / 429),
merido/freeis omitted from that caller's list. - An org that defines its own
merido/freevirtual model sees that one (its override wins, as in dispatch); the system pool is not listed twice.
Step 1 — Mark pool sources (Providers tab)
In the dashboard Providers tab (admin view), each account row shows a "Use for Free pool" button. Clicking it marks that account as a pool source; the button changes to "Remove from Free pool", and a Pool source badge appears on the row.
This works for both kinds of upstream account:
- API-key connections (Groq, Gemini, DeepSeek, …) — always eligible.
- Sign-in / subscription accounts (Claude, Codex/ChatGPT, Gemini CLI, Qwen, Kiro, …) — eligible when the provider can actually route via sign-in. The toggle is hidden for sign-in accounts whose provider has no OAuth inference profile (it would be marked but never route). Because a subscription token is shared with every free-pool user, enabling it pops a confirmation warning: heavy shared use can exhaust your plan's limits and may violate the provider's terms of service for personal accounts — only enrol a plan you're comfortable sharing. The background token-refresh scheduler keeps enrolled sign-in accounts fresh automatically, the same as any other connected account.
You can also mark accounts via the HTTP API (kind-agnostic — pass any account id):
http
POST /api/system/accounts/{id}/pool-source
Content-Type: application/json
{ "pool_source": true }Repeat for every account you want to feed the pool. Marked accounts appear in the system VM editor's account picker (see Step 2).
Step 2 — Configure the system VM (Virtual tab)
In the dashboard Virtual models tab (admin view), an admin-only section at the bottom lists system virtual models, including merido/free. Click Edit to open the inline editor.
The system VM editor is identical to the regular VM editor except the account picker only shows pool-source accounts (those marked in Step 1). Choose:
- Strategy —
cost_optimized,failover,load_balance,weighted,latency_based, orsticky_*variants. - Targets — one or more
provider/modelentries, each optionally pinned to a specific pool-source account. An unpinned target lets the router choose any enabled pool-source account for that provider. - Enabled — toggle the VM on or off without touching its configuration.
Changes are saved immediately and take effect on the next request.
You can also update the system VM directly via the HTTP API:
http
PUT /api/system/virtual-models/{id}
Content-Type: application/json
{
"name": "merido/free",
"strategy": "cost_optimized",
"targets": [
{ "model": "groq/llama-3.3-70b-versatile", "account_id": 7 },
{ "model": "deepseek/deepseek-chat", "account_id": 12 }
],
"enabled": true
}Step 3 — Set a global daily token cap (optional but recommended)
Prevent runaway consumption with an environment variable (this is the default; you can also edit the cap at runtime from the operator console — see below):
MERIDO_FREE_POOL_GLOBAL_DAILY_TOKEN_CAP=500000This is a UTC-day token budget shared across the entire pool. Once the counter reaches the cap, every subsequent request to merido/free receives HTTP 429 until the counter resets at midnight UTC. The cap is off (unlimited) when the variable is unset.
The global cap is best-effort, not transactional. The pool-wide counter is checked before a request and incremented after it completes, so under concurrent load several in-flight requests can each pass the pre-flight check before any of them have booked their usage. The cap can therefore overshoot by roughly the size of the concurrent burst — set it as a soft guardrail against runaway spend, not as a hard ceiling you can rely on to the exact token.
See Environment variables for this knob and MERIDO_FREE_POOL_VM.
User override
If a user's org already has a virtual model whose name matches merido/free, merido serves that model instead of the system one. This lets individual users substitute their own provider keys — e.g. a higher-rate paid Groq account — without disrupting other users.
Response headers
Every response routed through the pool includes transparency headers:
| Header | Description |
|---|---|
X-Routed-Via | The provider/model that actually served the request (e.g. groq/llama-3.3-70b-versatile). |
X-Fallback-Attempts | Number of targets tried before a successful response. 0 means the first target served it. |
Per-org token quota
In addition to the pool-wide daily cap, operators can set per-org token limits (enforced per org, shared across all of that org's keys — see Why per-org) on rolling hourly, daily, and weekly windows. The environment variables below set the defaults; each can be overridden at runtime from the operator console without a restart (see Operator console):
| Variable (default) | Window | Default |
|---|---|---|
MERIDO_FREE_TIER_TOKEN_LIMIT_HOUR | rolling 60 minutes | unset (no per-user hourly cap) |
MERIDO_FREE_TIER_TOKEN_LIMIT_DAY | rolling 24 hours | unset (no per-user daily cap) |
MERIDO_FREE_TIER_TOKEN_LIMIT_WEEK | rolling 7 days | unset (no per-user weekly cap) |
When unset (and not overridden in the console), no per-user quota is enforced for that window; only the pool-wide MERIDO_FREE_POOL_GLOBAL_DAILY_TOKEN_CAP (Phase 1) applies.
Why per-org (anti-sybil)
Quota is tracked at the org level. All API keys belonging to the same user share a single counter for each window. Creating additional API keys does not multiply a user's free allowance.
Note: Per-user quota only applies to requests that carry an org — i.e. org-scoped keys in hosted (multi-tenant) mode. A key with no org (legacy or system-wide keys, and all keys in single-user/local mode) is not subject to per-user windows; such requests are still bounded by the pool-wide global daily cap. Provision org-scoped keys for users you want metered per-user.
Enforcement: most-restrictive wins
Before serving a request to merido/free, merido checks every configured window. If the user's usage for any set window has reached or exceeded its limit, the request is rejected immediately with HTTP 429:
json
{
"error": {
"message": "Free tier quota reached. Wait for the window to reset, attach your own provider key, or upgrade.",
"type": "rate_limit_error",
"retry_after_ms": 1847000
}
}The retry_after_ms field (and the standard Retry-After response header, in seconds) point to the soonest window reset — the window that will free up capacity first. The client can use this to schedule an exact retry rather than back off blindly.
Interaction with the global daily cap
Per-user quotas and the pool-wide global cap are independent guards; both can trigger a 429. A user who has not exhausted their personal quota is still blocked for the remainder of the UTC day if the global cap for the entire pool is reached, and vice versa.
Single-user / local mode
Per-user quota enforcement requires a multi-tenant deployment where requests carry an org identity. In single-user local mode (no org attached to the request), per-user quota checks are skipped entirely. The pool-wide MERIDO_FREE_POOL_GLOBAL_DAILY_TOKEN_CAP remains the only token guard in that mode.
Recommended starting values
A balanced starting point for a hosted deployment:
MERIDO_FREE_TIER_TOKEN_LIMIT_HOUR=200000
MERIDO_FREE_TIER_TOKEN_LIMIT_DAY=1000000
MERIDO_FREE_TIER_TOKEN_LIMIT_WEEK=5000000Tune these based on the number of users and the capacity of your provider pool. The hourly limit is the most effective guard against a single user monopolising the pool in a burst; the weekly limit provides a soft ceiling for sustained heavy users.
Onboarding (Phase 3a)
Verify-email gate
Set MERIDO_FREE_TIER_REQUIRES_VERIFIED_EMAIL=true (default) to require that the calling key's org has at least one verified email address — either a user whose home org it is, or a verified member of the org (via an invite) — before merido/free is accessible. When the gate fires, merido returns HTTP 403:
json
{
"error": {
"type": "permission_error",
"message": "Verify your email to use the free pool (merido/free).",
"param": null,
"code": "email_unverified"
}
}The envelope is OpenAI-compatible: type is the standard category and the free-pool-specific slug lives in code. The user must complete email verification (the link arrives at signup or via a re-send from the dashboard). Once verified, subsequent requests proceed normally.
The gate is only enforced when email is configured. It needs a working email channel (
MERIDO_RESEND_API_KEY+MERIDO_EMAIL_FROM) so users can actually verify. If email is not configured, the gate is automatically skipped (the pool stays open to everyone) rather than locking it with a verification nobody can complete — the server logs a warning at startup so you know the gate you requested isn't being enforced.Verification links also need
MERIDO_PUBLIC_URL(it forms the link target). If email is configured butMERIDO_PUBLIC_URLis unset, links can't be sent, users can never satisfy the gate, and the free pool becomes unreachable — the server warns about this case too. So to actually enforce the gate, set all three; to leave the pool open, either disable the gate (below) or leave email unconfigured.
Set MERIDO_FREE_TIER_REQUIRES_VERIFIED_EMAIL=false to disable the gate — useful for self-hosted deployments where you control user onboarding through other means.
This gate applies only in multi-tenant mode. In single-user local mode it is ignored.
Starter API key on signup
POST /api/auth/signup now returns a one-time api_key field alongside the normal session response:
json
{
"token": "...",
"api_key": "md-..."
}This is an org-scoped proxy key pre-configured for merido/free. The user can paste it into their coding CLI immediately after signing up (and after verifying their email if the gate is on). The key is shown once and cannot be retrieved later — if it is lost, the user can create a new key from the dashboard API Keys page.
Quota endpoint
Authenticated users (session cookie or bearer token — dashboard context, not the proxy key) can query their current free-pool usage across all configured windows:
http
GET /api/free-pool/quota
Authorization: Bearer <session-token>Response — multi-tenant, org present:
json
{
"applicable": true,
"hour": { "limit": 200000, "used": 42000, "remaining": 158000, "resets_at": 1750000000000 },
"day": { "limit": 1000000, "used": 42000, "remaining": 958000, "resets_at": 1750000000000 },
"week": { "limit": 5000000, "used": 42000, "remaining": 4958000, "resets_at": 1750000000000 }
}Each window object contains:
| Field | Type | Description |
|---|---|---|
limit | number | null | Token cap for the window (null if unset / unlimited). |
used | number | Tokens consumed in the current rolling window. |
remaining | number | null | limit - used, or null if there is no limit. |
resets_at | number | Epoch milliseconds at the window's fixed bucket boundary — (bucket + 1) × window_ms — when the counter rolls over to zero. |
Response — single-user mode or no org:
json
{ "applicable": false }Rich 429 body
When a per-user quota window is exceeded, merido returns HTTP 429 with a structured error body and a standard Retry-After header:
http
HTTP/1.1 429 Too Many Requests
Retry-After: 1847json
{
"error": {
"type": "insufficient_quota",
"message": "Free tier quota reached. Wait for the window to reset, attach your own provider key, or upgrade.",
"param": null,
"code": "free_pool_quota_exceeded",
"retry_after_ms": 1847000,
"resets_at": 1750000000000
}
}The envelope is OpenAI-compatible: type is the standard category (insufficient_quota), the free-pool-specific slug is in code (free_pool_quota_exceeded), and retry_after_ms / resets_at are extension fields OpenAI SDKs ignore but the dashboard reads.
retry_after_ms and resets_at point to the soonest window reset across all configured windows — so the client knows the earliest time it can retry successfully.
Operator console
Admins manage free-pool limits at /app/admin/free-pool (admin-only — non-admins receive HTTP 403).
The page shows the limit controls and today's usage. The operator's actions are:
- Edit the limit knobs — the per-user hour/day/week limits, the global daily cap, and the verified-email gate are all editable in the console and apply at runtime — no restart.
Pool sources and the merido/free virtual model are managed in the Providers and Virtual models tabs respectively (see Steps 1 and 2 above).
The limit knobs are not read-only: the env vars (MERIDO_FREE_TIER_*, MERIDO_FREE_POOL_GLOBAL_DAILY_TOKEN_CAP, MERIDO_FREE_TIER_REQUIRES_VERIFIED_EMAIL) supply the defaults, and any value you set in the console is stored as a per-instance override that takes precedence and takes effect on the next request. Clearing a console override reverts that knob to its env default.
Management endpoints
All routes below require an admin session (session cookie or Authorization: Bearer <admin-token>). Non-admins receive HTTP 403.
http
GET /api/system/pool # Pool state: sources list + active VM snapshot
POST /api/system/accounts/{id}/pool-source # Mark/unmark a provider account as a pool source
# Body: { "pool_source": true | false }
GET /api/system/pool-sources # List pool-source accounts (for VM editor picker)
GET /api/system/virtual-models # List system VMs (including merido/free)
PUT /api/system/virtual-models/{id} # Update a system VM (strategy, targets, enabled)
PATCH /api/system/pool/limits # Edit limit overrides at runtime (no restart)PATCH /api/system/pool/limits takes a JSON body where every field is optional — absent fields are left unchanged:
jsonc
{
"hour": 200000, // per-user hourly limit (positive integer, or null to clear)
"day": 1000000, // per-user daily limit
"week": 5000000, // per-user weekly limit
"global_daily_cap": 500000, // pool-wide daily cap
"requires_verified_email": true // email gate (boolean, or null to clear)
}- A present numeric value writes the override; it must be a positive integer (
>= 1) — zero, negatives, floats, and non-numbers are rejected with HTTP 400. - A present
nullclears that override, reverting the knob to its environment-variable default. requires_verified_emailis a boolean (ornullto clear); a non-boolean is rejected with HTTP 400.
The /api/system/virtual-models and /api/system/accounts endpoints are used by the dashboard VM editor and provider pool-source toggle respectively.
Per-user limits and the email gate only take effect in multi-tenant deployments. They are enforced per org, so in the default single-user/local profile (requests carry no org) editing the hour/day/week limits or the email gate has no effect even though the console accepts the values. The pool-wide global daily cap applies in every profile.
Admin: Health page
Admins have access to a dedicated Health page for the free pool at /app/admin/free-pool → Health tab (or GET /api/admin/free-pool/health). This view is cross-tenant: it aggregates across all orgs that have ever used the free pool, not just the admin's own org.
Key performance indicators exposed by the health endpoint:
| KPI | Description |
|---|---|
total_requests | Total requests served by the free pool (all time or the selected window). |
error_rate | Fraction of requests that ended in a provider error. |
avg_latency_ms | Mean end-to-end latency across all free-pool requests. |
active_users | Distinct org count that sent at least one free-pool request. |
top_providers | The provider accounts contributing the most traffic, ranked by request count. |
Because the health endpoint is cross-tenant, it never surfaces individual request bodies or user-identifiable content — only aggregated counters and provider-level breakdowns.
Admin: Body reveal (audited)
Request and response bodies for free-pool traffic are hidden by default — the admin Logs view (free pool scope) shows metadata only (model, status, tokens, latency). This protects user privacy: most requests contain private prompt content.
When an admin needs to inspect the actual content of a specific request — for abuse investigation, provider debugging, or compliance — they can reveal it via the "Reveal body (audited)" control in the log detail panel:
- Open the Logs page and switch to Free pool · all tenants scope (admin only).
- Click a log row to open the detail panel.
- Scroll to the Admin: Reveal body (audited) section at the bottom.
- Read the notice: "Revealing these bodies writes a tamper-evident entry to the audit trail."
- Click Reveal body (audited) — the (redacted) request and response bodies are fetched and displayed inline.
Every successful reveal:
- Calls
GET /api/admin/free-pool/logs/{request_id}/bodyon the backend. - The backend verifies that the row has
free_pool = truebefore returning bodies — a non-pool request ID always returns HTTP 404, even to admins. - Appends exactly one audit entry to the tamper-evident audit chain:
action = "free_pool.body.reveal"actor= the admin who triggered the revealtarget= therequest_id
- The audit entry is visible at Admin → Audit and is included in any audit export.
A 404 (non-pool row or unknown ID) writes no audit entry — an unsuccessful reveal attempt is not audited.
Via the API
http
GET /api/admin/free-pool/logs/{request_id}/body
Authorization: Bearer <admin-token>Non-admin callers receive HTTP 403. Non-pool rows (or unknown IDs) receive HTTP 404. A successful response has the same shape as GET /api/logs/{request_id}:
json
{
"detail": { "request_id": "...", "status": 200, ... },
"request_body": "{ \"model\": \"merido/free\", ... }",
"response_body": "{ \"choices\": [...] }",
"request_headers": { "content-type": "application/json" },
"response_headers": { "x-routed-via": "groq/llama-3.3-70b-versatile" }
}Secrets (authorization headers, API keys) are redacted by the same body_text helper used for per-org log detail.
Privacy boundary
The free-pool admin endpoints enforce a strict privacy boundary between free-pool traffic and regular tenant traffic:
- Admins see only free-pool traffic.
GET /api/admin/free-pool/logsreturns rows wherefree_pool = trueonly. The body-reveal endpoint (/body) additionally verifiesAND free_pool = 1at the SQL layer before returning any content — even if an admin somehow supplies a non-poolrequest_id, they receive HTTP 404. - Regular tenant traffic is never accessible via admin free-pool endpoints. The
free_pool = 1guard is enforced in storage (get_free_pool_log_bodyinFreePoolAdminStore) — not just in the handler — so a compromised or misconfigured handler cannot bypass it. - User-facing endpoints are org-scoped.
GET /api/logs,GET /api/usage, andGET /api/usage/splitall scope to the caller's ownorg_id. An admin using these endpoints sees only their own org's data, the same as any other user.
In summary: the admin free-pool scope is a narrow, audited window into cross-tenant free-pool traffic only. Admins do not gain access to other tenants' own-provider traffic.
User: Free pool vs your providers
Filter logs by pool source
The Logs page shows a three-state Pool segment control in the filter toolbar:
| Option | What you see |
|---|---|
| All (default) | All your requests, regardless of whether they used the free pool. |
| Free pool | Only requests served by the shared merido/free pool (operator-funded). |
| My providers | Only requests served by your own provider accounts. |
Select a value and click Apply. The filter is org-scoped — you always see only your own traffic. The control is available to all users (not admin-only).
Usage split (Cost page)
The Usage & Cost page shows a "Free pool vs your providers" split card whenever your org has any usage. It appears below the total spend hero card and shows:
- Free pool — request count and token totals for traffic served by
merido/free(operator-funded, billed to the operator's pool accounts, not to you). - Your providers — request count and token totals for traffic served by your own provider accounts (billed to you).
The split respects the selected time window (24 h / 7 d / 30 d / All time). It is powered by GET /api/usage/split?window=<window> (org-scoped, never cross-tenant):
http
GET /api/usage/split?window=7d
Authorization: Bearer <session-token>json
{
"free_pool": {
"bucket": "free_pool",
"requests": 142,
"prompt_tokens": 284000,
"completion_tokens": 71000,
"charge_usd": 0.0,
"cost_usd": 0.0,
...
},
"own": {
"bucket": "own",
"requests": 58,
"prompt_tokens": 116000,
"completion_tokens": 29000,
"charge_usd": 0.87,
"cost_usd": 0.82,
...
},
"window": "7d"
}The split card is hidden when both sides have zero requests (no usage yet in the selected window).
Related
- Add providers & keys — add and manage provider accounts.
- Virtual models & fallback — strategies, circuit breaker, and fallback chain.
- Environment variables —
MERIDO_FREE_POOL_VM,MERIDO_FREE_POOL_GLOBAL_DAILY_TOKEN_CAP,MERIDO_FREE_TIER_TOKEN_LIMIT_HOUR,MERIDO_FREE_TIER_TOKEN_LIMIT_DAY,MERIDO_FREE_TIER_TOKEN_LIMIT_WEEK,MERIDO_FREE_TIER_REQUIRES_VERIFIED_EMAIL.