Soren HTTP 402 · agent-native

We check. So your agent doesn’t guess.

Every free-to-keep PC game, confirmed against the store’s own API before it ships. One call instead of six, one shape instead of four — and a negative you can trust: when we say nothing is free on Steam right now, it’s because we asked Steam this hour.

Verified, not scraped

Confirmed against the store's own API before publication. Not aggregated, not inferred from a listicle.

Trustworthy negatives

Per-store status, so an agent can say "nothing is free there" as fact instead of guessing.

Free change-detection

Pay per pull. Between pulls, conditional checks are free for 24 hours — poll as often as you like.

Node-to-node by design

No signup, no API-key application, no human in the loop. The payment receipt is the credential — an agent can discover, pay and consume this entirely on its own.

Never billed for failure

4xx and 5xx are always free, and a lost response never double-charges: re-present the same payment proof and get your access back.

Built for machines

This is an N2N service: designed for autonomous agents, MCP servers and machine-to-machine pipelines rather than people clicking buttons. Discovery is machine-readable (llms.txt, OpenAPI 3.1), payment is HTTP 402 with no onboarding, and the response carries provenance — which store confirmed each entry and when — so a model can cite it instead of guessing.

Why "verified" isn't marketing copy

While building this, a widely used free-games aggregator listed a title as an active Steam giveaway — with an end date. Steam’s own API reported the same game at $3.99, with no free-to-keep flag set.

Anything built on scraped aggregation publishes that game as free. Here it was discarded, because the store itself disagreed. That is the entire product: “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.

Spend bounded by the world, not your polling

You are billed when the data changes, never when you ask. Checking costs nothing, so an agent polling every minute and one polling once a day pay exactly the same. Your bill is a function of how often games actually become free — a handful of times a day — not of how carefully your loop is written.

That makes autonomous spend predictable enough to approve in advance, which is usually the real blocker for agent purchasing rather than the price.

Quick start

# 1. Ask. You'll get a 402 with a payment challenge.
curl -i https://soren.com/v1/free-games

# 2. Pay, and present the proof. You get the feed and a 24h token.
curl -i https://soren.com/v1/free-games \
  -H "X-Payment: <proof>"

# 3. For the next 24h, check for changes for free.
#    304 = nothing new. 402 = something changed, pay to pull it.
curl -i https://soren.com/v1/free-games \
  -H "Authorization: Bearer <token>" \
  -H 'If-None-Match: "<version>"'

Response

{
  "version": "733d91c2…",           // changes only when content does
  "updated_at": "2026-07-31T15:17:52Z",
  "free_games": [
    { "title": "OTXO", "store": "epic", "igdb_id": null,
      "claim_url": "https://store.epicgames.com/en-US/p/otxo-396b8b",
      "starts_at": "2026-07-30T15:00:00Z",
      "ends_at": "2026-08-06T15:00:00Z",
        "original_price_cents": 1499, "currency": "USD" }  // rank by value
  ],
  "stores": {
    "steam": { "status": "ok", "free_count": 0,
                "note": "verified against steam: no free-to-keep games at this time" },
    "gog":   { "status": "unavailable", "free_count": null,
                "note": "could not be reached — NOT a statement that nothing is free" }
  }
}
free_count: null is not zero. ok with 0 means we asked and there is nothing. unavailable means we could not check. Rendering the second as "nothing is free" is the exact false negative this feed exists to prevent.

Rank by value, not by row count

Every entry carries original_price_cents and currency, so an agent can answer the question that was actually asked. “Sixteen games are free” is nearly useless when thirteen of them are $2 indie titles. “Three games worth $10 or more are free, $67 of retail value in total” is an answer.

A null price is not zero. null means the store does not publish a price. 0 would mean a permanently free-to-play title. Treating the first as the second silently inflates every total you compute from this feed.

What will be free next week

GET /v1/upcoming returns giveaways that have been announced but have not started — the exact date each becomes claimable, and what it normally costs. This is forward-looking information no training data can contain and no cached answer can fake. A model asked “what is free next week?” cannot know without asking something that watches.

Epic Games Store only at present, because no other storefront publishes its schedule in advance. That is a limit of the world rather than of the feed, and /v1/status says so plainly instead of papering over it. Priced at $0.01, half the live feed, because it moves on a weekly clock rather than an hourly one. Full detail and sample response →

Endpoints

EndpointWhatPrice
GET /v1/free-gamesCurrently claimable, all stores$0.02 / pull
GET /v1/upcomingAnnounced but not started yet$0.01 / pull
POST /mcpMCP server — same data, same billingsee below
GET /v1/sampleFrozen fixture — build against it before you payfree
GET /v1/statusService healthfree
GET /openapi.jsonOpenAPI 3.1 specfree
GET /llms.txtWhat this is, for agentsfree

Every entry says where it came from

Each game carries the exact endpoint and test that confirmed it, plus when we first saw it — so an answer can be specific rather than a general appeal to having checked.

{
  "title": "OTXO",
  "claim_url": "https://store.epicgames.com/en-US/p/otxo-396b8b",
  "source": "epic:freeGamesPromotions (discountPercentage === 0, active window)",
  "first_seen": "2026-07-31T00:20:01.987Z"
}

There is deliberately no per-game verified_at: every entry from a store is confirmed in the same pass, so it would duplicate the store-level timestamp — and being a per-run value inside the hashed content, it would change the version every pass and bill every check. Freshness lives at the store level; origin lives on the entry.

Don’t trust us — verify us

Every response carries a version that is a SHA-256 of its own content. Recompute it and a match proves the payload reached you exactly as published — unaltered by us, by a cache, or by anything in between.

// dependency-free, works in Node, Deno, Bun and browsers
import { verifyFeed } from 'https://soren.com/verify.js';
const feed = await (await fetch('https://soren.com/v1/free-games', { headers })).json();
console.log(await verifyFeed(feed));   // { valid: true, ... }

Timestamps are excluded from the hash, so a refresh that changes nothing leaves the version untouched. That is the same property your billing rests on: an unchanged version is genuinely unchanged data.

Test all of that for free first. GET /v1/sample returns a frozen fixture with the exact shape of the paid feed, really signed with the production key — so verification code written against it works unchanged against paid data. Build your parser, your signature check and your error handling before you spend anything.

Every offer in the sample has already ended. The titles and claim URLs are real, but following one reaches a store page where the game costs money again — nothing in it is claimable, so there is nothing to gain by using it instead of the live feed. It is marked sample: true, carries a not_current warning and sends an X-Soren-Sample header. Never present its contents as games that are currently free.

MCP server

Point your agent at https://soren.com/mcp — streamable HTTP, JSON-RPC 2.0, no separate signup. Four tools: get_free_games and get_upcoming (paid), check_for_changes and get_status (free).

Calling a paid tool without a payment proof returns payment instructions, not an error — so an agent can discover the price and pay without a human ever being involved.

Status codes

CodeMeaningBilled
200Feed returned, fresh 24h token issuedyes
304Nothing changed since your versionno
402Payment needed, or the feed changedno
429Rate limited — wait Retry-After secondsno
503Feed stale or unpublished — we fail rather than serve itno

Signed, and contractually stable

Every payload carries an Ed25519 signature over its version string, with the public key at /signing-key.json. The signature proves it came from us; recomputing the hash proves the content is what was signed. verifyFully() does both.

And the shape holds still. Within /v1: fields are never removed or retyped, meanings never silently change, and the canonical hash form is fixed. Additions can appear without notice — ignore unknown fields. Breaking changes ship at /v2 with 180 days’ notice. Full contract.

Our operating record, published

/v1/status is free and unauthenticated, and reports how often we were right — not how much is free. Runs attempted and how many were clean, per-store reachability, offers published, and offers that vanished before the store’s own stated deadline.

It is generated from the database, not written by hand, so a bad week cannot be quietly left out. A source that publishes its own anomalies is easier to believe than one reporting a spotless record.

Coverage

Steam · Epic · GOG · itch.io — checked hourly. Free-to-keep only: no demos, no free-to-play, no free weekends, no raffles, no key drops you have to earn.