Skip to content

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:

TermWhat it is
Pool sourceAn 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 keyThe 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 proxyA network-egress proxy (HTTP/SOCKS) configured under Outbound proxies. Unrelated to the free token pool, despite the historical "proxy pool" name.

How it works

  1. The operator marks one or more provider accounts as pool sources in the Providers tab.
  2. The operator configures the merido/free system virtual model in the Virtual models tab — choosing strategy, targets (model + pool-source account), and enable/disable.
  3. Any user calls model: "merido/free" and gets served by the pool.
  4. A user who has their own org-level virtual model also named merido/free is 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/free system 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/free is omitted from that caller's list.
  • An org that defines its own merido/free virtual 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:

  • Strategycost_optimized, failover, load_balance, weighted, latency_based, or sticky_* variants.
  • Targets — one or more provider/model entries, 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
}

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=500000

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

HeaderDescription
X-Routed-ViaThe provider/model that actually served the request (e.g. groq/llama-3.3-70b-versatile).
X-Fallback-AttemptsNumber 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)WindowDefault
MERIDO_FREE_TIER_TOKEN_LIMIT_HOURrolling 60 minutesunset (no per-user hourly cap)
MERIDO_FREE_TIER_TOKEN_LIMIT_DAYrolling 24 hoursunset (no per-user daily cap)
MERIDO_FREE_TIER_TOKEN_LIMIT_WEEKrolling 7 daysunset (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.

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=5000000

Tune 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 but MERIDO_PUBLIC_URL is 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:

FieldTypeDescription
limitnumber | nullToken cap for the window (null if unset / unlimited).
usednumberTokens consumed in the current rolling window.
remainingnumber | nulllimit - used, or null if there is no limit.
resets_atnumberEpoch 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: 1847
json
{
  "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 null clears that override, reverting the knob to its environment-variable default.
  • requires_verified_email is a boolean (or null to 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-poolHealth 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:

KPIDescription
total_requestsTotal requests served by the free pool (all time or the selected window).
error_rateFraction of requests that ended in a provider error.
avg_latency_msMean end-to-end latency across all free-pool requests.
active_usersDistinct org count that sent at least one free-pool request.
top_providersThe 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:

  1. Open the Logs page and switch to Free pool · all tenants scope (admin only).
  2. Click a log row to open the detail panel.
  3. Scroll to the Admin: Reveal body (audited) section at the bottom.
  4. Read the notice: "Revealing these bodies writes a tamper-evident entry to the audit trail."
  5. 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}/body on the backend.
  • The backend verifies that the row has free_pool = true before 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 reveal
    • target = the request_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/logs returns rows where free_pool = true only. The body-reveal endpoint (/body) additionally verifies AND free_pool = 1 at the SQL layer before returning any content — even if an admin somehow supplies a non-pool request_id, they receive HTTP 404.
  • Regular tenant traffic is never accessible via admin free-pool endpoints. The free_pool = 1 guard is enforced in storage (get_free_pool_log_body in FreePoolAdminStore) — 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, and GET /api/usage/split all scope to the caller's own org_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:

OptionWhat you see
All (default)All your requests, regardless of whether they used the free pool.
Free poolOnly requests served by the shared merido/free pool (operator-funded).
My providersOnly 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).

  • Add providers & keys — add and manage provider accounts.
  • Virtual models & fallback — strategies, circuit breaker, and fallback chain.
  • Environment variablesMERIDO_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.

© merido. All rights reserved.