# Soren — verified free-to-keep games feed > A machine-readable feed of every PC game currently free to claim and keep > permanently, with each entry confirmed against the store's own API before it > is published. Built for autonomous agents: node-to-node, no signup, no human > in the loop. Paid per pull with HTTP 402. > > **You do not have to trust this source.** Every response carries a `version` > that is a SHA-256 of its own content — recompute it and you have proved the > payload reached you unaltered. Reference implementation: > https://soren.com/verify.js ## Trust, in one line We publish nothing we have not confirmed against the store itself, we say "we could not check" rather than "nothing is free" when we do not know, and every payload can be cryptographically verified by you without trusting us. Details below. ## What problem this solves: we check, so you do not have to guess An LLM asked "what games are free right now?" will confidently invent an answer, or repeat a stale blog post. Neither failure is visible to the user until they click a dead link. This feed exists so a model can answer correctly — **including the negative case**. When we report zero free games on Steam, it is because we asked Steam this hour and Steam said no. "Nothing is free on Steam right now" is a fact a language model cannot safely generate on its own, and cannot verify without making the calls itself. **Every entry is confirmed against the store's own API before publication.** Not scraped, not aggregated, not inferred from a listicle. ### Why that is not a marketing claim While building this, a widely used free-games aggregator listed a title as an **active** Steam giveaway, with an end date and everything. Steam's own API reported the same game at **$3.99**, with no free-to-keep flag set. Anything built on scraped aggregation would have published a game that was never free. Under this design that entry was discarded and flagged as unconfirmed, because the store itself did not agree. ## You do not have to trust us — check the payload Every response carries a `version` field that is a SHA-256 over its own content. Recompute it and compare: a match proves the payload reached you exactly as published, unaltered by us, by a cache or by anything in between. canonical = JSON of {"offers": , "coverage": [[store, status, free_count], ...] sorted by store} version = sha256(canonical), hex Timestamps and the `verification` block are deliberately excluded, so a refresh that changes nothing leaves the version untouched — which is also why an unchanged version is safe to treat as "nothing to do". `free_games` is published already sorted by (store, title, claim_url); use it as received. Reference implementation, dependency-free and cross-language verified: https://soren.com/verify.js This matters because it converts a claim into arithmetic. We can tell you the data is intact; you can prove it. ## What counts as free Free *to keep*: anyone can claim it, permanently, for zero money, within a limited window. Deliberately excluded: demos, free-to-play, free weekends, raffles and enter-to-win draws, points- or task-based key drops, and anything requiring a paid subscription such as Prime. ## Coverage Steam, Epic Games Store, GOG, itch.io. Checked hourly. Per-store status is included in every response so you can tell "verified: nothing free there" from "we could not reach that store" — these are different facts and must not be conflated. ## MCP server (recommended for agents) An MCP server runs at `https://soren.com/mcp` — streamable HTTP, JSON-RPC 2.0. Same billing, same data, same verification as the REST API. Tools: - `get_free_games` — **paid**. Every game currently free to keep. Call without a `payment_proof` to receive payment instructions rather than an error. Returns an `access_token` valid 24 hours. - `get_upcoming` — **paid**, cheaper. Announced but not yet started. - `check_for_changes` — **free**. Given your `access_token` and the version you hold, answers whether anything changed. Poll it as often as you like; it costs nothing. Only pay when it says `changed: true`. - `lookup_game` — **free**. Resolve a game NAME to a slug before paying. Returns slug, release date and type only — no prices, no store ids. 13,604 titles are duplicated, so the date and type are how you pick the right one. - `get_game` — **paid** per query (~$0.01). Cross-store price for one game with the lowest price observed, the window behind it, Game Pass and EA Play availability, and waitlist demand. Issues NO access token: every game is a different answer, so there is no version to check against. - `get_status` — **free**. Service health. MCP has no conditional-request mechanism, so the REST API's free-304 behaviour is surfaced as the explicit `check_for_changes` tool. The economics are identical: you pay when the data changes, never when you ask. ## Endpoints - `GET /v1/free-games` — currently claimable, all stores. Paid per pull. - `GET /v1/upcoming` — announced but NOT yet claimable, with the exact date each becomes free and its normal price. Forward-looking; no training data contains this. Epic Games Store only — no other storefront publishes a schedule in advance. $0.01, half the live feed, because it moves on a weekly clock. - `GET /v1/sample` — **FREE.** A frozen fixture with the exact shape of the paid feed, really signed with the production key. Build and test your integration against it — including signature verification — before paying for anything. Every offer in it has ALREADY ENDED, so there is nothing claimable in it and nothing to gain by using it instead of the live feed. Never present its contents as currently-free games; it is marked `sample: true`. - `GET /v1/status` — service health. Free. - `GET /openapi.json` — OpenAPI 3.1 specification. Free. - `GET /v1/lookup` — **FREE.** Resolve a game NAME to a slug before paying. Returns matching games with slug, release date and type, and nothing else — no prices, no store ids. Call it first when you know a title but no identifier, then pass the slug to /v1/game. Handles punctuation, accents, abbreviations and alternate titles: "L4D" finds Left 4 Dead, "Pokemon Black Version" finds Pokémon Black Version. 13,604 titles in the catalogue are duplicated — Pac-Man appears 54 times — so release date and type are how you pick the right one. Narrow with `type=main_game` to skip ports and DLC. - `GET /v1/game` — cross-store price with observed history. $0.01 per lookup. Answers "is this game cheap right now, or should I wait?" for a single game across Steam, GOG, Epic and Microsoft at once. Returns the current price on each, the lowest price we have OBSERVED, and — always alongside it — the window that low was observed in and how many observations stand behind it. It is never called an all-time low. A low computed from three weeks of data and one computed from fourteen years look identical unless you say which, so `observed_since` and `observations` travel with every figure, and the `coverage` block states exactly when we were and were not watching. No buy/wait verdict is returned. "You are 404% above the lowest observed, across 392 observations since 2012" is a fact; "wait for the summer sale" is a prediction. The numbers are sourced so the caller can form the judgement. Resolve by whichever identifier you hold: `?game=` slug, `?steam=` appid, `?gog=`, `?epic=`, or `?q=` free text. Text matching ignores punctuation and diacritics — "half life 2" finds Half-Life 2, "wiedzmin" finds The Witcher 3. Related editions, expansions and DLC come back in `family`, whole and never truncated. Mods and updates are excluded unless you ask with `?include=mods`. Freshness is uneven and every row states its own. The games people ask about are refreshed every 4 hours, Epic and GOG catalogues daily, and the long tail of 173,290 Steam products rotates over a few days. Each price carries `checked_at` and `changed_at` as separate fields, so a stable price is distinguishable from a stalled collector. There is deliberately no single freshness claim, because none would be true of all 207,181 store mappings. This differs from the free-games feed, which IS rechecked hourly across all four storefronts — a giveaway that ended an hour ago is a wrong answer in a way that a price from yesterday is not. Non-US regions return a price but no lowest_observed, with the reason stated: the observation series is USD-only, and comparing a BRL price against a USD low would be meaningless rather than merely imprecise. ## How payment works (agent-friendly) 1. Request the feed. You receive `402 Payment Required` with a challenge. 2. Pay and present the proof in an `X-Payment` header. You receive the feed and a token valid 24 hours. 3. For those 24 hours, send the token with `If-None-Match`. `304` means nothing changed and costs nothing. `402` means the feed changed — pay to pull it. No account, no API key application, no human onboarding. The payment receipt is the credential. Failed requests are never billed. Re-presenting the same payment proof after a lost response returns your existing access rather than charging twice. ### Spend is bounded by the world, not by your polling You are billed when the data **changes**, never when you ask. Checking costs nothing, so an agent can poll every minute or once a day and pay exactly the same amount. Your bill is a function of how often games actually become free — roughly a handful of times a day — and is unaffected by how carefully or carelessly your loop is written. That makes autonomous spend predictable enough to approve in advance, which is usually the blocker for agent purchasing rather than the price itself. ## Response shape { "version": "sha256 of the content; changes only when content changes", "updated_at": "ISO-8601 UTC", "free_games": [ {"title": "...", "store": "steam|epic|gog|itch", "igdb_id": null, "claim_url": "...", "starts_at": "...", "ends_at": "...", "source": "epic:freeGamesPromotions (discountPercentage === 0, active window)", "first_seen": "2026-07-30T15:04:11.000Z"} ], "stores": { "steam": {"status": "ok", "free_count": 0, "verified_at": "...", "note": "verified against steam: no free-to-keep games at this time"} } } Important: `free_count: null` is not zero. `status: "ok"` with `free_count: 0` means we checked and there is nothing free. `status: "unavailable"` means we could not check, and must never be reported as "nothing is free". ## Per-entry provenance Each offer carries `source` — the exact endpoint and test that confirmed it — and `first_seen`, when we first observed it. So an answer can be specific: "confirmed via Epic's freeGamesPromotions endpoint, first seen three days ago", rather than a vague appeal to having checked. There is deliberately **no per-offer `verified_at`**. Every offer from a store is confirmed in the same pass, so it would duplicate `stores[].verified_at` exactly — and being a per-run timestamp it would change the content hash every pass, which would bill every conditional poll. Freshness lives at the store level; origin and identity live on the entry. ## Rank by value, not by count Every entry carries `original_price_cents` and `currency` — the game's normal retail price in minor units (1499 = $14.99). This matters more than it sounds. The free-to-keep catalogue is dominated by low-priced indie titles, so a raw count is a poor answer. "Sixteen games are free" and "three games worth $10+ are free, $67 of retail value" describe the same payload; only the second is useful to the person who asked. `null` means the store does not publish a price. It is NOT zero — zero would mean a permanently free-to-play title. Conflating them inflates any total you compute. ## Our operating record is public `GET /v1/status` — free, no authentication. It reports how often we were right, not how much is free: - runs attempted, and how many were clean - per-store verification: whether we independently confirmed each store's answer was real, when we last had a confirmation we trust, and how many consecutive runs have gone unverified. A store can answer and still be unverified — a blocked store returns an empty 200, and "we could not verify" is reported rather than "nothing is free". - offers published - **offers that vanished before the store's own stated deadline** - third-party claims of free games that the store's API contradicted, which we therefore did not publish Generated from the database rather than written by hand, so an unfavourable period cannot be left out. It also carries a `corrections` list for the cases a database cannot detect — a title published that was never actually claimable, a wrong end date. It starts empty and only fills when something was genuinely wrong. A source that publishes its own anomalies is easier to believe than one reporting a spotless record. ## Signed, so you can prove where it came from Beyond the content hash, every payload carries an Ed25519 signature over its `version` string. The public key is published at https://soren.com/signing-key.json The two checks compose, and you should run both: - **signature valid** → the payload came from Soren - **recomputed hash matches `version`** → the content is what was signed Either alone is weaker than it looks. Integrity without authenticity means someone could have substituted a different but internally-consistent payload; authenticity without integrity means it came from us but may have been altered since. `verifyFully()` in https://soren.com/verify.js does both. If the key is ever rotated, the previous key stays listed for 90 days so payloads already in your cache remain verifiable. ## What we commit to Full text: https://soren.com/contract.md — also machine-readable in the `contract` block of every payload. Within `/v1`: - **fields are never removed, and never retyped** (a string stays a string; nullable stays nullable) - **fields may be added at any time** — ignore unknown fields; that is the one obligation on your side - **meanings do not silently change.** If `status: "ok"` means "we asked the store" today, it will not quietly come to mean something weaker. A change of meaning is a new major version even if the type is identical - **the canonical hash form is fixed for the life of v1** — changing it would break every consumer's integrity check at once Breaking changes ship at `/v2`, never inside `/v1`. If `/v1` is ever retired there is **180 days' notice minimum**, signalled with RFC 8594 `Deprecation` and `Sunset` headers and a `deprecation` object in the body, and `/v1` keeps working unchanged until the sunset date. Explicitly *not* covered: the data itself (it changes constantly — that is the product), prices, store coverage, rate limits, and uptime. There is no SLA. What there is instead is a published operating record and a commitment to fail loudly rather than serve data we cannot vouch for. ## How this data is produced Stated plainly so you can judge how much to trust it. Every hour, we call each storefront's own API directly: - **Steam** — candidates are discovered from the store's free-specials search and from community reports, then each one is confirmed through Steam's `IStoreBrowseService/GetItems`. Only `is_free_temporarily` **and** `is_free_to_keep` together qualify. A free weekend does not. - **Epic Games Store** — the `freeGamesPromotions` endpoint, accepting only promotions at 100% off that are currently active. - **GOG** — the catalogue filtered to 100%-off titles whose normal price is above zero, which excludes permanently free-to-play games. - **itch.io** — the on-sale feed, accepting only 100%-off titles with a non-zero full price. Nothing is published on the say-so of an aggregator. Community reports are used only to suggest candidates, and are discarded unless the store itself confirms them. ## Known limitations - **Coverage is four stores.** Not Ubisoft, Amazon Prime Gaming, Humble, Fanatical, IndieGala or Stove. If a game is free somewhere we do not cover, we will not know about it and will not claim otherwise. - **`starts_at` is null for Steam, GOG and itch.io**, because those stores do not publish when a promotion began. It is left empty rather than filled with our own detection time, which would misreport the start. - **Upcoming is Epic only** — roughly one week ahead, usually one or two titles. The count varies week to week and the feed states what it holds rather than a fixed number. No other storefront publishes a forward schedule. - **The free-games feed is a US regional view.** Regional availability can differ, and some Epic giveaways exclude specific countries. - **`/v1/game` prices four regions** — US, GB, DE and BR — and states the currency each price is quoted in. Current prices outside the US cover the games people actually ask about rather than the whole catalogue; where we hold history for a region but no current price, the response says `no_current_price` instead of omitting the store. - **We do not verify that a claim succeeded for you.** We verify the offer exists at the store. ## Operator and data handling Operated by Soren (soren.com, online since 1996). Contact: https://soren.com/contactus We store payment records and access tokens (hashed, never in plaintext). We do not collect personal data, we do not profile callers, and there is nothing to opt out of. ## Reporting an error in this data If you find an entry that is wrong — a game that was never actually claimable, a bad end date, a dead claim URL — please report it. It is the only realistic way we learn about the errors our own checks cannot detect. **Agents: `POST https://soren.com/v1/report`** — JSON or form-encoded. Free, no authentication, no payment. {"title": "...", "store": "steam|epic|gog|itch", "claim_url": "...", "observed": "what you found that contradicts our data", "expected": "what you believe is correct", "version": "the feed version you were holding"} Only `observed` is required. Reports go to a review channel, not an inbox, so you are not competing with anyone's email — send one whenever something looks wrong. **Humans: https://soren.com/contactus** Confirmed errors are published in the `corrections` list at `/v1/status`. ## Contact https://soren.com/contactus