{
  "openapi": "3.1.0",
  "info": {
    "title": "Soren Free Games API",
    "version": "1.0.0",
    "summary": "Verified free-to-keep PC games, for autonomous agents.",
    "description": "Every PC game currently free to claim and keep permanently, each entry confirmed against the store's own API before publication. Not scraped, not aggregated.\n\nWHY THIS EXISTS: an LLM asked 'what games are free right now?' will invent an answer or repeat a stale post. This feed lets it answer correctly, including the negative case. 'Nothing is free on Steam right now' is a fact a language model cannot safely generate on its own.\n\nPROOF THE VERIFICATION MATTERS: while building this, a widely used free-games aggregator listed a title as an active Steam giveaway. Steam's own API reported the same game at $3.99 with no free-to-keep flag. That entry was discarded here because the store itself disagreed.\n\nBILLING: you are charged when the data CHANGES, never when you ask. Conditional checks are free, so polling every minute and polling once a day cost the same. Spend is bounded by how often games become free (a handful of times a day), not by your request rate \u2014 which makes autonomous spend predictable enough to pre-approve.\n\nLIFECYCLE: see x-lifecycle below for a worked 402 -> pay -> 304 -> 402 example. An agent that mismodels this will over-pay or stall.\n\nSELF-VERIFYING: the `version` field is a SHA-256 over the payload's own content (free_games plus per-store status and count; timestamps excluded). Recompute it to prove the response was not altered in transit. Reference implementation at https://soren.com/verify.js.\n\nSIGNED: every payload carries an Ed25519 signature over its version string (public key at https://soren.com/signing-key.json), so you can prove origin as well as integrity.\n\nCONTRACT: within /v1, fields are never removed or retyped and meanings never silently change. Additions may appear without notice \u2014 ignore unknown fields. Breaking changes ship at /v2 with 180 days' notice. Machine-readable in the `contract` block of every response.",
    "contact": {
      "url": "https://soren.com/contactus"
    }
  },
  "servers": [
    {
      "url": "https://soren.com"
    }
  ],
  "paths": {
    "/v1/free-games": {
      "get": {
        "summary": "Games free to claim right now, across all stores ($0.02 per pull; conditional checks free)",
        "description": "Paid per pull. With a live token and If-None-Match, an unchanged feed returns 304 free; a changed feed returns 402.",
        "parameters": [
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The version you already hold. Unchanged returns 304 and is not billed."
          },
          {
            "name": "Authorization",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Bearer token from a previous payment, valid 24h."
          },
          {
            "name": "X-Payment",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Payment proof. Presenting a valid proof returns the feed and a fresh 24h token."
          }
        ],
        "responses": {
          "200": {
            "description": "Feed payload plus a fresh 24h access token. ILLUSTRATIVE EXAMPLE - values are representative, not live data. Note gog here shows status 'unavailable' with free_count null: that means we could NOT check, and must never be rendered as 'nothing is free on GOG'.",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "The feed version. Send it back as If-None-Match."
              },
              "X-Access-Expires": {
                "schema": {
                  "type": "string",
                  "format": "date-time"
                },
                "description": "When the free-check window closes. Renew before this."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Feed"
                },
                "example": {
                  "version": "733d91c2f5353b42a77c23b560b311a1346870a351bc3bb60121abfbb78ab2d2",
                  "updated_at": "2026-07-31T15:17:52.470Z",
                  "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:00.000Z",
                      "ends_at": "2026-08-06T15:00:00.000Z",
                      "source": "epic:freeGamesPromotions (discountPercentage === 0, active window)",
                      "first_seen": "2026-07-30T15:04:11.000Z",
                      "original_price_cents": 1999,
                      "currency": "USD"
                    },
                    {
                      "title": "NIGHTBELL",
                      "store": "itch",
                      "igdb_id": null,
                      "claim_url": "https://example.itch.io/nightbell",
                      "starts_at": null,
                      "ends_at": "2026-08-09T14:57:00.000Z",
                      "source": "itch:on-sale.xml (discountpercent === 100, fullPrice > 0)",
                      "first_seen": "2026-07-30T15:04:11.000Z",
                      "original_price_cents": 499,
                      "currency": "USD"
                    }
                  ],
                  "stores": {
                    "steam": {
                      "status": "ok",
                      "free_count": 0,
                      "verified_at": "2026-07-31T15:17:52.470Z",
                      "note": "verified against steam: no free-to-keep games at this time"
                    },
                    "epic": {
                      "status": "ok",
                      "free_count": 1,
                      "verified_at": "2026-07-31T15:17:52.470Z",
                      "note": "verified against epic: 1 free-to-keep game"
                    },
                    "gog": {
                      "status": "unavailable",
                      "free_count": null,
                      "verified_at": "2026-07-31T09:02:11.000Z",
                      "note": "gog could not be reached on this pass - this is NOT a statement that nothing is free there"
                    },
                    "itch": {
                      "status": "ok",
                      "free_count": 1,
                      "verified_at": "2026-07-31T15:17:52.470Z",
                      "note": "verified against itch: 1 free-to-keep game"
                    }
                  },
                  "access": {
                    "token": "sk_live_EXAMPLE_NOT_A_REAL_TOKEN",
                    "expires_at": "2026-08-01T15:17:52.470Z"
                  }
                }
              }
            }
          },
          "304": {
            "description": "Nothing changed since your version. Empty body. NOT BILLED - the common case.",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "402": {
            "description": "Payment required - either you hold no live token, or the feed changed. NOT BILLED. Follows the x402 spec, so `error` is a STRING here, unlike other error responses. Deliberately reveals nothing about feed contents.",
            "content": {
              "application/json": {
                "example": {
                  "error": "payment_required",
                  "message": "The feed has changed. Payment required to pull it.",
                  "x402Version": 2,
                  "accepts": [
                    {
                      "scheme": "exact",
                      "network": "eip155:8453",
                      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
                      "amount": "20000",
                      "payTo": "0x...",
                      "maxTimeoutSeconds": 120,
                      "extra": {
                        "resource": "/v1/free-games"
                      }
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Rate limited. Wait Retry-After seconds."
          },
          "503": {
            "description": "Feed unpublished or stale. We fail rather than serve data we cannot vouch for."
          }
        },
        "operationId": "getFreeGames"
      }
    },
    "/v1/upcoming": {
      "get": {
        "summary": "Giveaways announced but not yet started ($0.01 per pull; conditional checks free)",
        "responses": {
          "200": {
            "description": "Announced but not yet started. ILLUSTRATIVE EXAMPLE.",
            "content": {
              "application/json": {
                "example": {
                  "version": "09df59d960066f3cb3afdecbf715d6fa963470af297a9c0e4f27d04cbc426352",
                  "updated_at": "2026-07-31T15:49:09.000Z",
                  "upcoming": [
                    {
                      "title": "Beacon Pines",
                      "store": "epic",
                      "igdb_id": null,
                      "claim_url": "https://store.epicgames.com/en-US/p/beacon-pines",
                      "starts_at": "2026-08-06T15:00:00.000Z",
                      "ends_at": "2026-08-13T15:00:00.000Z",
                      "original_price_cents": 1999,
                      "currency": "USD"
                    }
                  ]
                }
              }
            }
          },
          "402": {
            "description": "Payment required. Not billed."
          }
        },
        "operationId": "getUpcoming"
      }
    },
    "/v1/status": {
      "get": {
        "summary": "Service health and accuracy record. Free. Reports per-store verification (verified, verified_at, consecutive_unverified_runs). Deliberately excludes how many games are free.",
        "responses": {
          "200": {
            "description": "Health"
          }
        },
        "operationId": "getStatus"
      }
    },
    "/mcp": {
      "get": {
        "summary": "MCP server discovery (free) \u2014 returns usage guidance",
        "responses": {
          "200": {
            "description": "How to speak to this MCP server",
            "content": {
              "application/json": {
                "example": {
                  "name": "soren",
                  "title": "Soren",
                  "transport": "streamable-http",
                  "usage": "POST JSON-RPC 2.0 to this URL. Try {\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}."
                }
              }
            }
          }
        },
        "operationId": "getMcpDiscovery"
      },
      "post": {
        "summary": "MCP server (streamable HTTP, JSON-RPC 2.0). Same data and billing as the REST API.",
        "description": "Tools: get_free_games (paid), get_upcoming (paid), check_for_changes (free), get_status (free). Calling a paid tool without payment_proof returns payment instructions rather than an error, so an agent can discover the price and pay unattended. MCP has no conditional-request mechanism, so the REST API's free-304 behaviour is surfaced as the explicit check_for_changes tool.",
        "responses": {
          "200": {
            "description": "JSON-RPC response"
          },
          "202": {
            "description": "Notification accepted, no body"
          },
          "429": {
            "description": "Rate limited"
          }
        },
        "operationId": "callMcp"
      }
    },
    "/contactus/send": {
      "post": {
        "summary": "Report an error in the data (free, no authentication)",
        "description": "The reporting path for a wrong entry \u2014 a game that was never actually claimable, a bad end date, a dead claim URL. Usable by agents as well as humans: it is a plain form POST, not a web form that requires a browser. Confirmed errors are published in the corrections list at /v1/status.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "message"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Where we can reply."
                  },
                  "message": {
                    "type": "string",
                    "description": "Title, store, what we published, what you observed."
                  },
                  "name": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received"
          },
          "400": {
            "description": "Invalid email or message"
          }
        },
        "operationId": "submitContactForm"
      }
    },
    "/v1/report": {
      "post": {
        "summary": "Report an error in the data (free, unauthenticated, machine-facing)",
        "description": "Tell us an entry is wrong \u2014 a game that was never claimable, a bad end date, a dead claim URL. Accepts JSON or form encoding. Free and unauthenticated: gating the channel through which someone reports an error would be self-defeating. Reports go to a review channel rather than email, so volume is not a problem \u2014 send one whenever something looks wrong. Confirmed errors appear in the corrections list at /v1/status.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "observed"
                ],
                "properties": {
                  "observed": {
                    "type": "string",
                    "description": "Required. What you found that contradicts our data."
                  },
                  "title": {
                    "type": "string"
                  },
                  "store": {
                    "type": "string",
                    "enum": [
                      "steam",
                      "epic",
                      "gog",
                      "itch"
                    ]
                  },
                  "claim_url": {
                    "type": "string"
                  },
                  "expected": {
                    "type": "string"
                  },
                  "version": {
                    "type": "string",
                    "description": "The feed version you were holding."
                  },
                  "reporter": {
                    "type": "string",
                    "description": "Optional, if we may need detail."
                  },
                  "original_price_cents": 499,
                  "currency": "USD"
                }
              },
              "example": {
                "title": "Example Game",
                "store": "steam",
                "observed": "Steam shows this at $9.99; your feed lists it as free to keep.",
                "version": "733d91c2..."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received"
          },
          "400": {
            "description": "Missing `observed`; the response lists every accepted field"
          },
          "429": {
            "description": "Rate limited"
          }
        },
        "operationId": "reportError"
      }
    },
    "/v1/sample": {
      "get": {
        "summary": "Frozen test fixture \u2014 free, real shape, every offer already ended",
        "description": "A free, immutable sample of the /v1/free-games payload so you can build and test an integration before paying for anything. Identical field names, types and hash canonicalisation to the live feed, and really signed with the production key \u2014 so verification code tested against this works unchanged against paid data.\n\nEvery offer in it has already ended. The titles and claim URLs are real, but following one reaches a store page where the game costs money again, so there is nothing to claim and nothing to gain by using this instead of the live feed.\n\nFrozen on purpose: it does not roll forward, so assertions written against it keep passing. Do NOT present its contents to a user as currently-free games \u2014 the `sample` and `not_current` fields, and the X-Soren-Sample response header, all say so.",
        "operationId": "getSample",
        "tags": [
          "Free"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "The frozen fixture.",
            "headers": {
              "X-Soren-Sample": {
                "schema": {
                  "type": "string"
                },
                "description": "Always present. Marks the response as a fixture, for clients that never parse the body."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Feed"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "sample": {
                          "type": "boolean",
                          "const": true
                        },
                        "not_current": {
                          "type": "string",
                          "description": "Human-readable warning that every offer has ended."
                        },
                        "frozen_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/game": {
      "get": {
        "summary": "Cross-store price with observed history ($0.01 per lookup)",
        "description": "Current price across Steam, GOG, Epic and Microsoft for one game, with the lowest price we have OBSERVED and the window we observed it in. Never called an all-time low: a low computed from three weeks and one computed from fourteen years are different claims, so observed_since and observations always travel with it. No buy/wait verdict is given \u2014 the numbers are sourced, the judgement is the caller's. Resolve by slug, Steam appid, GOG id, Epic id, or a free-text name. Freshness is uneven and stated per row: checked_at and changed_at are separate fields on every price. The demand head refreshes every 4 hours, Epic and GOG catalogues daily, and the 173,290-product Steam tail rotates over a few days. No single freshness figure is claimed because none would be true of all of them.",
        "parameters": [
          {
            "name": "game",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Soren slug, e.g. the-witcher-3-wild-hunt"
          },
          {
            "name": "steam",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Steam appid, e.g. 1145360"
          },
          {
            "name": "gog",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "GOG product id"
          },
          {
            "name": "epic",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Epic offer id"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Free-text title. Punctuation- and diacritic-insensitive: 'half life 2' finds Half-Life 2, 'wiedzmin' finds The Witcher 3."
          },
          {
            "name": "region",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "US"
            },
            "description": "ISO 3166-1 alpha-2. Non-US regions return prices but no lowest_observed: the observation series is USD-only, and comparing a BRL price to a USD low would be meaningless."
          },
          {
            "name": "family",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "full",
                "none"
              ],
              "default": "full"
            },
            "description": "Related editions, expansions and DLC. Returned whole, never truncated."
          },
          {
            "name": "include",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "mods"
              ]
            },
            "description": "Include mods, updates and forks in the family. Excluded by default."
          }
        ],
        "responses": {
          "200": {
            "description": "Price answer. ILLUSTRATIVE EXAMPLE.",
            "content": {
              "application/json": {
                "example": {
                  "game": {
                    "id": "20",
                    "slug": "fallout-new-vegas",
                    "title": "Fallout: New Vegas",
                    "type": "main_game",
                    "released_at": null,
                    "store_ids": {
                      "epic": {
                        "id": "68282d75318a4e47b5bacf1994f648d3",
                        "url": "https://store.epicgames.com/en-US/p/fallout-new-vegas"
                      },
                      "microsoft": {
                        "id": "xbox36066acd000-77fe-1000-9115-d802425307e0",
                        "url": "http://marketplace.xbox.com/en-US/Product/Fallout-New-Vegas/66acd000-77fe-1000-9115-d802425307e0"
                      },
                      "steam": {
                        "id": "22490",
                        "url": "https://store.steampowered.com/app/22490"
                      }
                    }
                  },
                  "prices": [
                    {
                      "store": "steam",
                      "region": "US",
                      "status": "ok",
                      "currency": "USD",
                      "price": 4.99,
                      "regular_price": 9.99,
                      "discount_pct": 50,
                      "lowest_observed": 0.99,
                      "lowest_observed_at": "2026-03-19T18:41:47.000Z",
                      "pct_above_lowest_observed": 404,
                      "observed_since": "2012-10-29T17:23:10.000Z",
                      "observations": 392,
                      "checked_at": "2026-08-04T18:54:14.753Z",
                      "changed_at": "2026-08-03T21:21:04.338Z"
                    },
                    {
                      "store": "epic",
                      "region": "US",
                      "status": "ok",
                      "currency": "USD",
                      "price": 4.99,
                      "regular_price": 9.99,
                      "discount_pct": 50,
                      "lowest_observed": null,
                      "lowest_observed_at": null,
                      "pct_above_lowest_observed": null,
                      "observed_since": null,
                      "observations": 0,
                      "lowest_unavailable_reason": "no price history recorded for this game yet",
                      "checked_at": "2026-08-04T22:30:56.791Z",
                      "changed_at": "2026-08-04T20:41:56.284Z"
                    }
                  ],
                  "coverage": [
                    {
                      "store": "steam",
                      "from": "2012-03-29",
                      "to": "2026-08-04",
                      "source": "soren-legacy"
                    },
                    {
                      "store": "steam",
                      "from": "2026-08-04",
                      "to": null,
                      "source": "soren-collector"
                    }
                  ],
                  "region": "US",
                  "family": {
                    "root_id": "20",
                    "count": 7,
                    "members": [
                      {
                        "id": "9907",
                        "slug": "fallout-new-vegas-dead-money",
                        "title": "Fallout: New Vegas - Dead Money",
                        "type": "dlc",
                        "status": "not_tracked",
                        "price": null,
                        "discount_pct": 0
                      },
                      {
                        "id": "9910",
                        "slug": "fallout-new-vegas-lonesome-road",
                        "title": "Fallout: New Vegas - Lonesome Road",
                        "type": "dlc",
                        "status": "not_tracked",
                        "price": null,
                        "discount_pct": 0
                      }
                    ]
                  },
                  "generated_at": "2026-08-05T02:05:06.087Z",
                  "data_as_of": "2026-08-05T01:42:45.786Z"
                }
              }
            }
          },
          "402": {
            "description": "Payment required. x402 challenge in the body."
          },
          "404": {
            "description": "No game matches that identifier. found:false \u2014 a real answer, not an error."
          }
        },
        "operationId": "getGame"
      }
    },
    "/v1/lookup": {
      "get": {
        "summary": "Resolve a game name to a slug. Free.",
        "description": "Free and unauthenticated. Returns games matching a name with their slug, release date and type \u2014 no prices and no store identifiers. Call this first when you hold a title but no identifier, then pass the slug to /v1/game. Matching folds case, punctuation, diacritics, alternate titles and abbreviations. 13,604 titles in the catalogue are duplicated, so release date and type are what distinguish them.",
        "operationId": "lookupGame",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Game name to resolve.",
            "example": "forza horizon"
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated filter: main_game, dlc, expansion, port, remaster, \u2026",
            "example": "main_game"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 100
            },
            "description": "Maximum matches to return."
          }
        ],
        "responses": {
          "200": {
            "description": "Matches, most relevant first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "query": {
                      "type": "string"
                    },
                    "matched": {
                      "type": "integer",
                      "description": "How many exist, not just how many were returned."
                    },
                    "returned": {
                      "type": "integer"
                    },
                    "matches": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "released_at": {
                            "type": "string",
                            "nullable": true,
                            "format": "date"
                          },
                          "type": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "price": {
                      "type": "string",
                      "example": "free"
                    },
                    "next": {
                      "type": "string"
                    },
                    "attribution": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing q, or an unrecognised type."
          },
          "429": {
            "description": "Rate limited. Free, but not unmetered."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Feed": {
        "type": "object",
        "required": [
          "version",
          "updated_at",
          "free_games",
          "stores"
        ],
        "properties": {
          "version": {
            "type": "string",
            "description": "SHA-256 of the canonical form {\"offers\":<free_games as published>,\"coverage\":[[store,status,free_count],...] sorted by store}, hex-encoded. Changes only when content changes, never on a mere refresh \u2014 so an unchanged version means nothing to do, and a mismatch means the payload was modified after publication."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "free_games": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Offer"
            }
          },
          "stores": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/StoreStatus"
            }
          },
          "verification": {
            "type": "object",
            "description": "How to recompute `version` yourself. Present so integrity can be checked rather than trusted."
          },
          "signature": {
            "type": [
              "object",
              "null"
            ],
            "description": "Ed25519 signature over the `version` string. Public key at https://soren.com/signing-key.json. Proves the payload originated from Soren; combine with recomputing the hash, which proves the content matches what was signed."
          },
          "contract": {
            "type": "object",
            "description": "Machine-readable statement of what is guaranteed within this major version: fields never removed or retyped, additions possible without notice, meanings never silently changed, canonical hash form fixed, 180 days' deprecation notice. Full text at https://soren.com/contract.md."
          }
        }
      },
      "Offer": {
        "type": "object",
        "required": [
          "title",
          "store",
          "claim_url"
        ],
        "properties": {
          "title": {
            "type": "string"
          },
          "store": {
            "type": "string",
            "enum": [
              "steam",
              "epic",
              "gog",
              "itch"
            ]
          },
          "igdb_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "claim_url": {
            "type": "string",
            "format": "uri"
          },
          "starts_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Null where the store does not publish a start time. Never substituted with our detection time."
          },
          "ends_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "original_price_cents": {
            "type": [
              "integer",
              "null"
            ],
            "description": "What the game normally costs, in minor units of `currency` (1999 = $19.99). Lets you rank by value rather than by row count \u2014 \"3 games worth $60 are free\" is a more useful answer than \"20 games are free\", and this catalogue is dominated by low-priced indie titles. null means the store does not publish a price, which is NOT the same as zero: a permanently free-to-play title would be 0.",
            "example": 1999
          },
          "currency": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 4217 code for original_price_cents. null when the price is unknown.",
            "example": "USD"
          },
          "source": {
            "type": "string",
            "description": "The exact endpoint and test that confirmed this entry, e.g. 'epic:freeGamesPromotions (discountPercentage === 0, active window)'. Static per store, so it is included in the version hash."
          },
          "first_seen": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When this offer was first observed. Set once and stable thereafter. 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 version on every pass and bill every conditional poll."
          }
        }
      },
      "StoreStatus": {
        "type": "object",
        "description": "The authoritative per-store answer. free_count null is NOT zero: 'ok' with 0 means we checked and nothing is free; 'unavailable' means we could not check, and must never be rendered as 'nothing is free'.",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "unavailable",
              "not_configured"
            ]
          },
          "free_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "verified_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "note": {
            "type": "string"
          }
        }
      }
    }
  },
  "x-lifecycle": {
    "summary": "Worked example of the full request lifecycle. Follow this exactly.",
    "steps": [
      {
        "step": 1,
        "request": "GET /v1/free-games",
        "response": "402 Payment Required",
        "body": {
          "error": {
            "code": "payment_required"
          },
          "payment": {
            "amount": "0.02",
            "asset": "USDC",
            "recipient": "0x...",
            "nonce": "..."
          }
        },
        "note": "No credentials. Not billed. The challenge tells you what to pay."
      },
      {
        "step": 2,
        "request": "GET /v1/free-games  with header  X-Payment: <proof>",
        "response": "200 OK",
        "headers": {
          "ETag": "\"733d91c2...\"",
          "X-Access-Expires": "2026-08-01T15:00:00Z"
        },
        "body_excerpt": {
          "version": "733d91c2...",
          "free_games": [
            "..."
          ],
          "stores": {
            "...": "..."
          },
          "access": {
            "token": "sk_live_...",
            "expires_at": "2026-08-01T15:00:00Z"
          }
        },
        "note": "BILLED. Store BOTH the token and the version \u2014 you need both for step 3."
      },
      {
        "step": 3,
        "request": "GET /v1/free-games  with  Authorization: Bearer <token>  and  If-None-Match: \"733d91c2...\"",
        "response": "304 Not Modified",
        "body": null,
        "note": "FREE, and this is the common case. Nothing changed. Keep your cached copy. Repeat as often as you like \u2014 it costs nothing."
      },
      {
        "step": 4,
        "request": "same as step 3, once the feed has changed",
        "response": "402 Payment Required",
        "headers": {
          "ETag": "\"d41d8c...\""
        },
        "note": "FREE. The feed changed. The response deliberately does not say what changed or which store \u2014 pay to see it."
      },
      {
        "step": 5,
        "request": "GET /v1/free-games  with  X-Payment: <new proof>",
        "response": "200 OK",
        "note": "BILLED. Returns the new data and a FRESH 24h token. The cycle returns to step 3."
      }
    ],
    "rules": [
      "Never billed: 304, 402, 429, 4xx, 5xx. Only a 200 carrying data is charged.",
      "Always send If-None-Match. It is free, faster, and the reason your bill stays flat.",
      "The token expires 24h after the last payment. X-Access-Expires is on every response \u2014 renew before it, do not wait for a 402.",
      "If a 200 is lost in transit, retry with the SAME payment proof. Idempotency is keyed on the transaction id, so you will not be charged twice.",
      "On 429, wait exactly Retry-After seconds (an integer, not a date).",
      "free_count: null is NOT zero. status 'ok' with 0 means verified-nothing-free; 'unavailable' means we could not check and you must not report it as nothing free."
    ]
  }
}