diff --git a/README.md b/README.md
index 74d8b0e0bac..b64f3dc543f 100644
--- a/README.md
+++ b/README.md
@@ -17,9 +17,9 @@
-> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **442 free-tier entries across 34 recurring pool keys** and computes the token headline from the **16 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. The result stays visible on the dashboard (`/dashboard/free-tiers`).
+> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **444 free-tier entries across 34 recurring pool keys** and computes the token headline from the **16 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. Quotas that only open after a regional identity check (today: ModelScope) are shown apart, +~6M behind regional identity verification, and never summed into the headline. The result stays visible on the dashboard (`/dashboard/free-tiers`).
-
+
> Animated summary of the live `/dashboard/free-tiers` page. Full methodology (pool dedupe, credit tiers, provider terms): **[docs/reference/FREE_TIERS.md](docs/reference/FREE_TIERS.md)**.
>
@@ -648,7 +648,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
-> **352 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **442 per-model rows**, **34 recurring pools** and **52 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
+> **352 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **444 per-model rows**, **34 recurring pools** and **52 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
@@ -1307,7 +1307,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
Resilience Guide Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing
Auto-Combo Engine 16-factor scoring, mode packs, self-healing
Proxy Guide 3-level proxy system, 1proxy marketplace, registry CRUD
-
Free Tiers Consolidated directory: 34 documented recurring pools / 442 cataloged free-tier entries
+
Free Tiers Consolidated directory: 34 documented recurring pools / 444 cataloged free-tier entries
Features Gallery Visual dashboard tour with screenshots
Codebase Documentation Beginner-friendly codebase walkthrough
diff --git a/docs/diagrams/free-tier-budget.svg b/docs/diagrams/free-tier-budget.svg
index 19e0ca5f16b..f706e4824f5 100644
--- a/docs/diagrams/free-tier-budget.svg
+++ b/docs/diagrams/free-tier-budget.svg
@@ -1,4 +1,4 @@
-
+
Pool-deduplicated chart of the 16 recurring free-token pools with positive published budgets (plus Groq's five per-model caps as one segment), plus signup credits and uncapped providers shown separately.
@@ -64,7 +64,7 @@
~1.47B
FREE TOKENS / MONTH · STEADY
up to ~2.10B in your first month — signup credits
- documented free tiers · 34 recurring pools · 442 catalog entries · one endpoint
+ documented free tiers · 34 recurring pools · 444 catalog entries · one endpoint
diff --git a/docs/getting-started/FREE-TIERS-GUIDE.md b/docs/getting-started/FREE-TIERS-GUIDE.md
index 804707209d7..f93250470ab 100644
--- a/docs/getting-started/FREE-TIERS-GUIDE.md
+++ b/docs/getting-started/FREE-TIERS-GUIDE.md
@@ -1,6 +1,6 @@
# Free Tiers Guide: Understand and Combine Free AI Access
-> **TL;DR**: OmniRoute registers 352 provider IDs, with **152 provider-catalog entries marked `hasFree`**. The stricter audited free-model catalog covers **34 recurring pool keys / 442 entries** (435 active + 7 discontinued). Connect several suitable providers for broader fallback capacity; every quota, approval rule, privacy policy, and paid-overage condition still applies.
+> **TL;DR**: OmniRoute registers 352 provider IDs, with **152 provider-catalog entries marked `hasFree`**. The stricter audited free-model catalog covers **34 recurring pool keys / 444 entries** (437 active + 7 discontinued). Connect several suitable providers for broader fallback capacity; every quota, approval rule, privacy policy, and paid-overage condition still applies.
---
@@ -159,13 +159,13 @@ provider's quota or access policy.
The live, pool-deduplicated catalog currently reports:
-| Metric | Current audited value | Interpretation |
-| ---------------------------------------------------- | -----------------------------------------------: | ----------------------------------------------------------------------------------------- |
-| Recurring quantified grant | **~1.47B tokens/month** | Shared pools counted once; excludes uncapped providers from the sum |
-| First month with signup grants | **~2.10B tokens** | Recurring total plus one-time and recurring credits |
-| Audited free-model inventory | **34 recurring pool keys / 442 catalog entries** | 435 active + 7 discontinued; distinct from the 352-provider catalog |
-| Recurring/keyless free-forever providers represented | **52** | Unique providers across recurring daily/monthly/credit/uncapped and keyless catalog types |
-| Provider catalog entries marked `hasFree` | **152 / 352** | Broader provider metadata; not all have a quantifiable recurring quota |
+| Metric | Current audited value | Interpretation |
+| ---------------------------------------------------- | -----------------------------------------------: | -------------------------------------------------------------------------------------------------------------------------- |
+| Recurring quantified grant | **~1.47B tokens/month** | Shared pools counted once; excludes uncapped providers from the sum |
+| First month with signup grants | **~2.10B tokens** | Recurring total plus one-time and recurring credits |
+| Audited free-model inventory | **34 recurring pool keys / 444 catalog entries** | 437 active + 7 discontinued; distinct from the 352-provider catalog |
+| Recurring/keyless free-forever providers represented | **52** | Unique providers across recurring daily/monthly/credit/uncapped and keyless catalog types, eligibility-gated rows excluded |
+| Provider catalog entries marked `hasFree` | **152 / 352** | Broader provider metadata; not all have a quantifiable recurring quota |
These values are computed from `open-sse/config/freeModelCatalog.ts`; see the
[Free Tiers Reference](../reference/FREE_TIERS.md) for pool deduplication, ToS flags,
diff --git a/docs/reference/FREE_TIERS.md b/docs/reference/FREE_TIERS.md
index 394142ccd28..963bd3e421f 100644
--- a/docs/reference/FREE_TIERS.md
+++ b/docs/reference/FREE_TIERS.md
@@ -1,7 +1,7 @@
---
title: "Free Tiers & Free-Token Budget"
version: 3.8.50
-lastUpdated: 2026-09-02
+lastUpdated: 2026-09-03
---
# Free Tiers & Free-Token Budget
@@ -19,6 +19,7 @@ lastUpdated: 2026-09-02
| **+ first month with signup credits** | **~2.10B** | Steady + one-time signup credits (Together $25, Z.AI 20M, DeepSeek 5M, …), deduped per account. **First month only** — does not recur. |
| **+ permanently free, no published cap** | _un-quantifiable_ | `siliconflow`, `glm-cn` (GLM-4-Flash), `tencent`, `baidu`, `kilo-gateway`, `opencode-zen`, `gemini`, `ollama-cloud` — real recurring access, rate/concurrency-limited, **no token cap to count**. Listed, never summed (counting them at `RPM×24/7` is the inflation we reject). |
| **+ deposit-unlock boost** | **+~24M** | A one-time **$10** OpenRouter top-up raises its free pool from 50 → 1000 req/day. Reported separately so it never inflates the steady number. |
+| **+ behind a regional identity check** | **+~6M** | `modelscope` (Alibaba Cloud binding + mainland-China real-name verification). Real recurring quota, exposed as `gatedRecurringTokens` / `gatedProviders` and on the dashboard. Never summed into the headline: +~6M behind regional identity verification. |
| Theoretical ceiling (all rate limits, 24/7) | ~10B | Sum of every provider rate limit extrapolated to non-stop use. **Not a guarantee** — do not headline this. |
**Honest headline:** _OmniRoute aggregates **~1.47B documented free tokens per month** (up to ~2.10B in your first month with signup credits) across 34 free-tier pools — plus a long tail of permanently-free, no-cap providers — and RTK + Caveman compression (15–95% token savings) stretches that further._
@@ -78,10 +79,23 @@ purpose.
- Daily token cap → `monthly = daily × 30`. Only RPD documented → `RPD × ~800 output tokens × 30`. Only RPM/TPM (no daily cap) → **uncapped** (see below).
- **Permanently free, but no published token cap** (`siliconflow`, `glm-cn`, `tencent`, `baidu`, `kilo-gateway`, `opencode-zen`, `gemini`, `ollama-cloud`): these are real recurring free access, rate/concurrency-limited. We classify them `recurring-uncapped` and **never sum them** — multiplying `RPM × 24/7 × 30d` would produce a fantasy ceiling (the inflation we reject). They are listed so you know they exist.
- **Deposit-unlock boost:** a one-time small top-up that permanently raises a free quota (OpenRouter: $10 → 1000 req/day ≈ +24M/mo). Reported as a separate figure, kept out of the steady headline.
+- **Eligibility-gated quotas** (`eligibilityGate: "regional-identity"`): a real recurring quota that only opens after a region-bound identity check (mainland-China real-name verification today). Counted with the same pool-dedupe rule into a separate figure (`gatedRecurringTokens`), never into the steady headline. The regime (`freeType`) is unchanged, so routing is unchanged.
- **Evidence classes.** The rule: a number in the catalog cites its source in an `// evidence:` comment next to the entry — `public-page` (a provider page anyone can read), `api-public` (an unauthenticated endpoint of the provider, e.g. NaraRouter's public plans endpoint at router.bynara.id), or `console-verified por ` (the figure is only visible inside an account console; the comment records who saw it and when, and the public page that says the cap exists). The state today: the five blocks re-audited on 2026-09-02 carry it (`gemini`, `groq`, `mistral`, `ollama-cloud`, `nara`); entries that predate the 2026-09-02 re-audit inherit the earlier research until they are touched; any **new or changed** number without an evidence comment is a bug. Today only `mistral` is console-verified.
---
+## Why our number is smaller than other aggregators'
+
+Most "free tokens per month" figures in this space are sums of per-model labels. Ours is not, on purpose:
+
+- **Each shared pool is counted once.** Mistral's free plan is one 1B/month allowance per organization; listing it under five models does not make it 5B. Summed per model, our own catalog would read **~7.4B** (recomputed on 2026-09-03; this figure is not CI-gated — re-measure it whenever the catalog changes) — the headline says **~1.47B** because that is what one account of each provider can actually spend.
+- **Daily caps are converted, rates are not.** A documented tokens/day cap becomes `× 30`; a documented requests/day cap becomes `RPD × ~800 tokens × 30`; a provider that only publishes requests-per-minute has **no** monthly figure and is listed as _uncapped_, never summed. Multiplying a rate limit by 24/7 is the inflation we refuse.
+- **Quotas behind a regional identity check are shown apart** (`+~6M behind regional identity verification`), because most readers cannot use them.
+- **Signup credits are first-month only** and reported as a second figure, never blended into the steady number.
+- **The figure is enforced by CI.** `npm run check:docs-counts` recomputes the totals from the catalog and fails the build when this file, the README or the budget card drift from them.
+
+---
+
## ToS attention table
> **ToS flag is advisory, not a routing gate.** Providers marked `tos` are still included in routing and combo/fallback by default; the flag only surfaces on `/dashboard/free-tiers` and `/api/free-tier/summary`. The `excludeTosAvoid` query parameter affects the summary view only, not global routing. The verdict lives in `open-sse/config/freeTierCatalog.ts` (informational, not read by routing engines).
diff --git a/docs/screenshots/free-tier-budget-card.svg b/docs/screenshots/free-tier-budget-card.svg
index 90ab6b02794..15e11774a90 100644
--- a/docs/screenshots/free-tier-budget-card.svg
+++ b/docs/screenshots/free-tier-budget-card.svg
@@ -1,94 +1,104 @@
-
-
-
-OmniRoute · /dashboard/free-tiers · preview mockup
+
+
+
+OmniRoute · /dashboard/free-tiers · preview mockup
Monthly free-token budget
-20 free pools · 446 models · one endpoint
+21 free pools · 444 models · one endpoint
Steady / month
-~1.48B
+~1.47B
First month (+ signup credits)
~2.10B
ToS-flagged (you decide)
13 providers
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Each segment = one free pool · widths floored so every provider shows · honest numbers in the grid.
Mistral Large 3 1.00B
-GPT-4o mini 150M
+Agnes 2.0 Flash 210M
-Tencent Hy3 150M
+GPT-4o mini 150M
-Gemini 2.5 Flash 60M
+Llama 3.3 70B 30M
-Llama 3.3 70B 30M
+Grok-3 24M
-Grok-3 24M
+GPT-4o 7M
-DeepSeek V4 Pro 20M
+GPT-OSS 120B 6M
-GPT-4o 7M
+GPT-OSS 20B 6M
-MiniMax-M2.7 6M
+GPT-OSS Safeguard 20B 6M
-Arcee Trinity Large Prev 5M
+Qwen3.6 27B 6M
-NavyAI free pool 5M
+Qwen3.8 27B 6M
-Auto Free 4M
+MiniMax-M2.7 6M
-Auto 1M
+Arcee Trinity Large Prev 5M
-Command A Reasoning 800K
+NavyAI free pool 5M
-ERNIE 4.5 VL 424B A47B B 500K
+Auto Free 4M
-morph-v3-large 400K
+Auto 1M
-Llama 3.1 8B 200K
+Command A Reasoning 800K
-Claude Sonnet 4.5 25K
-
-+ First month: one-time signup credits (~626M)
-
-vertex 300M
-
-agentrouter 200M
-
-predibase 25M
-
-together 25M
-
-glm-cn 20M
-
-doubao 15M
-
-ai21 10M
-
-longcat 10M
+ERNIE 4.5 VL 424B A47B B 500K
+
+morph-v3-large 400K
+
+Llama 3.1 8B 200K
+
+Claude Sonnet 4.5 25K
+
++ First month: one-time signup credits (~626M)
-deepseek 5M
-
-Pool-deduped, honest counting — no inflated rate-limit ceilings. Some terms suggest personal-use only; we flag them so you decide.
-+ 12 permanently-free, no-cap providers (e.g. baidu, glm-cn, opencode-zen) · OpenRouter $10 → +24M/mo.
+vertex 300M
+
+agentrouter 200M
+
+predibase 25M
+
+together 25M
+
+glm-cn 20M
+
+doubao 15M
+
+ai21 10M
+
+longcat 10M
+
+deepseek 5M
+
+Pool-deduped, honest counting — no inflated rate-limit ceilings. Some terms suggest personal-use only; we flag them so you decide.
++ 15 permanently-free, no-cap providers (e.g. agnes, ainative, aion) · OpenRouter $10 → +24M/mo.
++ ~6M behind regional identity verification (modelscope) — real quota, never in the headline.
diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts
index 08b54e455df..c6b8b133a34 100644
--- a/open-sse/config/freeModelCatalog.data.ts
+++ b/open-sse/config/freeModelCatalog.data.ts
@@ -287,6 +287,14 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [
{ provider: "mistral", modelId: "mistral-small-latest", displayName: "Mistral Small 4", monthlyTokens: 1000000000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "mistral", tos: "caution" },
{ provider: "mistral", modelId: "devstral-latest", displayName: "Devstral 2", monthlyTokens: 1000000000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "mistral", tos: "caution" },
{ provider: "mistral", modelId: "codestral-latest", displayName: "Codestral", monthlyTokens: 1000000000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "mistral", tos: "caution" },
+ // evidence: public-page https://modelscope.cn/docs/model-service/API-Inference/limits and
+ // https://modelscope.cn/docs/magicube/intro (2026-09-02) — API-Inference is free; calls are paid with
+ // 魔粒: "注册并登录 200 魔粒/日" + "绑定阿里云账号 50 魔粒/日", 1 魔粒 per call on "主流" models ⇒ ~250 calls/day
+ // ⇒ 250 × 800 × 30 = 6M/month, one balance per account (single pool).
+ // eligibilityGate: "账号注册后需绑定阿里云账号,并且通过实名认证后才可使用" (Alibaba Cloud binding + mainland
+ // real-name verification). The docs also call the product "非商业化,非盈利" — hence tos: caution.
+ { provider: "modelscope", modelId: "Qwen/Qwen3.5-397B-A17B", displayName: "Qwen3.5 397B A17B (ModelScope)", monthlyTokens: 6000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "modelscope-free", tos: "caution", eligibilityGate: "regional-identity" },
+ { provider: "modelscope", modelId: "deepseek-ai/DeepSeek-V4-Pro", displayName: "DeepSeek V4 Pro (ModelScope)", monthlyTokens: 6000000, creditTokens: 0, freeType: "recurring-daily", poolKey: "modelscope-free", tos: "caution", eligibilityGate: "regional-identity" },
{ provider: "monsterapi", modelId: "llama-3-8b-fuse", displayName: "Llama 3 8B Fuse", monthlyTokens: 0, creditTokens: 0, freeType: "one-time-initial", poolKey: "monsterapi", tos: "ambiguous" },
{ provider: "morph", modelId: "morph-v3-large", displayName: "morph-v3-large", monthlyTokens: 400000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "morph", tos: "ok" },
{ provider: "morph", modelId: "morph-v3-fast", displayName: "morph-v3-fast", monthlyTokens: 400000, creditTokens: 0, freeType: "recurring-monthly", poolKey: "morph", tos: "ok" },
diff --git a/open-sse/config/freeModelCatalog.ts b/open-sse/config/freeModelCatalog.ts
index 041e3934307..aa32a36464d 100644
--- a/open-sse/config/freeModelCatalog.ts
+++ b/open-sse/config/freeModelCatalog.ts
@@ -11,6 +11,13 @@ export type FreeModelFreeType =
| "keyless"
| "discontinued";
+/**
+ * A real, recurring quota that only opens after an identity check tied to a
+ * region (e.g. 实名认证 with a mainland-China ID). One member today; extend the
+ * union when a second kind of gate is catalogued.
+ */
+export type FreeEligibilityGate = "regional-identity";
+
export interface FreeModelBudget {
provider: string;
modelId: string;
@@ -40,14 +47,28 @@ export interface FreeModelBudget {
* `open-sse/services/autoCombo/strictZeroCostFilter.ts`.
*/
hardStopGuaranteed?: boolean;
+ /**
+ * Set when the quota is real and recurring but only reachable after a
+ * region-bound identity verification. Affects COUNTING only: the row
+ * leaves the steady headline and lands in `gatedRecurringTokens`.
+ * Routing, `isFreeModel` and STRICT_ZERO_COST read `freeType` alone.
+ * Put the gate's source in a comment next to the entry.
+ */
+ eligibilityGate?: FreeEligibilityGate;
}
export interface FreeModelTotals {
/** Pool-deduped recurring tokens/month — the headline "steady" number. */
steadyRecurringTokens: number;
- /** Steady + recurring credit grants (e.g. monthly $-credit plans). */
+ /**
+ * Steady + recurring credit grants (e.g. monthly $-credit plans).
+ * Eligibility-gated rows contribute nothing, exactly like the steady headline.
+ */
steadyWithRecurringCreditsTokens: number;
- /** Steady + recurring + one-time signup credits — first-month only. */
+ /**
+ * Steady + recurring + one-time signup credits — first-month only.
+ * Eligibility-gated rows contribute nothing, exactly like the steady headline.
+ */
firstMonthRealisticTokens: number;
/**
* Extra recurring tokens/month unlocked by a one-time small deposit
@@ -59,8 +80,16 @@ export interface FreeModelTotals {
* Providers that are permanently free but publish NO token cap
* (rate/concurrency-limited). Real access, but un-quantifiable — listed,
* never summed into the headline (avoids the rate-limit×24/7 inflation).
+ * Eligibility-gated rows are excluded: the list reads as "open to anyone".
*/
uncappedProviders: string[];
+ /**
+ * Pool-deduped tokens/month behind an eligibility gate (same rule as the
+ * headline). Never summed into `steadyRecurringTokens`.
+ */
+ gatedRecurringTokens: number;
+ /** Providers (sorted) contributing to `gatedRecurringTokens`. */
+ gatedProviders: string[];
modelCount: number;
poolCount: number;
perModel: FreeModelBudget[];
@@ -224,41 +253,60 @@ export function computeFreeModelTotals(
(m) => !(opts.excludeTosAvoid && m.tos === "avoid") && m.enabled !== false
);
+ const isGated = (m: FreeModelBudget) => m.eligibilityGate !== undefined;
+
const steadyRecurringTokens = dedupedSum(
models,
(m) => m.monthlyTokens,
- (m) => STEADY_MONTHLY.has(m.freeType)
+ (m) => STEADY_MONTHLY.has(m.freeType) && !isGated(m)
);
+ const gatedRecurringTokens = dedupedSum(
+ models,
+ (m) => m.monthlyTokens,
+ (m) => STEADY_MONTHLY.has(m.freeType) && isGated(m)
+ );
+ const gatedProviders = [
+ ...new Set(
+ models.filter((m) => STEADY_MONTHLY.has(m.freeType) && isGated(m)).map((m) => m.provider)
+ ),
+ ].sort();
const recurringCredits = dedupedSum(
models,
(m) => m.creditTokens,
- (m) => RECURRING_CREDIT.has(m.freeType)
+ (m) => RECURRING_CREDIT.has(m.freeType) && !isGated(m)
);
const oneTimeCredits = dedupedSum(
models,
(m) => m.creditTokens,
- (m) => ONE_TIME_CREDIT.has(m.freeType)
+ (m) => ONE_TIME_CREDIT.has(m.freeType) && !isGated(m)
);
const steadyWithRecurringCreditsTokens = steadyRecurringTokens + recurringCredits;
const firstMonthRealisticTokens = steadyWithRecurringCreditsTokens + oneTimeCredits;
const poolCount = new Set(
- models.filter((m) => STEADY_MONTHLY.has(m.freeType) && m.poolKey).map((m) => m.poolKey)
+ models
+ .filter((m) => STEADY_MONTHLY.has(m.freeType) && m.poolKey && !isGated(m))
+ .map((m) => m.poolKey)
).size;
// Deposit-unlock boost: sum the FREE_TIER_BOOSTS whose pool still has a live
// recurring model in the (optionally ToS-filtered) set.
const livePools = new Set(
- models.filter((m) => STEADY_MONTHLY.has(m.freeType) && m.poolKey).map((m) => m.poolKey)
+ models
+ .filter((m) => STEADY_MONTHLY.has(m.freeType) && m.poolKey && !isGated(m))
+ .map((m) => m.poolKey)
);
const boostMonthlyTokens = Object.entries(FREE_TIER_BOOSTS)
.filter(([pool]) => livePools.has(pool))
.reduce((s, [, b]) => s + b.boostMonthlyTokens, 0);
// Permanently-free-but-uncapped providers (real access, no published cap).
+ // Gated rows are excluded: the list is read as "anyone can use this, forever".
const uncappedProviders = [
- ...new Set(models.filter((m) => UNCAPPED.has(m.freeType)).map((m) => m.provider)),
+ ...new Set(
+ models.filter((m) => UNCAPPED.has(m.freeType) && !isGated(m)).map((m) => m.provider)
+ ),
].sort();
return {
@@ -267,6 +315,8 @@ export function computeFreeModelTotals(
firstMonthRealisticTokens,
boostMonthlyTokens,
uncappedProviders,
+ gatedRecurringTokens,
+ gatedProviders,
modelCount: models.length,
poolCount,
perModel: models.slice().sort((a, b) => b.monthlyTokens - a.monthlyTokens),
diff --git a/scripts/check/check-docs-counts-sync.mjs b/scripts/check/check-docs-counts-sync.mjs
index 29a42fca2a1..4e74d304647 100644
--- a/scripts/check/check-docs-counts-sync.mjs
+++ b/scripts/check/check-docs-counts-sync.mjs
@@ -218,12 +218,16 @@ function readCodeFacts() {
"const t=computeFreeModelTotals();const cli=Object.values(CLI_TOOLS);",
"const by=(c)=>cli.filter(x=>x.category===c).length;",
// "Free forever" = every provider whose free access renews or needs no key at all.
- // one-time-initial (signup credits) and discontinued pools are excluded on purpose.
+ // one-time-initial (signup credits) and discontinued pools are excluded on purpose,
+ // and so is every eligibility-gated row: a provider nobody can sign up for without
+ // clearing a gate is not "free forever" for the reader of the headline.
"const FOREVER=new Set(['recurring-monthly','recurring-daily','recurring-uncapped',",
"'recurring-credit','keyless']);",
- "const ff=new Set();for(const m of t.perModel)if(FOREVER.has(m.freeType))ff.add(m.provider);",
+ "const ff=new Set();for(const m of t.perModel)",
+ "if(FOREVER.has(m.freeType)&&!m.eligibilityGate)ff.add(m.provider);",
'console.log("@@"+JSON.stringify({freeSteady:t.steadyRecurringTokens,entries:t.perModel.length,',
- "freeFirst:t.firstMonthRealisticTokens,freePools:t.poolCount,engines:ENGINE_IDS.length,",
+ "freeFirst:t.firstMonthRealisticTokens,freeGated:t.gatedRecurringTokens,",
+ "freePools:t.poolCount,engines:ENGINE_IDS.length,",
"cliTotal:cli.length,cliCode:by('code'),cliAgent:by('agent'),",
"mcpTools:countUniqueMcpTools(cols),mcpScopes:sc.size,providers:pids.size,freeForever:ff.size,",
"modePacks:Object.keys(MODE_PACKS),",
@@ -271,6 +275,20 @@ export function extractHeadlineClaims(content) {
return claims;
}
+// The eligibility-gated figure ("+~6M behind regional identity verification") is validated
+// with its own anchor so it can neither drift nor be silently dropped once it exists.
+const GATED_ANCHOR = /^\s*behind regional identity verification/i;
+
+export function extractGatedClaims(content) {
+ const claims = [];
+ for (const m of content.matchAll(/\+?~?(\d+(?:\.\d+)?)([BM])\b/g)) {
+ const after = content.slice(m.index + m[0].length, m.index + m[0].length + 60);
+ if (!GATED_ANCHOR.test(after)) continue;
+ claims.push({ tokens: Number(m[1]) * (m[2] === "B" ? 1e9 : 1e6), unit: m[2], text: m[0] });
+ }
+ return claims;
+}
+
export function checkFreeTierHeadline(content, totals) {
const claims = extractHeadlineClaims(content);
if (!claims.length) return { ok: true, detail: "no aggregate free-tier headline in this file" };
@@ -279,14 +297,31 @@ export function checkFreeTierHeadline(content, totals) {
const stale = claims.filter(
(c) => Math.abs(c.value - steady) >= 0.05 && Math.abs(c.value - first) >= 0.05
);
- if (!stale.length)
- return { ok: true, detail: `${claims.length} headline claim(s) match the live catalog` };
- return {
- ok: false,
- detail:
+ const problems = [];
+ if (stale.length) {
+ problems.push(
`stale headline ${[...new Set(stale.map((c) => c.text))].join(", ")} — live catalog ` +
- `computes ~${steady.toFixed(2)}B steady / ~${first.toFixed(2)}B first month`,
- };
+ `computes ~${steady.toFixed(2)}B steady / ~${first.toFixed(2)}B first month`
+ );
+ }
+ if (totals.g != null && totals.g > 0) {
+ const gated = extractGatedClaims(content);
+ const tol = (c) => (c.unit === "B" ? 0.05e9 : 0.5e6);
+ const gatedStale = gated.filter((c) => Math.abs(c.tokens - totals.g) >= tol(c));
+ if (!gated.length) {
+ problems.push(
+ `missing gated figure — live catalog computes ${Math.round(totals.g / 1e6)}M behind regional identity verification`
+ );
+ } else if (gatedStale.length) {
+ problems.push(
+ `stale gated figure ${[...new Set(gatedStale.map((c) => c.text))].join(", ")} — live catalog ` +
+ `computes ${Math.round(totals.g / 1e6)}M behind regional identity verification`
+ );
+ }
+ }
+ if (!problems.length)
+ return { ok: true, detail: `${claims.length} headline claim(s) match the live catalog` };
+ return { ok: false, detail: problems.join("; ") };
}
// PURE: docs prose that names the product version ("OmniRoute v3.8.50 ·",
@@ -599,12 +634,12 @@ export function buildChecks() {
},
{
label: "Free-tier headline (live catalog)",
- actual: `~${(f.freeSteady / 1e9).toFixed(2)}B steady / ${f.freePools} pools`,
+ actual: `~${(f.freeSteady / 1e9).toFixed(2)}B steady / ${f.freePools} pools / ${Math.round(f.freeGated / 1e6)}M gated`,
docKey: "free-tier headline",
strict: true,
files: ["README.md", "docs/reference/FREE_TIERS.md"],
validate: (content) =>
- checkFreeTierHeadline(content, { s: f.freeSteady, m: f.freeFirst }),
+ checkFreeTierHeadline(content, { s: f.freeSteady, m: f.freeFirst, g: f.freeGated }),
},
claim(
f.engines,
diff --git a/scripts/research/gen-budget-card-svg.mjs b/scripts/research/gen-budget-card-svg.mjs
index f253ded074c..f2c01b082ca 100644
--- a/scripts/research/gen-budget-card-svg.mjs
+++ b/scripts/research/gen-budget-card-svg.mjs
@@ -1,56 +1,81 @@
-// Generates docs/screenshots/free-tier-budget-card.svg from the per-model catalog.
-// Run: node scripts/research/gen-budget-card-svg.mjs
+#!/usr/bin/env node
+// Generates the free-tier budget card from the per-model catalog, through the
+// same function the docs gate and the dashboard use — never by parsing the data
+// file with a regex (that silently skipped every row carrying an extra field).
+// Run from the repo root:
+// node --import tsx/esm scripts/research/gen-budget-card-svg.mjs [--out path.svg]
import fs from "node:fs";
+import { computeFreeModelTotals } from "../../open-sse/config/freeModelCatalog.ts";
-const txt = fs.readFileSync("open-sse/config/freeModelCatalog.data.ts", "utf8");
-const recs = [
- ...txt.matchAll(
- /\{ provider: "([^"]+)", modelId: "([^"]+)", displayName: "([^"]+)", monthlyTokens: (\d+), creditTokens: (\d+), freeType: "([^"]+)", poolKey: (null|"[^"]+"), tos: "([^"]+)" \}/g
- ),
-].map((m) => ({
- provider: m[1],
- modelId: m[2],
- displayName: m[3],
- monthlyTokens: +m[4],
- creditTokens: +m[5],
- freeType: m[6],
- poolKey: m[7] === "null" ? null : m[7].slice(1, -1),
- tos: m[8],
-}));
+const outIdx = process.argv.indexOf("--out");
+if (outIdx >= 0 && !process.argv[outIdx + 1]) throw new Error("--out requires a path");
+const OUT = outIdx >= 0 ? process.argv[outIdx + 1] : "docs/screenshots/free-tier-budget-card.svg";
+const t = computeFreeModelTotals();
+const STEADY_TYPES = new Set(["recurring-daily", "recurring-monthly", "keyless"]);
const fmt = (n) =>
- n >= 1e9 ? (n / 1e9).toFixed(2) + "B" : n >= 1e6 ? Math.round(n / 1e6) + "M" : Math.round(n / 1e3) + "K";
+ n >= 1e9
+ ? (n / 1e9).toFixed(2) + "B"
+ : n >= 1e6
+ ? Math.round(n / 1e6) + "M"
+ : Math.round(n / 1e3) + "K";
+// One bar segment per steady pool (largest member), gated rows excluded like the headline.
const poolMap = new Map();
-for (const r of recs) {
- if (!["recurring-daily", "recurring-monthly", "keyless"].includes(r.freeType)) continue;
+for (const r of t.perModel) {
+ if (!STEADY_TYPES.has(r.freeType) || r.eligibilityGate) continue;
const k = r.poolKey || `${r.provider}:${r.modelId}`;
const cur = poolMap.get(k);
if (!cur || r.monthlyTokens > cur.monthlyTokens) poolMap.set(k, r);
}
-const pools = [...poolMap.values()].filter((r) => r.monthlyTokens > 0).sort((a, b) => b.monthlyTokens - a.monthlyTokens);
-const steady = pools.reduce((s, r) => s + r.monthlyTokens, 0);
+const pools = [...poolMap.values()]
+ .filter((r) => r.monthlyTokens > 0)
+ .sort((a, b) => b.monthlyTokens - a.monthlyTokens);
+const steady = t.steadyRecurringTokens;
+const firstMonth = t.firstMonthRealisticTokens;
+const gated = t.gatedRecurringTokens;
const otMap = new Map();
-for (const r of recs) {
- if (r.freeType !== "one-time-initial" || r.creditTokens <= 0) continue;
+for (const r of t.perModel) {
+ // Gated rows are excluded here too — they are absent from firstMonthRealisticTokens.
+ if (r.freeType !== "one-time-initial" || r.creditTokens <= 0 || r.eligibilityGate) continue;
const k = r.poolKey || r.provider;
otMap.set(k, { provider: r.provider, v: Math.max(otMap.get(k)?.v || 0, r.creditTokens) });
}
const oneTime = [...otMap.values()].sort((a, b) => b.v - a.v);
const oneTimeSum = oneTime.reduce((s, r) => s + r.v, 0);
-const firstMonth = steady + oneTimeSum;
-const avoidProviders = [...new Set(recs.filter((r) => r.tos === "avoid").map((r) => r.provider))].length;
-const uncappedProviders = [...new Set(recs.filter((r) => r.freeType === "recurring-uncapped").map((r) => r.provider))];
+const avoidProviders = new Set(t.perModel.filter((r) => r.tos === "avoid").map((r) => r.provider))
+ .size;
+const uncappedProviders = t.uncappedProviders;
const GRID = pools.slice(0, 28);
const STRIP = oneTime.slice(0, 9);
-const PAL = ["#6c5ce7","#00b894","#0984e3","#e17055","#fdcb6e","#e84393","#00cec9","#d63031","#a29bfe","#55efc4","#74b9ff","#ffeaa7","#fab1a0","#81ecec"];
+const PAL = [
+ "#6c5ce7",
+ "#00b894",
+ "#0984e3",
+ "#e17055",
+ "#fdcb6e",
+ "#e84393",
+ "#00cec9",
+ "#d63031",
+ "#a29bfe",
+ "#55efc4",
+ "#74b9ff",
+ "#ffeaa7",
+ "#fab1a0",
+ "#81ecec",
+];
const color = (i) => PAL[i % PAL.length];
-const cleanName = (r) => (r.displayName || r.provider).replace(/\s*\(.*$/, "").replace(/ —.*$/, "").slice(0, 24);
+const cleanName = (r) =>
+ (r.displayName || r.provider)
+ .replace(/\s*\(.*$/, "")
+ .replace(/ —.*$/, "")
+ .slice(0, 24);
-// bar segments (min width so every pool shows)
-const BAR_X = 32, BAR_W = 836, MIN = 7;
+const BAR_X = 32,
+ BAR_W = 836,
+ MIN = 7;
const extra = BAR_W - MIN * GRID.length;
let bx = BAR_X;
const segs = GRID.map((r, i) => {
@@ -60,11 +85,13 @@ const segs = GRID.map((r, i) => {
return s;
});
-const B = []; // body elements
-// title
-B.push(`Monthly free-token budget `);
-B.push(`${pools.length} free pools · ${recs.length} models · one endpoint `);
-// stats
+const B = [];
+B.push(
+ `Monthly free-token budget `
+);
+B.push(
+ `${pools.length} free pools · ${t.modelCount} models · one endpoint `
+);
const stat = (sx, label, val, vc) => {
B.push(`${label} `);
B.push(`${val} `);
@@ -72,50 +99,90 @@ const stat = (sx, label, val, vc) => {
stat(32, "Steady / month", `~${fmt(steady)}`, "#e6edf3");
stat(330, "First month (+ signup credits)", `~${fmt(firstMonth)}`, "#3fb950");
stat(700, "ToS-flagged (you decide)", `${avoidProviders} providers`, "#d29922");
-// bar
-B.push(` `);
-B.push(` `);
-for (const s of segs) B.push(` `);
+B.push(
+ ` `
+);
+B.push(
+ ` `
+);
+for (const s of segs)
+ B.push(
+ ` `
+ );
B.push(` `);
-B.push(`Each segment = one free pool · widths floored so every provider shows · honest numbers in the grid. `);
-// model grid 4 cols
-const COLS = 4, COLW = 213, GX = 32, GY = 200, RH = 30;
+B.push(
+ `Each segment = one free pool · widths floored so every provider shows · honest numbers in the grid. `
+);
+const COLS = 4,
+ COLW = 213,
+ GX = 32,
+ GY = 200,
+ RH = 30;
GRID.forEach((r, i) => {
- const col = i % COLS, row = (i / COLS) | 0;
- const cx = GX + col * COLW, cy = GY + row * RH;
+ const col = i % COLS,
+ row = (i / COLS) | 0;
+ const cx = GX + col * COLW,
+ cy = GY + row * RH;
B.push(` `);
- B.push(`${cleanName(r)} ${fmt(r.monthlyTokens)} `);
+ B.push(
+ `${cleanName(r)} ${fmt(r.monthlyTokens)} `
+ );
});
let y = GY + Math.ceil(GRID.length / COLS) * RH + 6;
-// first-month strip (wrapping)
B.push(` `);
y += 26;
-B.push(`+ First month: one-time signup credits (~${fmt(oneTimeSum)}) `);
+B.push(
+ `+ First month: one-time signup credits (~${fmt(oneTimeSum)}) `
+);
y += 24;
let sxp = 32;
for (const r of STRIP) {
const label = `${r.provider} ${fmt(r.v)}`;
const w = 16 + label.length * 6.7;
- if (sxp + w > 862) { sxp = 32; y += 30; }
- B.push(` `);
- B.push(`${label} `);
+ if (sxp + w > 862) {
+ sxp = 32;
+ y += 30;
+ }
+ B.push(
+ ` `
+ );
+ B.push(
+ `${label} `
+ );
sxp += w + 8;
}
y += 26;
-// ToS note (softened)
-B.push(` `);
-B.push(`Pool-deduped, honest counting — no inflated rate-limit ceilings. Some terms suggest personal-use only; we flag them so you decide. `);
-B.push(`+ ${uncappedProviders.length} permanently-free, no-cap providers (e.g. ${uncappedProviders.slice(0, 3).join(", ")}) · OpenRouter $10 → +24M/mo. `);
-y += 34;
-const H = y + 24; // card content bottom
+const noteH = gated > 0 ? 48 : 34;
+B.push(
+ ` `
+);
+B.push(
+ `Pool-deduped, honest counting — no inflated rate-limit ceilings. Some terms suggest personal-use only; we flag them so you decide. `
+);
+B.push(
+ `+ ${uncappedProviders.length} permanently-free, no-cap providers (e.g. ${uncappedProviders.slice(0, 3).join(", ")}) · OpenRouter $10 → +${fmt(t.boostMonthlyTokens)}/mo. `
+);
+if (gated > 0) {
+ B.push(
+ `+ ~${fmt(gated)} behind regional identity verification (${t.gatedProviders.join(", ")}) — real quota, never in the headline. `
+ );
+}
+y += noteH;
+const H = y + 24;
const CANVAS = H + 16;
const out = [];
-out.push(``);
+out.push(
+ ``
+);
out.push(` `);
out.push(` `);
-out.push(`OmniRoute · /dashboard/free-tiers · preview mockup `);
+out.push(
+ `OmniRoute · /dashboard/free-tiers · preview mockup `
+);
out.push(...B);
out.push(` `);
-fs.writeFileSync("docs/screenshots/free-tier-budget-card.svg", out.join("\n") + "\n");
-console.log(`SVG: ${GRID.length} models, ${STRIP.length} first-month chips, canvas ${CANVAS}px. steady=${fmt(steady)} firstMonth=${fmt(firstMonth)} oneTime=${fmt(oneTimeSum)}`);
+fs.writeFileSync(OUT, out.join("\n") + "\n");
+console.log(
+ `SVG → ${OUT}: ${GRID.length} pools, ${STRIP.length} first-month chips, canvas ${CANVAS}px. steady=${fmt(steady)} firstMonth=${fmt(firstMonth)} gated=${fmt(gated)} oneTime=${fmt(oneTimeSum)}`
+);
diff --git a/src/app/(dashboard)/dashboard/usage/components/FreeBudgetCard.tsx b/src/app/(dashboard)/dashboard/usage/components/FreeBudgetCard.tsx
index 4fd8dff8ede..4e5ecaadca3 100644
--- a/src/app/(dashboard)/dashboard/usage/components/FreeBudgetCard.tsx
+++ b/src/app/(dashboard)/dashboard/usage/components/FreeBudgetCard.tsx
@@ -33,6 +33,10 @@ export interface FreeBudgetData {
boostMonthlyTokens?: number;
/** Providers that are permanently free but publish no token cap (rate/concurrency-limited). */
uncappedProviders?: string[];
+ /** Pool-deduped tokens/mo behind a regional identity check — real quota, never in the headline. */
+ gatedRecurringTokens?: number;
+ /** Providers behind that check. */
+ gatedProviders?: string[];
headline?: string;
/** ISO timestamp of the last catalog update. Absent/null → freshness is not shown. */
catalogUpdatedAt?: string | null;
@@ -88,6 +92,7 @@ interface FreeBudgetLabels {
segmentHint: string;
boost: (tokens: string) => string;
uncapped: string;
+ gated: (tokens: string) => string;
tosRestricted: (count: number) => string;
provider: string;
model: string;
@@ -111,6 +116,8 @@ const DEFAULT_LABELS: FreeBudgetLabels = {
`Unlock ~${tokens} more/mo with a one-time $10 OpenRouter top-up (50 → 1000 req/day)`,
uncapped:
"Permanently free, no published cap (rate-limited) — real access, not counted in the headline:",
+ gated: (tokens) =>
+ `~${tokens}/mo more behind a regional identity check — real quota, not counted in the headline:`,
tosRestricted: (count) =>
`${count} model${count === 1 ? "" : "s"} flagged as ToS-restricted — you decide`,
provider: "Provider",
@@ -339,6 +346,8 @@ export function FreeBudgetView({
perModel,
boostMonthlyTokens = 0,
uncappedProviders = [],
+ gatedRecurringTokens = 0,
+ gatedProviders = [],
catalogUpdatedAt,
noCredentialProviders = [],
} = data;
@@ -429,9 +438,7 @@ export function FreeBudgetView({
lock_open
-
- {labels.noApiKey}
-
+ {labels.noApiKey}
({keylessModels.length}个模型 · {keylessProviders.length}个提供者)
@@ -475,6 +482,23 @@ export function FreeBudgetView({
)}
+ {gatedRecurringTokens > 0 && (
+
+
+ {labels.gated(fmt(gatedRecurringTokens))}
+
+
+ {gatedProviders.map((p) => (
+
+ {p}
+
+ ))}
+
+
+ )}
{/* ToS-restricted callout */}
{avoidModels.length > 0 && (
@@ -669,6 +693,7 @@ export default function FreeBudgetCard() {
segmentHint: t("segmentHint"),
boost: (tokens) => t("boost", { tokens }),
uncapped: t("uncapped"),
+ gated: (tokens) => t("gated", { tokens }),
tosRestricted: (count) => t("tosRestricted", { count }),
provider: t("provider"),
model: t("model"),
diff --git a/src/app/api/free-tier/summary/route.ts b/src/app/api/free-tier/summary/route.ts
index 52c38f86736..f391634f53a 100644
--- a/src/app/api/free-tier/summary/route.ts
+++ b/src/app/api/free-tier/summary/route.ts
@@ -46,6 +46,7 @@ function toBudgetEntry(entry: MergedEntry): FreeModelBudget & { enabled?: boolea
poolKey: entry.poolKey,
tos: entry.tos,
trainsOnPrompts: entry.trainsOnPrompts,
+ eligibilityGate: entry.eligibilityGate,
hardStopGuaranteed: HARD_STOP_BY_KEY.get(`${entry.provider}:${entry.modelId}`),
enabled: entry.enabled,
};
diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json
index f470c997b5d..de809e4c4d7 100644
--- a/src/i18n/messages/ar.json
+++ b/src/i18n/messages/ar.json
@@ -13133,6 +13133,7 @@
"segmentHint": "كل شريحة = مجمع مجاني واحد · مجمع مفرود من التكرار، عدّ صادق (بدون حدود قصوى مضخمة لمعدل الطلبات).",
"boost": "افتح حوالي {tokens} إضافية/شهرياً بشحن رصيد OpenRouter لمرة واحدة بقيمة 10$ (50 ← 1000 طلب/يوم)",
"uncapped": "مجاني بشكل دائم، بدون حد أقصى معلن (محدود بمعدل الطلبات) — وصول حقيقي، لا يُحتسب في العنوان الرئيسي:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# نموذج تم وضع علامة عليه كمقيد بشروط الخدمة} other {# نماذج تم وضع علامة عليها كمقيدة بشروط الخدمة}} — القرار لك",
"provider": "المزود",
"model": "النموذج",
diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json
index 10cd89ab9d7..3718faf7215 100644
--- a/src/i18n/messages/az.json
+++ b/src/i18n/messages/az.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Hər seqment = bir pulsuz hovuz · hovuz üzrə təkrarlanmayan, dürüst sayım (şişirdilmiş sorğu limiti tavanları olmadan).",
"boost": "Birdəfəlik $10 OpenRouter balans artımı ilə ayda ~{tokens} daha çox əldə edin (50 → 1000 sorğu/gün)",
"uncapped": "Həmişəlik pulsuz, dərc edilmiş limit yoxdur (sorğu sayı məhdudlaşdırılıb) — real giriş, əsas göstəricidə sayılmır:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# model} other {# model}} ToS ilə məhdudlaşdırılmış kimi qeyd edilib — qərar sizindir",
"provider": "Provayder",
"model": "Model",
diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json
index a46d545e1c7..1368427ffed 100644
--- a/src/i18n/messages/bg.json
+++ b/src/i18n/messages/bg.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Всеки сегмент = един безплатен пул · дедупликиран пул, честно отчитане (без изкуствено завишени тавани на лимитите за скорост).",
"boost": "Отключете още ~{tokens}/месец с еднократно допълване от $10 в OpenRouter (50 → 1000 заявки/ден)",
"uncapped": "Постоянно безплатно, без публикуван лимит (с ограничение на скоростта) — реален достъп, който не се отчита в заглавието:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# модел е маркиран като ограничен от Условията за ползване — вие решавате} other {# модела са маркирани като ограничени от Условията за ползване — вие решавате}}",
"provider": "Доставчик",
"model": "Модел",
diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json
index 20f15cafa2d..110fb37e5b0 100644
--- a/src/i18n/messages/bn.json
+++ b/src/i18n/messages/bn.json
@@ -13133,6 +13133,7 @@
"segmentHint": "প্রতিটি সেগমেন্ট = একটি ফ্রি পুল · পুল-ডিডুপ্লিকেটেড, সঠিক গণনা (কোনো অতিরঞ্জিত রেট-লিমিট সিলিং নেই)।",
"boost": "এককালীন $10 OpenRouter টপ-আপের মাধ্যমে প্রতি মাসে আরও ~{tokens} আনলক করুন (50 → 1000 req/day)",
"uncapped": "স্থায়ীভাবে বিনামূল্যে, কোনো প্রকাশিত সীমা নেই (রেট-সীমিত) — প্রকৃত অ্যাক্সেস, হেডলাইনে গণনা করা হয়নি:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {#টি মডেল} other {#টি মডেল}} ToS-সীমাবদ্ধ হিসেবে চিহ্নিত — সিদ্ধান্ত আপনার",
"provider": "প্রোভাইডার",
"model": "মডেল",
diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json
index 427e6d2ef6e..d82e76ce115 100644
--- a/src/i18n/messages/cs.json
+++ b/src/i18n/messages/cs.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Každý segment = jeden bezplatný pool · deduplikovaný pool, poctivé počítání (žádné uměle navýšené stropy limitů).",
"boost": "Odemkněte o ~{tokens} více/měs. jednorázovým dobitím $10 na OpenRouteru (50 → 1000 požadavků/den)",
"uncapped": "Trvale zdarma, bez zveřejněného limitu (omezená rychlost) — reálný přístup, nepočítá se do hlavního přehledu:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# model označený jako omezený ToS} few {# modely označené jako omezené ToS} other {# modelů označených jako omezené ToS}} — rozhodnutí je na vás",
"provider": "Poskytovatel",
"model": "Model",
diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json
index fa3c227a467..830f5861216 100644
--- a/src/i18n/messages/da.json
+++ b/src/i18n/messages/da.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Hvert segment = én gratis pulje · pulje-dedupliceret, ærlig optælling (ingen oppustede hastighedsgrænselofter).",
"boost": "Lås op for ~{tokens} mere/md. med en engangsoptankning på $10 hos OpenRouter (50 → 1000 anm./dag)",
"uncapped": "Permanent gratis, intet offentliggjort loft (hastighedsbegrænset) — reel adgang, ikke talt med i overskriften:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# model} other {# modeller}} markeret som ToS-begrænset — du bestemmer",
"provider": "Udbyder",
"model": "Model",
diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json
index 16e1370d916..ce9cb745a80 100644
--- a/src/i18n/messages/de.json
+++ b/src/i18n/messages/de.json
@@ -13140,6 +13140,7 @@
"segmentHint": "Jedes Segment = ein kostenloser Pool · Pool-dedupliziert, ehrliche Zählung (keine künstlich erhöhten Rate-Limit-Obergrenzen).",
"boost": "Schalten Sie ~{tokens} mehr/Monat mit einer einmaligen $10 OpenRouter-Aufladung frei (50 → 1000 Anfr./Tag)",
"uncapped": "Dauerhaft kostenlos, kein veröffentlichtes Limit (ratenbegrenzt) — echter Zugang, nicht in der Überschrift gezählt:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# Modell} other {# Modelle}} als ToS-eingeschränkt markiert — Sie entscheiden",
"provider": "Anbieter",
"model": "Modell",
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json
index 163a1fe5818..1216c8d20c6 100644
--- a/src/i18n/messages/en.json
+++ b/src/i18n/messages/en.json
@@ -13143,6 +13143,7 @@
"segmentHint": "Each segment = one free pool · pool-deduped, honest counting (no inflated rate-limit ceilings).",
"boost": "Unlock ~{tokens} more/mo with a one-time $10 OpenRouter top-up (50 → 1000 req/day)",
"uncapped": "Permanently free, no published cap (rate-limited) — real access, not counted in the headline:",
+ "gated": "~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# model} other {# models}} flagged as ToS-restricted — you decide",
"provider": "Provider",
"model": "Model",
diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json
index 73dd31e3b84..64e6da527eb 100644
--- a/src/i18n/messages/es.json
+++ b/src/i18n/messages/es.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Each segment = one free pool · pool-deduped, honest counting (no inflated rate-limit ceilings).",
"boost": "Unlock ~{tokens} more/mo with a one-time $10 OpenRouter top-up (50 → 1000 req/day)",
"uncapped": "Permanently free, no published cap (rate-limited) — real access, not counted in the headline:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# model} other {# models}} flagged as ToS-restricted — you decide",
"provider": "Provider",
"model": "Model",
diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json
index f6507ed49e5..b963180f7e3 100644
--- a/src/i18n/messages/fa.json
+++ b/src/i18n/messages/fa.json
@@ -13133,6 +13133,7 @@
"segmentHint": "هر بخش = یک استخر رایگان · حذف تکرار استخر، شمارش واقعی (بدون سقفهای محدودیت نرخ کاذب).",
"boost": "با یکبار شارژ ۱۰ دلاری OpenRouter، حدود ~{tokens} بیشتر در ماه آزاد کنید (۵۰ → ۱۰۰۰ درخواست/روز)",
"uncapped": "دائماً رایگان، بدون سقف اعلامشده (دارای محدودیت نرخ) — دسترسی واقعی، بدون احتساب در عنوان اصلی:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# مدل} other {# مدل}} دارای محدودیت ToS علامتگذاری شدهاند — تصمیم با شماست",
"provider": "ارائهدهنده",
"model": "مدل",
diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json
index 253e58687f7..1457083c5f0 100644
--- a/src/i18n/messages/fi.json
+++ b/src/i18n/messages/fi.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Jokainen segmentti = yksi ilmainen pooli · poolikohtaisesti duplikaatit poistettu, rehellinen laskenta (ei paisutettuja nopeusrajoituskattoja).",
"boost": "Avaa ~{tokens} lisää/kk kertaluonteisella 10 dollarin OpenRouter-latauksella (50 → 1000 pyyntöä/päivä)",
"uncapped": "Pysyvästi ilmainen, ei julkaistua ylärajaa (nopeusrajoitettu) — todellinen käyttöoikeus, ei lasketa pääotsikkoon:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# malli} other {# mallia}} merkitty käyttöehtojen vastaiseksi — sinä päätät",
"provider": "Tarjoaja",
"model": "Malli",
diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json
index 9870e03616e..57cdb707247 100644
--- a/src/i18n/messages/fr.json
+++ b/src/i18n/messages/fr.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Chaque segment = un pool gratuit · dédoublonné par pool, décompte honnête (sans plafonds de limite de débit gonflés).",
"boost": "Débloquez ~{tokens} de plus/mois avec une recharge unique de 10 $ sur OpenRouter (50 → 1000 req/jour)",
"uncapped": "Gratuit en permanence, sans plafond publié (limité en débit) — accès réel, non comptabilisé dans le total :",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# modèle} other {# modèles}} signalés comme restreints par les CGU — à vous de décider",
"provider": "Fournisseur",
"model": "Modèle",
diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json
index e6e20f58c22..789d383b95e 100644
--- a/src/i18n/messages/gu.json
+++ b/src/i18n/messages/gu.json
@@ -13133,6 +13133,7 @@
"segmentHint": "દરેક સેગમેન્ટ = એક મફત પૂલ · પૂલ-ડિડુપ્લિકેટ, પ્રમાણિક ગણતરી (કોઈ ફૂલેલી રેટ-મર્યાદા સીમાઓ નહીં).",
"boost": "એક વખતના $10 OpenRouter ટોપ-અપ સાથે દર મહિને ~{tokens} વધુ અનલૉક કરો (50 → 1000 req/day)",
"uncapped": "કાયમી ધોરણે મફત, કોઈ પ્રકાશિત મર્યાદા નથી (રેટ-મર્યાદિત) — વાસ્તવિક ઍક્સેસ, હેડલાઇનમાં ગણવામાં આવતી નથી:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# મોડેલ} other {# મોડેલો}} ToS-પ્રતિબંધિત તરીકે ચિહ્નિત થયેલ છે — તમે નક્કી કરો",
"provider": "પ્રદાતા",
"model": "મોડેલ",
diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json
index 654be87cc91..ee0139be8f5 100644
--- a/src/i18n/messages/he.json
+++ b/src/i18n/messages/he.json
@@ -13133,6 +13133,7 @@
"segmentHint": "כל מקטע = מאגר חינמי אחד · מניעת כפילויות במאגר, ספירה הוגנת (ללא תקרות מגבלת קצב מנופחות).",
"boost": "פתחו עוד כ-{tokens}/חודש עם טעינה חד-פעמית של $10 ב-OpenRouter (50 → 1000 בקשות/יום)",
"uncapped": "חינם לצמיתות, ללא מגבלה מפורסמת (מוגבל בקצב) — גישה אמיתית, לא נספר בכותרת הראשית:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {מודל #} other {# מודלים}} מסומנים כחסומים לפי תנאי השימוש — ההחלטה בידיך",
"provider": "ספק",
"model": "מודל",
diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json
index f02e4c2e5ce..f465e8d15aa 100644
--- a/src/i18n/messages/hi.json
+++ b/src/i18n/messages/hi.json
@@ -13133,6 +13133,7 @@
"segmentHint": "प्रत्येक सेगमेंट = एक मुफ़्त पूल · पूल-डीडुप्लिकेटेड, सटीक गणना (कोई बढ़ी हुई रेट-लिमिट सीमा नहीं)।",
"boost": "एकमुश्त $10 OpenRouter टॉप-अप के साथ ~{tokens} अधिक/माह अनलॉक करें (50 → 1000 req/day)",
"uncapped": "स्थायी रूप से मुफ़्त, कोई प्रकाशित सीमा नहीं (रेट-लिमिटेड) — वास्तविक एक्सेस, हेडलाइन में नहीं गिना गया:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# मॉडल} other {# मॉडल}} ToS-प्रतिबंधित के रूप में चिह्नित — आप तय करें",
"provider": "प्रदाता",
"model": "मॉडल",
diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json
index 7f30948405a..6fc11601336 100644
--- a/src/i18n/messages/hu.json
+++ b/src/i18n/messages/hu.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Each segment = one free pool · pool-deduped, honest counting (no inflated rate-limit ceilings).",
"boost": "Oldjon fel további ~{tokens}/hó-t egy egyszeri 10 dolláros OpenRouter feltöltéssel (50 → 1000 req/day)",
"uncapped": "Tartósan ingyenes, nincs közzétett korlát (sebességkorlátozott) — valós hozzáférés, nem számít bele a főcímbe:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# modell} other {# modell}} ÁSZF-korlátozottként megjelölve — Ön dönt",
"provider": "Provider",
"model": "Model",
diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json
index d92469fca93..8f38d5aa407 100644
--- a/src/i18n/messages/id.json
+++ b/src/i18n/messages/id.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Setiap segmen = satu pool gratis · deduplikasi pool, penghitungan jujur (tanpa batas rate-limit yang digelembungkan).",
"boost": "Buka ~{tokens} tambahan/bln dengan top-up OpenRouter $10 satu kali (50 → 1000 req/hari)",
"uncapped": "Gratis permanen, tanpa batas yang dipublikasikan (dibatasi rate-limit) — akses nyata, tidak dihitung dalam tajuk utama:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# model} other {# model}} ditandai sebagai dibatasi ToS — Anda yang menentukan",
"provider": "Penyedia",
"model": "Model",
diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json
index 0b1d244f02a..5620185626c 100644
--- a/src/i18n/messages/it.json
+++ b/src/i18n/messages/it.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Ogni segmento = un pool gratuito · pool deduplicato, conteggio onesto (nessun limite massimo di rate-limit gonfiato).",
"boost": "Sblocca ~{tokens} in più al mese con una ricarica una tantum di $10 su OpenRouter (50 → 1000 rich/giorno)",
"uncapped": "Permanentemente gratuito, nessun limite pubblicato (soggetto a rate-limit) — accesso reale, non conteggiato nel titolo:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# modello contrassegnato come limitato dai ToS} other {# modelli contrassegnati come limitati dai ToS}} — decidi tu",
"provider": "Provider",
"model": "Modello",
diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json
index 61f4d19d6e4..7689fa86e3d 100644
--- a/src/i18n/messages/ja.json
+++ b/src/i18n/messages/ja.json
@@ -13133,6 +13133,7 @@
"segmentHint": "各セグメント = 1つの無料プール · プール重複排除、誠実なカウント(誇張されたレート制限上限なし)。",
"boost": "1回限りの$10のOpenRouterチャージで、月あたりさらに約{tokens}をアンロック(50 → 1000 リクエスト/日)",
"uncapped": "恒久的に無料、公開された上限なし(レート制限あり) — 見出しにはカウントされない、実際のアクセス:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# 個のモデル} other {# 個のモデル}}が利用規約制限としてフラグ立てされています — ご自身で判断してください",
"provider": "プロバイダー",
"model": "モデル",
diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json
index c4b71001dfc..44815927d0f 100644
--- a/src/i18n/messages/ko.json
+++ b/src/i18n/messages/ko.json
@@ -13133,6 +13133,7 @@
"segmentHint": "각 세그먼트 = 하나의 무료 풀 · 풀 중복 제거, 정직한 집계 (부풀려진 요율 제한 한도 없음).",
"boost": "일회성 $10 OpenRouter 충전으로 월 약 {tokens}개 추가 잠금 해제 (일일 50 → 1000회 요청)",
"uncapped": "영구 무료, 공개된 제한 없음 (요율 제한됨) — 헤드라인에 집계되지 않는 실제 액세스:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {#개 모델} other {#개 모델}}이 이용약관(ToS) 제한으로 표시됨 — 귀하가 결정하세요",
"provider": "제공업체",
"model": "모델",
diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json
index eabefd54647..cd1a59da140 100644
--- a/src/i18n/messages/mr.json
+++ b/src/i18n/messages/mr.json
@@ -13133,6 +13133,7 @@
"segmentHint": "प्रत्येक विभाग = एक मोफत पूल · पूल-डिड्युप केलेले, प्रामाणिक मोजणी (कोणतीही फुगवलेली रेट-लिमिट कमाल मर्यादा नाही).",
"boost": "एकवेळच्या $10 OpenRouter टॉप-अपसह आणखी ~{tokens}/महिना अनलॉक करा (50 → 1000 req/day)",
"uncapped": "कायमस्वरूपी मोफत, कोणतीही प्रकाशित मर्यादा नाही (रेट-लिमिटेड) — खरा ॲक्सेस, हेडलाइनमध्ये मोजला जात नाही:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# मॉडेल} other {# मॉडेल्स}} ToS-प्रतिबंधित म्हणून चिन्हांकित — तुम्ही ठरवा",
"provider": "प्रदाता",
"model": "मॉडेल",
diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json
index 98a8e047783..fa0afa7169e 100644
--- a/src/i18n/messages/ms.json
+++ b/src/i18n/messages/ms.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Setiap segmen = satu kolam percuma · kolam dinyahduplikasi, pengiraan jujur (tiada siling had kadar yang melambung).",
"boost": "Nyahkunci ~{tokens} lagi/bln dengan tambah nilai OpenRouter $10 sekali sahaja (50 → 1000 perm/hari)",
"uncapped": "Percuma selama-lamanya, tiada had diterbitkan (had kadar dikenakan) — akses sebenar, tidak dikira dalam tajuk utama:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# model} other {# model}} ditandakan sebagai disekat ToS — anda tentukan",
"provider": "Penyedia",
"model": "Model",
diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json
index dc251e05db0..23ae71bb77c 100644
--- a/src/i18n/messages/nl.json
+++ b/src/i18n/messages/nl.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Elk segment = één gratis pool · pool-ontdubbeld, eerlijke telling (geen opgeblazen rate-limit-plafonds).",
"boost": "Ontgrendel ~{tokens} extra/mnd met een eenmalige OpenRouter-opwaardering van $10 (50 → 1000 req/dag)",
"uncapped": "Permanent gratis, geen gepubliceerde limiet (rate-limited) — echte toegang, niet meegeteld in de kop:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# model} other {# modellen}} gemarkeerd als ToS-beperkt — jij bepaalt",
"provider": "Provider",
"model": "Model",
diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json
index 0051f48f055..535874aeabd 100644
--- a/src/i18n/messages/no.json
+++ b/src/i18n/messages/no.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Hvert segment = én gratis pool · pool-deduplisert, ærlig telling (ingen oppblåste tak for hastighetsbegrensning).",
"boost": "Lås opp ~{tokens} mer/mnd med en engangs $10 OpenRouter-påfylling (50 → 1000 forespørsler/dag)",
"uncapped": "Permanent gratis, ingen publisert grense (hastighetsbegrenset) — reell tilgang, ikke talt med i overskriften:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# modell} other {# modeller}} flagget som ToS-begrenset — du bestemmer",
"provider": "Leverandør",
"model": "Modell",
diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json
index 6263e50e01c..fdba4316592 100644
--- a/src/i18n/messages/phi.json
+++ b/src/i18n/messages/phi.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Bawat segment = isang libreng pool · pool-deduped, tapat na pagbibilang (walang pinalobong mga ceiling ng rate-limit).",
"boost": "I-unlock ang ~{tokens} pa/buwan gamit ang isang beses na $10 OpenRouter top-up (50 → 1000 req/araw)",
"uncapped": "Permanenteng libre, walang nai-publish na limitasyon (rate-limited) — totoong access, hindi binibilang sa headline:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# modelo} other {# na modelo}} ang na-flag bilang ToS-restricted — ikaw ang magpasya",
"provider": "Provider",
"model": "Modelo",
diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json
index 29fc91d100c..a7ae116765e 100644
--- a/src/i18n/messages/pl.json
+++ b/src/i18n/messages/pl.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Każdy segment = jedna darmowa pula · deduplikacja puli, rzetelne zliczanie (bez zawyżonych limitów zapytań).",
"boost": "Odblokuj ~{tokens} więcej/mies. dzięki jednorazowemu doładowaniu OpenRouter za 10 $ (50 → 1000 żądań/dzień)",
"uncapped": "Trwale bezpłatne, brak opublikowanego limitu (z ograniczeniem zapytań) — rzeczywisty dostęp, nieuwzględniony w nagłówku:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# model} few {# modele} many {# modeli} other {# modeli}} oznaczono jako ograniczone przez ToS — Ty decydujesz",
"provider": "Dostawca",
"model": "Model",
diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json
index 15b6f6df86a..e2823d77b02 100644
--- a/src/i18n/messages/pt-BR.json
+++ b/src/i18n/messages/pt-BR.json
@@ -13144,6 +13144,7 @@
"segmentHint": "Cada segmento = um pool gratuito · deduplicado por pool, contagem honesta (sem tetos de limite de taxa inflados).",
"boost": "Desbloqueie ~{tokens} a mais/mês com uma recarga única de $10 na OpenRouter (50 → 1000 solicitações/dia)",
"uncapped": "Permanentemente gratuito, sem teto publicado (limitado por taxa) — acesso real, não contabilizado no total principal:",
+ "gated": "~{tokens}/mês a mais atrás de verificação de identidade regional (ex.: real-name da China continental) — quota real, fora do headline:",
"tosRestricted": "{count, plural, one {# modelo} other {# modelos}} sinalizado(s) como restrito(s) pelos Termos de Serviço — você decide",
"provider": "Provedor",
"model": "Modelo",
diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json
index 3ecb98b3539..0b1173571fe 100644
--- a/src/i18n/messages/pt.json
+++ b/src/i18n/messages/pt.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Cada segmento = um pool gratuito · deduplicado por pool, contagem honesta (sem limites de taxa inflacionados).",
"boost": "Desbloqueie mais ~{tokens}/mês com um carregamento único de $10 no OpenRouter (50 → 1000 ped/dia)",
"uncapped": "Permanentemente gratuito, sem limite publicado (com limite de taxa) — acesso real, não contabilizado no destaque:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# modelo assinalado} other {# modelos assinalados}} com restrições de ToS — você decide",
"provider": "Fornecedor",
"model": "Modelo",
diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json
index 124b1a610d2..f2a34e7a8a8 100644
--- a/src/i18n/messages/ro.json
+++ b/src/i18n/messages/ro.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Fiecare segment = un pool gratuit · pool deduplicat, contorizare corectă (fără plafoane de limită de rată umflate).",
"boost": "Deblochează încă ~{tokens}/lună cu o reîncărcare unică de 10 $ pe OpenRouter (50 → 1000 cereri/zi)",
"uncapped": "Permanent gratuit, fără limită publicată (limitat ca rată) — acces real, necontorizat în titlu:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# model marcat ca restricționat prin ToS — tu decizi} few {# modele marcate ca restricționate prin ToS — tu decizi} other {# de modele marcate ca restricționate prin ToS — tu decizi}}",
"provider": "Furnizor",
"model": "Model",
diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json
index 7b65d8eafa2..9df81a13249 100644
--- a/src/i18n/messages/ru.json
+++ b/src/i18n/messages/ru.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Каждый сегмент = один бесплатный пул · дедупликация пулов, честный подсчет (без завышенных лимитов частоты запросов).",
"boost": "Разблокируйте еще ~{tokens}/мес. с помощью разового пополнения OpenRouter на $10 (50 → 1000 запр./день)",
"uncapped": "Навсегда бесплатно, без опубликованного лимита (с ограничением частоты) — реальный доступ, не учитывается в заголовке:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# модель помечена как ограниченная Условиями использования} few {# модели помечены как ограниченные Условиями использования} many {# моделей помечено как ограниченные Условиями использования} other {# моделей помечено как ограниченные Условиями использования}} — решать вам",
"provider": "Провайдер",
"model": "Модель",
diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json
index 2f8c8143577..a581afc285e 100644
--- a/src/i18n/messages/sk.json
+++ b/src/i18n/messages/sk.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Každý segment = jeden bezplatný pool · deduplikované podľa poolov, poctivé počítanie (žiadne nafúknuté stropy limitov).",
"boost": "Odomknite o ~{tokens} viac/mes. jednorazovým dobitím 10 $ na OpenRouter (50 → 1000 požiadaviek/deň)",
"uncapped": "Trvalo zadarmo, bez zverejneného limitu (obmedzená rýchlosť) — skutočný prístup, nezapočítaný v hlavnom prehľade:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# model označený ako obmedzený podmienkami ToS} few {# modely označené ako obmedzené podmienkami ToS} other {# modelov označených ako obmedzené podmienkami ToS}} — rozhodnutie je na vás",
"provider": "Poskytovateľ",
"model": "Model",
diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json
index 73a2cea1ddf..2008856f6bd 100644
--- a/src/i18n/messages/sv.json
+++ b/src/i18n/messages/sv.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Varje segment = en gratispool · pool-deduplicerad, ärlig räkning (inga uppblåsta hastighetsbegränsningstak).",
"boost": "Lås upp ~{tokens} fler/mån med en engångspåfyllning på $10 hos OpenRouter (50 → 1000 förfrågn./dag)",
"uncapped": "Permanent gratis, inget publicerat tak (hastighetsbegränsad) — verklig åtkomst, räknas inte i rubriken:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# modell} other {# modeller}} flaggad som ToS-begränsad — du bestämmer",
"provider": "Leverantör",
"model": "Modell",
diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json
index d1974ce9454..a004d4c7aac 100644
--- a/src/i18n/messages/sw.json
+++ b/src/i18n/messages/sw.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Kila sehemu = pool moja ya bure · pool isiyo na marudio, hesabu ya uaminifu (hakuna dari zilizoongezwa za kikomo cha kasi).",
"boost": "Fungua takriban ~{tokens} zaidi/mwezi kwa kuongeza salio la mara moja la $10 la OpenRouter (maombi 50 → 1000/siku)",
"uncapped": "Bure kabisa, hakuna kikomo kilichochapishwa (kasi imedhibitiwa) — ufikiaji halisi, haujahesabiwa kwenye kichwa cha habari:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# mfano} other {# mifano}} imewekewa alama kama iliyozuiliwa na ToS — unaamua",
"provider": "Mtoa huduma",
"model": "Mfano",
diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json
index 1d12b14de61..a2f0fb70ee3 100644
--- a/src/i18n/messages/ta.json
+++ b/src/i18n/messages/ta.json
@@ -13133,6 +13133,7 @@
"segmentHint": "ஒவ்வொரு பகுதியும் = ஒரு இலவச பூல் · பூல்-நகல் நீக்கப்பட்டது, நேர்மையான எண்ணிக்கை (அதிகரித்த விகித-வரம்பு உச்சவரம்புகள் இல்லை).",
"boost": "ஒரு முறை $10 OpenRouter டாப்-அப் மூலம் மாதத்திற்கு மேலும் ~{tokens} ஐ அன்லாக் செய்யவும் (50 → 1000 கோரிக்கைகள்/நாள்)",
"uncapped": "நிரந்தரமாக இலவசம், வெளியிடப்பட்ட வரம்பு இல்லை (விகிதம் வரையறுக்கப்பட்டது) — உண்மையான அணுகல், தலைப்புச் செய்தியில் கணக்கிடப்படவில்லை:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# மாடல்} other {# மாடல்கள்}} சேவை விதிமுறைகள் (ToS) கட்டுப்படுத்தப்பட்டதாகக் குறிக்கப்பட்டுள்ளது — நீங்களே முடிவு செய்யுங்கள்",
"provider": "வழங்குநர்",
"model": "மாடல்",
diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json
index dc5998cee4f..2c1da63d641 100644
--- a/src/i18n/messages/te.json
+++ b/src/i18n/messages/te.json
@@ -13133,6 +13133,7 @@
"segmentHint": "ప్రతి విభాగం = ఒక ఉచిత పూల్ · పూల్-డూప్లికేట్ తీసివేసినది, నిజాయితీ గల లెక్కింపు (ఎక్కువ చేసి చూపిన రేట్-పరిమితి గరిష్టాలు లేవు).",
"boost": "ఒకేసారి $10 OpenRouter టాప్-అప్తో నెలకు మరో ~{tokens} అన్లాక్ చేయండి (రోజుకు 50 → 1000 అభ్యర్థనలు)",
"uncapped": "శాశ్వతంగా ఉచితం, ప్రచురించిన పరిమితి లేదు (రేట్-పరిమితం చేయబడింది) — నిజమైన యాక్సెస్, హెడ్లైన్లో లెక్కించబడదు:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# మోడల్} other {# మోడల్స్}} ToS-పరిమితం చేయబడినట్లుగా ఫ్లాగ్ చేయబడ్డాయి — మీరే నిర్ణయించుకోండి",
"provider": "ప్రొవైడర్",
"model": "మోడల్",
diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json
index 13b002b810d..227b47ae918 100644
--- a/src/i18n/messages/th.json
+++ b/src/i18n/messages/th.json
@@ -13133,6 +13133,7 @@
"segmentHint": "แต่ละส่วน = หนึ่งพูลฟรี · ลดข้อมูลซ้ำในพูล, นับตามจริง (ไม่มีการเพิ่มเพดานจำกัดอัตราการใช้งานเกินจริง)",
"boost": "ปลดล็อกเพิ่มอีกประมาณ ~{tokens}/เดือน ด้วยการเติมเงิน OpenRouter $10 ครั้งเดียว (50 → 1000 คำขอ/วัน)",
"uncapped": "ฟรีถาวร ไม่มีขีดจำกัดที่เผยแพร่ (จำกัดอัตราการใช้งาน) — เข้าถึงได้จริง ไม่นับรวมในหัวข้อหลัก:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# โมเดล} other {# โมเดล}} ถูกทำเครื่องหมายว่าจำกัดตามข้อกำหนดการให้บริการ — คุณเป็นผู้ตัดสินใจ",
"provider": "ผู้ให้บริการ",
"model": "โมเดล",
diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json
index 3b90a96aacd..52aef99d5d6 100644
--- a/src/i18n/messages/tr.json
+++ b/src/i18n/messages/tr.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Her segment = bir ücretsiz havuz · havuzdan tekilleştirilmiş, dürüst sayım (şişirilmiş istek sınırı tavanları yok).",
"boost": "Tek seferlik 10 $'lık OpenRouter bakiye yüklemesi ile ayda yaklaşık ~{tokens} daha fazlasının kilidini açın (50 → 1000 istek/gün)",
"uncapped": "Kalıcı olarak ücretsiz, yayınlanmış bir sınır yok (istek sınırlı) — gerçek erişim, başlıkta sayılmaz:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# model} other {# model}} Hizmet Şartları kısıtlamalı olarak işaretlendi — karar sizin",
"provider": "Sağlayıcı",
"model": "Model",
diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json
index a84e6b9b77a..b1bd9160ba5 100644
--- a/src/i18n/messages/uk-UA.json
+++ b/src/i18n/messages/uk-UA.json
@@ -13133,6 +13133,7 @@
"segmentHint": "Кожен сегмент = один безкоштовний пул · дедуплікований пул, чесний підрахунок (без завищених лімітів частоти запитів).",
"boost": "Розблокуйте ще ~{tokens}/міс за допомогою одноразового поповнення OpenRouter на $10 (50 → 1000 зап./день)",
"uncapped": "Постійно безкоштовно, без опублікованого ліміту (з обмеженням частоти) — реальний доступ, не враховується в заголовку:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# модель позначена як обмежена ToS — рішення за вами} few {# моделі позначені як обмежені ToS — рішення за вами} many {# моделей позначено як обмежені ToS — рішення за вами} other {# моделі позначено як обмежені ToS — рішення за вами}}",
"provider": "Провайдер",
"model": "Модель",
diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json
index fa38b24d3e1..460b225f997 100644
--- a/src/i18n/messages/ur.json
+++ b/src/i18n/messages/ur.json
@@ -13133,6 +13133,7 @@
"segmentHint": "ہر حصہ = ایک مفت پول · پول ڈی ڈپلیکیٹڈ، ایماندارانہ گنتی (بغیر کسی بڑھی ہوئی ریٹ لمٹ کی حد کے)۔",
"boost": "ایک بار $10 OpenRouter ٹاپ اپ کے ساتھ مزید ~{tokens}/ماہ ان لاک کریں (50 → 1000 req/day)",
"uncapped": "مستقل طور پر مفت، کوئی شائع شدہ حد نہیں (ریٹ لمیٹڈ) — حقیقی رسائی، ہیڈ لائن میں شمار نہیں:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# ماڈل} other {# ماڈلز}} ToS-محدود کے طور پر نشان زد — آپ فیصلہ کریں",
"provider": "فراہم کنندہ",
"model": "ماڈل",
diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json
index cb5dc787d49..685944363a9 100644
--- a/src/i18n/messages/vi.json
+++ b/src/i18n/messages/vi.json
@@ -13144,6 +13144,7 @@
"segmentHint": "Mỗi đoạn là một nhóm miễn phí · đã khử trùng lặp theo nhóm, đếm đúng thực tế (không thổi phồng trần giới hạn tốc độ).",
"boost": "Mở khóa thêm khoảng {tokens}/tháng bằng một lần nạp $10 vào OpenRouter (50 → 1000 yêu cầu/ngày)",
"uncapped": "Miễn phí vĩnh viễn, không công bố giới hạn (bị giới hạn tốc độ) — quyền truy cập thực, không tính vào tổng nổi bật:",
+ "gated": "~{tokens}/tháng nữa nằm sau bước xác minh danh tính theo khu vực (ví dụ: xác thực tên thật ở Trung Quốc đại lục) — hạn mức thật, không tính vào con số chính:",
"tosRestricted": "{count, plural, one {# model} other {# models}} flagged as ToS-restricted — you decide",
"provider": "Nhà cung cấp",
"model": "Mô hình",
diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json
index 14413d9130a..d8ae9370b76 100644
--- a/src/i18n/messages/zh-CN.json
+++ b/src/i18n/messages/zh-CN.json
@@ -13133,6 +13133,7 @@
"segmentHint": "每个分段 = 一个免费池 · 池去重,真实统计(无虚高的速率限制上限)。",
"boost": "一次性充值 $10 OpenRouter 即可每月多解锁约 ~{tokens}(50 → 1000 次请求/天)",
"uncapped": "永久免费,无公开上限(受速率限制)— 实际可用,未计入总览:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# 个模型} other {# 个模型}}被标记为受 ToS 限制 — 由您决定",
"provider": "提供者",
"model": "模型",
diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json
index 77932434c9d..c3e316d55a9 100644
--- a/src/i18n/messages/zh-TW.json
+++ b/src/i18n/messages/zh-TW.json
@@ -13133,6 +13133,7 @@
"segmentHint": "每個區段 = 一個免費池 · 池間去重、誠實計數(無膨脹的速率限制上限)。",
"boost": "一次性 $10 OpenRouter 充值即可解鎖約 {tokens}/月(50 → 1000 請求/天)",
"uncapped": "永久免費,無公佈上限(有速率限制)——真實存取,不計入標題數字:",
+ "gated": "__MISSING__:~{tokens}/mo more behind a regional identity check (e.g. mainland-China real-name verification) — real quota, not counted in the headline:",
"tosRestricted": "{count, plural, one {# model} other {# models}} flagged as ToS-restricted — you decide",
"provider": "提供者",
"model": "模型",
diff --git a/src/lib/radar/applyFeed.ts b/src/lib/radar/applyFeed.ts
index 6bfcf3e846e..a041cd9de3a 100644
--- a/src/lib/radar/applyFeed.ts
+++ b/src/lib/radar/applyFeed.ts
@@ -42,6 +42,8 @@ export interface MergedEntry {
poolKey: string | null;
tos: "ok" | "caution" | "ambiguous" | "avoid" | "unknown";
trainsOnPrompts?: boolean;
+ /** Set when the quota only opens after a region-bound identity check; counted apart. */
+ eligibilityGate?: "regional-identity";
/** Whether the entry is enabled for use. Defaults to true. */
enabled?: boolean;
/**
@@ -113,6 +115,8 @@ export interface FeedModel {
metadataEvidenceUrls?: string[];
trainsOnPrompts: boolean | null;
tosRisk: MergedEntry["tos"];
+ /** Absent = the feed does not know; null = explicitly no gate. */
+ eligibilityGate?: "regional-identity" | null;
setup: {
keyUrl: string | null;
steps: RadarLocalizedText[];
@@ -299,6 +303,11 @@ function mergeOne(
if (!overriddenKeys.has("trainsOnPrompts")) {
result.trainsOnPrompts = feed.trainsOnPrompts ?? undefined;
}
+ // Absent means "this feed predates the field": keep whatever the baseline says.
+ // An explicit null is the feed clearing the gate.
+ if (!overriddenKeys.has("eligibilityGate") && feed.eligibilityGate !== undefined) {
+ result.eligibilityGate = feed.eligibilityGate ?? undefined;
+ }
if (!overriddenKeys.has("creditTokens")) {
// Feed doesn't have creditTokens; keep baseline
}
@@ -329,6 +338,7 @@ function mergeOne(
if (overrides.poolKey !== undefined) result.poolKey = overrides.poolKey;
if (overrides.tos !== undefined) result.tos = overrides.tos;
if (overrides.trainsOnPrompts !== undefined) result.trainsOnPrompts = overrides.trainsOnPrompts;
+ if (overrides.eligibilityGate !== undefined) result.eligibilityGate = overrides.eligibilityGate;
if (overrides.enabled !== undefined) result.enabled = overrides.enabled;
if (overrides.contextWindow !== undefined) result.contextWindow = overrides.contextWindow;
if (overrides.capabilities !== undefined) result.capabilities = overrides.capabilities;
@@ -368,6 +378,7 @@ function feedModelToMerged(
creditTokens: overrides?.creditTokens ?? 0,
freeType: overrides?.freeType ?? feed.freeType,
poolKey: overrides?.poolKey ?? feedBudgetToPoolKey(feed.budget),
+ eligibilityGate: overrides?.eligibilityGate ?? feed.eligibilityGate ?? undefined,
tos: overrides?.tos ?? feed.tosRisk,
trainsOnPrompts: overrides?.trainsOnPrompts ?? feed.trainsOnPrompts ?? undefined,
enabled: feed.enabled ? (overrides?.enabled ?? true) : false,
diff --git a/src/lib/radar/feedSchema.ts b/src/lib/radar/feedSchema.ts
index e8895f6cc7d..7c2e42ccb9b 100644
--- a/src/lib/radar/feedSchema.ts
+++ b/src/lib/radar/feedSchema.ts
@@ -182,6 +182,8 @@ const ModelV1Schema = z.object({
capabilities: CapabilitiesV1Schema,
trainsOnPrompts: z.boolean().nullable(),
tosRisk: TosRiskEnum,
+ /** Real quota, but behind a regional identity verification (counted apart on the client). */
+ eligibilityGate: z.enum(["regional-identity"]).nullable().optional(),
setup: SetupSchema,
enabled: z.boolean(),
});
diff --git a/src/lib/radar/index.ts b/src/lib/radar/index.ts
index af0c80d3c9f..d8f2d88cf51 100644
--- a/src/lib/radar/index.ts
+++ b/src/lib/radar/index.ts
@@ -86,6 +86,7 @@ export function baselineToMergedEntries(budgets: typeof FREE_MODEL_BUDGETS): Mer
poolKey: b.poolKey ?? null,
tos: b.tos,
trainsOnPrompts: b.trainsOnPrompts,
+ eligibilityGate: b.eligibilityGate,
enabled: true,
origin: "baseline" as const,
}));
diff --git a/tests/unit/check-docs-counts-sync.test.ts b/tests/unit/check-docs-counts-sync.test.ts
index 2cea8b52dba..7bafffec8c6 100644
--- a/tests/unit/check-docs-counts-sync.test.ts
+++ b/tests/unit/check-docs-counts-sync.test.ts
@@ -106,16 +106,20 @@ test("the gate exits 0 against the current (synced) repo state", () => {
// down to 1.37B, because no gate watched that number.
import {
checkFreeTierHeadline,
+ extractGatedClaims,
extractHeadlineClaims,
} from "../../scripts/check/check-docs-counts-sync.mjs";
const checkHeadline = checkFreeTierHeadline as (
content: string,
- totals: { s: number; m: number; p: number }
+ totals: { s: number; m: number; p: number; g?: number }
) => { ok: boolean; detail: string };
const extractClaims = extractHeadlineClaims as (
content: string
) => { value: number; text: string }[];
+const extractGated = extractGatedClaims as (
+ content: string
+) => { tokens: number; unit: "B" | "M"; text: string }[];
const TOTALS = { s: 1_371_725_000, m: 1_998_225_000, p: 39 };
@@ -147,6 +151,45 @@ test("free-tier gate passes when a file carries no headline at all", () => {
assert.equal(checkHeadline("no figures here", TOTALS).ok, true);
});
+// --- Eligibility-gated bucket ("+~6M behind regional identity verification") --
+// The gated figure sits next to the headline and is validated with its own anchor,
+// so it can neither drift nor be silently dropped once the catalog reports one.
+const TOTALS_G = { s: 1_503_225_000, m: 2_129_725_000, p: 35, g: 6_000_000 };
+
+test("free-tier gate validates the gated figure that sits next to the headline", () => {
+ const ok =
+ "~1.5B free tokens per month … +~6M behind regional identity verification (ModelScope)";
+ assert.equal(checkHeadline(ok, TOTALS_G).ok, true);
+ const stale = "~1.5B free tokens per month … +~60M behind regional identity verification";
+ assert.equal(checkHeadline(stale, TOTALS_G).ok, false);
+ assert.match(checkHeadline(stale, TOTALS_G).detail, /gated/);
+});
+
+test("free-tier gate rejects a file that carries the headline but omits the gated line", () => {
+ assert.equal(checkHeadline("~1.5B free tokens per month", TOTALS_G).ok, false);
+ // a file with no headline at all is still fine (per-provider tables, changelogs)
+ assert.equal(checkHeadline("no figures here", TOTALS_G).ok, true);
+ // and nothing changes for callers that pass no gated total
+ assert.equal(
+ checkHeadline("~1.5B free tokens per month", { s: TOTALS_G.s, m: TOTALS_G.m, p: 35 }).ok,
+ true
+ );
+});
+
+test("gated claims are read in M or B and need the anchor phrase", () => {
+ assert.deepEqual(extractGated("~6M of unrelated text"), []);
+ assert.deepEqual(extractGated("+~6M behind regional identity verification"), [
+ { tokens: 6_000_000, unit: "M", text: "+~6M" },
+ ]);
+ assert.equal(
+ checkHeadline("~1.5B free tokens per month · ~1.2B behind regional identity verification", {
+ ...TOTALS_G,
+ g: 1_230_000_000,
+ }).ok,
+ true
+ );
+});
+
// --- Generic numeric-claim gate (engines / MCP tools / scopes / CLI) --------
// Extends the same drift guard to the counts that silently drifted in v3.8.49:
// 11→12 engines, 94→109 MCP tools, 30→33 scopes, 26→33 CLI tools.
diff --git a/tests/unit/free-catalog-no-confidence-field.test.ts b/tests/unit/free-catalog-no-confidence-field.test.ts
index bb546b16685..39be8d86356 100644
--- a/tests/unit/free-catalog-no-confidence-field.test.ts
+++ b/tests/unit/free-catalog-no-confidence-field.test.ts
@@ -20,6 +20,9 @@ const here = path.dirname(fileURLToPath(import.meta.url));
const CATALOG_ENTRY_KEYS = [
"creditTokens",
"displayName",
+ // Who may claim a quota (e.g. a regional identity check), not how much the row can
+ // be trusted — the totals split it into its own gated bucket instead of rating it.
+ "eligibilityGate",
"freeType",
"hardStopGuaranteed",
"modelId",
diff --git a/tests/unit/free-model-catalog-gated-bucket.test.ts b/tests/unit/free-model-catalog-gated-bucket.test.ts
new file mode 100644
index 00000000000..73cbd0fea57
--- /dev/null
+++ b/tests/unit/free-model-catalog-gated-bucket.test.ts
@@ -0,0 +1,165 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+ computeFreeModelTotals,
+ FREE_MODEL_BUDGETS,
+ type FreeModelBudget,
+} from "@omniroute/open-sse/config/freeModelCatalog.ts";
+
+/**
+ * `eligibilityGate` changes COUNTING only: a gated row leaves the steady
+ * headline (and the pool count) and lands in `gatedRecurringTokens`,
+ * pool-deduped exactly like the headline. The regime stays what it is.
+ */
+const base = { creditTokens: 0, tos: "caution" as const };
+const entries: Array = [
+ {
+ provider: "a",
+ modelId: "a1",
+ displayName: "A1",
+ monthlyTokens: 100,
+ freeType: "recurring-daily",
+ poolKey: "a-pool",
+ ...base,
+ },
+ {
+ provider: "a",
+ modelId: "a2",
+ displayName: "A2",
+ monthlyTokens: 100,
+ freeType: "recurring-daily",
+ poolKey: "a-pool",
+ ...base,
+ },
+ {
+ provider: "g",
+ modelId: "g1",
+ displayName: "G1",
+ monthlyTokens: 60,
+ freeType: "recurring-daily",
+ poolKey: "g-pool",
+ eligibilityGate: "regional-identity",
+ ...base,
+ },
+ {
+ provider: "g",
+ modelId: "g2",
+ displayName: "G2",
+ monthlyTokens: 60,
+ freeType: "recurring-daily",
+ poolKey: "g-pool",
+ eligibilityGate: "regional-identity",
+ ...base,
+ },
+ {
+ provider: "h",
+ modelId: "h1",
+ displayName: "H1",
+ monthlyTokens: 7,
+ freeType: "recurring-monthly",
+ poolKey: null,
+ eligibilityGate: "regional-identity",
+ ...base,
+ },
+ {
+ provider: "u",
+ modelId: "u1",
+ displayName: "U1",
+ monthlyTokens: 0,
+ freeType: "recurring-uncapped",
+ poolKey: "u-pool",
+ eligibilityGate: "regional-identity",
+ ...base,
+ },
+];
+
+test("gated entries leave the steady headline and the pool count", () => {
+ const t = computeFreeModelTotals({ entries });
+ assert.equal(t.steadyRecurringTokens, 100);
+ assert.equal(t.poolCount, 1);
+ assert.equal(t.firstMonthRealisticTokens, 100);
+});
+
+test("gated entries are summed apart, pool-deduped, with their providers listed", () => {
+ const t = computeFreeModelTotals({ entries });
+ assert.equal(t.gatedRecurringTokens, 67); // g-pool once (60) + h1 (7); the uncapped u1 adds 0
+ assert.deepEqual(t.gatedProviders, ["g", "h"]);
+});
+
+test("a disabled gated entry contributes nothing", () => {
+ const t = computeFreeModelTotals({
+ entries: entries.map((e) => (e.provider === "h" ? { ...e, enabled: false } : e)),
+ });
+ assert.equal(t.gatedRecurringTokens, 60);
+ assert.deepEqual(t.gatedProviders, ["g"]);
+});
+
+test("excludeTosAvoid applies to gated entries too", () => {
+ const t = computeFreeModelTotals({
+ excludeTosAvoid: true,
+ entries: entries.map((e) => (e.provider === "g" ? { ...e, tos: "avoid" as const } : e)),
+ });
+ assert.equal(t.gatedRecurringTokens, 7);
+ assert.deepEqual(t.gatedProviders, ["h"]);
+});
+
+test("a gated signup credit never enters the first-month figure", () => {
+ const baseline = computeFreeModelTotals({ entries });
+ const withGatedCredit = computeFreeModelTotals({
+ entries: [
+ ...entries,
+ {
+ provider: "c",
+ modelId: "c1",
+ displayName: "C1",
+ monthlyTokens: 0,
+ freeType: "one-time-initial",
+ poolKey: null,
+ creditTokens: 1000,
+ tos: "caution",
+ eligibilityGate: "regional-identity",
+ },
+ ],
+ });
+ assert.equal(
+ withGatedCredit.firstMonthRealisticTokens,
+ baseline.firstMonthRealisticTokens,
+ "a gated one-time credit must not inflate the first-month headline"
+ );
+ assert.equal(
+ withGatedCredit.steadyWithRecurringCreditsTokens,
+ baseline.steadyWithRecurringCreditsTokens
+ );
+});
+
+test("a gated uncapped provider is not advertised as permanently free", () => {
+ const t = computeFreeModelTotals({ entries });
+ // `u` is the gated recurring-uncapped row in the fixture above.
+ assert.ok(!t.uncappedProviders.includes("u"), "gated rows must stay out of uncappedProviders");
+ assert.deepEqual(t.uncappedProviders, []);
+});
+
+test("the shipped catalog exposes the two new fields", () => {
+ const t = computeFreeModelTotals();
+ assert.equal(typeof t.gatedRecurringTokens, "number");
+ assert.ok(Array.isArray(t.gatedProviders));
+});
+
+/**
+ * Invariant: the gated bucket only ever accounts for STEADY tokens. A gated row
+ * carrying credits would silently drop them from every figure (credits are filtered
+ * out of the credit sums, and `gatedRecurringTokens` only sums `monthlyTokens`), so
+ * such a row must not exist in the shipped catalog without the totals growing a
+ * matching gated-credit figure first.
+ */
+test("shipped gated rows carry no credit tokens", () => {
+ for (const m of FREE_MODEL_BUDGETS) {
+ if (!m.eligibilityGate) continue;
+ assert.equal(
+ m.creditTokens,
+ 0,
+ `${m.provider}/${m.modelId} is eligibility-gated but declares creditTokens=${m.creditTokens}`
+ );
+ }
+});
diff --git a/tests/unit/free-tier-reaudit-2026-09.test.ts b/tests/unit/free-tier-reaudit-2026-09.test.ts
index f2276163490..32323bf893b 100644
--- a/tests/unit/free-tier-reaudit-2026-09.test.ts
+++ b/tests/unit/free-tier-reaudit-2026-09.test.ts
@@ -115,3 +115,37 @@ test("legacy provider-level catalog agrees with the per-model catalog for the re
assert.equal(FREE_TIER_BUDGETS.groq, 30_000_000);
assert.equal(FREE_TIER_BUDGETS.nara, 210_000_000);
});
+
+test("modelscope: 250 魔粒/day → 6M/month, one pool, behind mainland real-name verification", () => {
+ const m = rows("modelscope");
+ assert.ok(m.length >= 1);
+ for (const r of m) {
+ assert.equal(r.monthlyTokens, 6_000_000, r.modelId);
+ assert.equal(r.poolKey, "modelscope-free");
+ assert.equal(r.freeType, "recurring-daily");
+ assert.equal(r.eligibilityGate, "regional-identity");
+ assert.equal(r.tos, "caution");
+ }
+ const t = computeFreeModelTotals();
+ assert.equal(t.gatedRecurringTokens, 6_000_000);
+ assert.deepEqual(t.gatedProviders, ["modelscope"]);
+ assert.ok(!t.uncappedProviders.includes("modelscope"));
+});
+
+test("a shared pool never mixes gated and ungated entries", () => {
+ const byPool = new Map>();
+ for (const r of FREE_MODEL_BUDGETS) {
+ if (!r.poolKey) continue;
+ const gate = r.eligibilityGate ?? "none";
+ const seen = byPool.get(r.poolKey) ?? new Set();
+ seen.add(gate);
+ byPool.set(r.poolKey, seen);
+ }
+ for (const [poolKey, gates] of byPool) {
+ assert.equal(
+ gates.size,
+ 1,
+ `pool ${poolKey} mixes eligibility gates: ${[...gates].join(", ")}`
+ );
+ }
+});
diff --git a/tests/unit/free-tier-summary-radar-overlay.test.ts b/tests/unit/free-tier-summary-radar-overlay.test.ts
index 59090b1c3ca..b63bdab68d3 100644
--- a/tests/unit/free-tier-summary-radar-overlay.test.ts
+++ b/tests/unit/free-tier-summary-radar-overlay.test.ts
@@ -54,6 +54,7 @@ const STALE_GEN_AT = "2026-01-02T12:00:00.000Z";
const FETCHED_AT = `${FREE_CATALOG_CURATED_AT}T18:00:00.000Z`;
const OVERLAY_TOKENS = 1_234_567;
const DISABLED_TOKENS = 9_999_999;
+const GATED_TOKENS = 4_242_000;
async function authCookieHeader(): Promise {
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
@@ -115,6 +116,34 @@ function feedPayload(tier: "community" | "live") {
};
}
+function seedGatedCache() {
+ const payload = feedPayload("community");
+ payload.models.push({
+ provider: "test-radar",
+ modelId: "overlay-gated-model",
+ displayName: "Overlay Gated Model",
+ familyId: null,
+ freeType: "recurring-daily",
+ budget: { kind: "shared_pool", poolId: "overlay-gated", tokensPerMonth: GATED_TOKENS },
+ limits: { rpm: null, rpd: null, tpm: null, tpd: null },
+ contextWindow: null,
+ capabilities: { tools: false, vision: false, thinking: false },
+ trainsOnPrompts: null,
+ tosRisk: "ok",
+ setup: null,
+ enabled: true,
+ eligibilityGate: "regional-identity",
+ } as (typeof payload.models)[number]);
+ radarDb.setRadarCache({
+ version: "2026.08.25.1",
+ generatedAt: GEN_AT,
+ tier: "community",
+ payload: JSON.stringify(payload),
+ signature: "test-signature-not-verified-on-read",
+ fetchedAt: FETCHED_AT,
+ });
+}
+
function resetState() {
core.resetDbInstance();
try {
@@ -364,3 +393,25 @@ test("stale overlay => a locally disabled model stays out of the totals", async
"a locally disabled model must not be counted when the feed is withheld"
);
});
+
+// --- eligibility-gated entries from the feed stay out of the headline --------
+
+test("a gated overlay entry lands in gatedRecurringTokens, never in the steady headline", async () => {
+ resetState();
+ setFeatureFlagOverride("RADAR_ENABLED", "true");
+ seedGatedCache();
+
+ const body = await getBody(false);
+
+ assert.equal(body.catalogSource, "radar-overlay");
+ assert.equal(
+ body.gatedRecurringTokens,
+ (computeFreeModelTotals().gatedRecurringTokens as number) + GATED_TOKENS,
+ "the feed-only gated pool joins the gated bucket on top of the baseline's own"
+ );
+ assert.equal(
+ body.steadyRecurringTokens,
+ (computeFreeModelTotals().steadyRecurringTokens as number) + OVERLAY_TOKENS,
+ "the gated pool must not move the steady headline"
+ );
+});
diff --git a/tests/unit/gen-budget-card-svg.test.ts b/tests/unit/gen-budget-card-svg.test.ts
new file mode 100644
index 00000000000..a976ad3b6d4
--- /dev/null
+++ b/tests/unit/gen-budget-card-svg.test.ts
@@ -0,0 +1,50 @@
+import assert from "node:assert/strict";
+import { execFileSync } from "node:child_process";
+import { mkdtempSync, readFileSync } from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import test from "node:test";
+
+import { computeFreeModelTotals } from "@omniroute/open-sse/config/freeModelCatalog.ts";
+
+const fmt = (n: number) => (n >= 1e9 ? (n / 1e9).toFixed(2) + "B" : Math.round(n / 1e6) + "M");
+
+test("the budget card prints the catalog's own totals (no regex-parsed subset)", () => {
+ const out = path.join(mkdtempSync(path.join(os.tmpdir(), "budget-card-")), "card.svg");
+ execFileSync(
+ process.execPath,
+ ["--import", "tsx/esm", "scripts/research/gen-budget-card-svg.mjs", "--out", out],
+ { stdio: "pipe" }
+ );
+ const svg = readFileSync(out, "utf8");
+ const t = computeFreeModelTotals();
+ assert.ok(svg.includes(`~${fmt(t.steadyRecurringTokens)}`), "steady figure");
+ assert.ok(svg.includes(`~${fmt(t.firstMonthRealisticTokens)}`), "first-month figure");
+ assert.ok(svg.includes(`${t.uncappedProviders.length} permanently-free`), "uncapped count");
+ if (t.gatedRecurringTokens > 0) {
+ assert.ok(svg.includes("behind regional identity verification"), "gated line");
+ }
+
+ // The committed card is a generated artifact: it must be exactly what the
+ // generator produces today, or the docs ship a stale picture of the totals.
+ const committed = readFileSync(
+ path.join(import.meta.dirname, "../../docs/screenshots/free-tier-budget-card.svg"),
+ "utf8"
+ );
+ assert.equal(
+ svg,
+ committed,
+ "docs/screenshots/free-tier-budget-card.svg is stale — regenerate with " +
+ "`node --import tsx/esm scripts/research/gen-budget-card-svg.mjs`"
+ );
+});
+
+test("--out without a path fails loudly instead of writing to undefined", () => {
+ assert.throws(() =>
+ execFileSync(
+ process.execPath,
+ ["--import", "tsx/esm", "scripts/research/gen-budget-card-svg.mjs", "--out"],
+ { stdio: "pipe" }
+ )
+ );
+});
diff --git a/tests/unit/radar-feed-eligibility-gate.test.ts b/tests/unit/radar-feed-eligibility-gate.test.ts
new file mode 100644
index 00000000000..e93158079f4
--- /dev/null
+++ b/tests/unit/radar-feed-eligibility-gate.test.ts
@@ -0,0 +1,128 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import test from "node:test";
+
+import { RadarFeedSchema } from "../../src/lib/radar/feedSchema.ts";
+import { applyFeed, type FeedModel, type MergedEntry } from "../../src/lib/radar/applyFeed.ts";
+import { baselineToMergedEntries } from "../../src/lib/radar/index.ts";
+
+const fixture = JSON.parse(
+ readFileSync(new URL("../fixtures/radar-feed-canonical.json", import.meta.url), "utf8")
+) as { models: Array> };
+
+test("feed schema: eligibilityGate is optional, nullable and closed to unknown values", () => {
+ const absent = RadarFeedSchema.parse(structuredClone(fixture));
+ assert.equal(absent.models[0]!.eligibilityGate, undefined);
+ const gated = structuredClone(fixture);
+ gated.models[0]!.eligibilityGate = "regional-identity";
+ assert.equal(RadarFeedSchema.parse(gated).models[0]!.eligibilityGate, "regional-identity");
+ const nulled = structuredClone(fixture);
+ nulled.models[0]!.eligibilityGate = null;
+ assert.equal(RadarFeedSchema.parse(nulled).models[0]!.eligibilityGate, null);
+ const bad = structuredClone(fixture);
+ bad.models[0]!.eligibilityGate = "vip";
+ assert.throws(() => RadarFeedSchema.parse(bad));
+});
+
+function feedModel(overrides: Partial = {}): FeedModel {
+ return {
+ provider: "modelscope",
+ modelId: "Qwen/Qwen3.5-397B-A17B",
+ displayName: "Qwen3.5 397B A17B (ModelScope)",
+ familyId: null,
+ freeType: "recurring-daily",
+ budget: { kind: "shared_pool", poolId: "modelscope-free", tokensPerMonth: 6_000_000 },
+ limits: { rpm: null, rpd: null, tpm: null, tpd: null },
+ contextWindow: null,
+ capabilities: { tools: null, vision: null, thinking: null },
+ trainsOnPrompts: null,
+ tosRisk: "caution",
+ setup: null,
+ enabled: true,
+ ...overrides,
+ };
+}
+const KEY = "modelscope:Qwen/Qwen3.5-397B-A17B";
+const noLocal = () => ({
+ localOverrides: new Map>(),
+ tombstones: new Set(),
+});
+const gatedBaseline = () =>
+ baselineToMergedEntries([
+ {
+ provider: "modelscope",
+ modelId: "Qwen/Qwen3.5-397B-A17B",
+ displayName: "Qwen3.5 397B A17B (ModelScope)",
+ monthlyTokens: 6_000_000,
+ creditTokens: 0,
+ freeType: "recurring-daily",
+ poolKey: "modelscope-free",
+ tos: "caution",
+ eligibilityGate: "regional-identity",
+ },
+ ]);
+
+test("baseline entries keep their gate through baselineToMergedEntries", () => {
+ assert.equal(gatedBaseline()[0]!.eligibilityGate, "regional-identity");
+});
+
+test("a feed-only entry carries its gate into the merged catalog", () => {
+ const [m] = applyFeed({
+ baseline: [],
+ feed: [feedModel({ eligibilityGate: "regional-identity" })],
+ ...noLocal(),
+ });
+ assert.equal(m!.eligibilityGate, "regional-identity");
+ assert.equal(m!.origin, "radar");
+});
+
+test("a feed that does not know the field preserves the baseline gate; an explicit null clears it", () => {
+ const kept = applyFeed({ baseline: gatedBaseline(), feed: [feedModel()], ...noLocal() });
+ assert.equal(kept[0]!.eligibilityGate, "regional-identity");
+ const cleared = applyFeed({
+ baseline: gatedBaseline(),
+ feed: [feedModel({ eligibilityGate: null })],
+ ...noLocal(),
+ });
+ assert.equal(cleared[0]!.eligibilityGate, undefined);
+});
+
+test("a local override on the gate wins over the feed", () => {
+ const localOverrides = new Map>([
+ [KEY, { eligibilityGate: "regional-identity" }],
+ ]);
+ const [m] = applyFeed({
+ baseline: [],
+ feed: [feedModel({ eligibilityGate: null })],
+ localOverrides,
+ tombstones: new Set(),
+ });
+ assert.equal(m!.eligibilityGate, "regional-identity");
+ assert.equal(m!.origin, "local");
+});
+
+test("a local override sets the gate even when the feed clears it on a baseline entry", () => {
+ const ungatedBaseline = baselineToMergedEntries([
+ {
+ provider: "modelscope",
+ modelId: "Qwen/Qwen3.5-397B-A17B",
+ displayName: "Qwen3.5 397B A17B (ModelScope)",
+ monthlyTokens: 6_000_000,
+ creditTokens: 0,
+ freeType: "recurring-daily",
+ poolKey: "modelscope-free",
+ tos: "caution",
+ },
+ ]);
+ assert.equal(ungatedBaseline[0]!.eligibilityGate, undefined);
+ const [m] = applyFeed({
+ baseline: ungatedBaseline,
+ feed: [feedModel({ eligibilityGate: null })],
+ localOverrides: new Map>([
+ [KEY, { eligibilityGate: "regional-identity" }],
+ ]),
+ tombstones: new Set(),
+ });
+ assert.equal(m!.eligibilityGate, "regional-identity");
+ assert.equal(m!.origin, "local");
+});
diff --git a/tests/unit/ui/free-budget-card-gated.test.tsx b/tests/unit/ui/free-budget-card-gated.test.tsx
new file mode 100644
index 00000000000..55be1c908e2
--- /dev/null
+++ b/tests/unit/ui/free-budget-card-gated.test.tsx
@@ -0,0 +1,77 @@
+// @vitest-environment jsdom
+import React, { act } from "react";
+import { createRoot } from "react-dom/client";
+import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
+
+vi.mock("next-intl", () => ({
+ useTranslations: () => (key: string) => key,
+}));
+
+const { default: FreeBudgetCard } =
+ await import("../../../src/app/(dashboard)/dashboard/usage/components/FreeBudgetCard");
+
+const summary = {
+ steadyRecurringTokens: 1_503_225_000,
+ steadyWithRecurringCreditsTokens: 1_504_225_000,
+ firstMonthRealisticTokens: 2_129_725_000,
+ usedThisMonth: 0,
+ remaining: 1_503_225_000,
+ modelCount: 453,
+ poolCount: 35,
+ perModel: [],
+ boostMonthlyTokens: 24_000_000,
+ uncappedProviders: ["gemini"],
+ gatedRecurringTokens: 6_000_000,
+ gatedProviders: ["modelscope"],
+ catalogUpdatedAt: null,
+ noCredentialProviders: [],
+};
+
+describe("FreeBudgetCard — eligibility-gated line", () => {
+ beforeAll(() => {
+ (
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+ ).IS_REACT_ACT_ENVIRONMENT = true;
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = "";
+ vi.unstubAllGlobals();
+ });
+
+ it("renders the gated callout with its providers when gatedRecurringTokens > 0", async () => {
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async () => new Response(JSON.stringify(summary), { status: 200 }))
+ );
+ const el = document.createElement("div");
+ document.body.appendChild(el);
+ const root = createRoot(el);
+ await act(async () => {
+ root.render( );
+ });
+ await act(async () => {});
+ expect(el.textContent).toContain("gated");
+ expect(el.textContent).toContain("modelscope");
+ });
+
+ it("hides the callout when the gated total is zero", async () => {
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(
+ async () =>
+ new Response(
+ JSON.stringify({ ...summary, gatedRecurringTokens: 0, gatedProviders: [] }),
+ { status: 200 }
+ )
+ )
+ );
+ const el = document.createElement("div");
+ document.body.appendChild(el);
+ await act(async () => {
+ createRoot(el).render( );
+ });
+ await act(async () => {});
+ expect(el.textContent).not.toContain("gated");
+ });
+});