{
  "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 — 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 — 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) — 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 — 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 — 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 — 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 — 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 — 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 — 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 — 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 — 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 — 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, …",
            "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."
          }
        }
      }
    },
    "/v1/testpayment": {
      "get": {
        "operationId": "testPayment",
        "summary": "Paid x402 sandbox — prove your client can pay",
        "description": "Pay $0.10 in USDC on Base and receive your own settled payment back so you can assert your x402 integration works. Scenario parameters exercise failure handling for free.",
        "parameters": [
          {
            "name": "scenario",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "success",
                "price",
                "underpaid",
                "expired",
                "settle_fail"
              ]
            },
            "description": "Force a path. Only success (and a paid price scenario) charge; the rest are simulated locally and cost nothing."
          },
          {
            "name": "price",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "With scenario=price, the amount to quote. Clamped to $5.00."
          }
        ],
        "responses": {
          "200": {
            "description": "Payment settled. Your client works.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "payer": {
                      "type": "string"
                    },
                    "amount": {
                      "type": "string"
                    },
                    "asset": {
                      "type": "string"
                    },
                    "network": {
                      "type": "string"
                    },
                    "settlement": {
                      "type": "string"
                    },
                    "resource": {
                      "type": "string"
                    },
                    "timestamp": {
                      "type": "string"
                    },
                    "scenario": {
                      "type": "string"
                    },
                    "next_steps": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Payment required. Requirements are in the PAYMENT-REQUIRED header (x402 v2) and the body (v1).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "x402Version": {
                      "type": "integer"
                    },
                    "error": {
                      "type": "string"
                    },
                    "accepts": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/telljoke": {
      "get": {
        "operationId": "tellJoke",
        "summary": "One joke setup. There is no punchline.",
        "description": "Returns a joke setup with no punchline — permanently, by design, stated in machine-readable fields on every response. First call from a wallet $0.05, every call after $0.50, capped at $10.05 per wallet per 4 hours. Non-refundable. See https://soren.com/telljoke",
        "responses": {
          "200": {
            "description": "Paid. A setup, and no punchline.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "setup": {
                      "type": "string"
                    },
                    "punchline": {
                      "type": "string",
                      "nullable": true
                    },
                    "punchline_exists": {
                      "type": "boolean"
                    },
                    "permanent": {
                      "type": "boolean"
                    },
                    "retrying_will_not_help": {
                      "type": "boolean"
                    },
                    "note": {
                      "type": "string"
                    },
                    "call_index": {
                      "type": "integer"
                    },
                    "amount_charged": {
                      "type": "string"
                    },
                    "next_call_price": {
                      "type": "string",
                      "nullable": true
                    },
                    "next_call_note": {
                      "type": "string"
                    },
                    "window_resets_at": {
                      "type": "string"
                    },
                    "settlement": {
                      "type": "string"
                    },
                    "docs": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Payment required. Requirements are in the PAYMENT-REQUIRED header (x402 v2) and the body (v1).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "x402Version": {
                      "type": "integer"
                    },
                    "error": {
                      "type": "string"
                    },
                    "accepts": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/oracle/weather": {
      "get": {
        "operationId": "oracleWeather",
        "summary": "Signed NOAA weather observation for a US coordinate",
        "description": "Latest US National Weather Service station observation, returned with a SHA-256 of the exact upstream bytes, the moment Soren read them, and an Ed25519 signature over both. Public-domain data; the attestation is what is sold. Soren signs, it does not originate. Verify with https://soren.com/oracle-key.json",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number"
            },
            "description": "Latitude, decimal degrees. US coverage only.",
            "example": 38.8894
          },
          {
            "name": "lon",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number"
            },
            "description": "Longitude, decimal degrees. US coverage only.",
            "example": -77.0352
          }
        ],
        "responses": {
          "200": {
            "description": "Signed observation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "value": {
                      "type": "object"
                    },
                    "source": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "url": {
                          "type": "string"
                        },
                        "licence": {
                          "type": "string"
                        },
                        "notice": {
                          "type": "string"
                        }
                      }
                    },
                    "provenance": {
                      "type": "object",
                      "properties": {
                        "resource": {
                          "type": "string"
                        },
                        "upstream_sha256": {
                          "type": "string"
                        },
                        "observed_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "signature": {
                          "type": "object",
                          "properties": {
                            "algorithm": {
                              "type": "string"
                            },
                            "over": {
                              "type": "string"
                            },
                            "value": {
                              "type": "string"
                            },
                            "public_key": {
                              "type": "string"
                            }
                          }
                        },
                        "verify": {
                          "type": "string"
                        }
                      }
                    },
                    "attestation": {
                      "type": "string"
                    },
                    "settlement": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid coordinates. Refused before payment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "402": {
            "description": "Payment required. Requirements in the PAYMENT-REQUIRED header (x402 v2) and the body (v1).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No observation station covers that point.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "The upstream source did not answer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/oracle/fx": {
      "get": {
        "operationId": "oracleFx",
        "summary": "Signed ECB euro reference exchange rate",
        "description": "European Central Bank euro reference rate for any pair among the ~30 currencies the ECB publishes, returned with a SHA-256 of the exact upstream bytes, the read time, and an Ed25519 signature. Non-EUR pairs are crossed via EUR and the derivation is stated, because that cross is Soren's calculation and not an ECB figure. The ECB publishes these free of charge; the attestation is what is sold.",
        "parameters": [
          {
            "name": "pair",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Six-letter currency pair, e.g. EURUSD.",
            "example": "EURUSD"
          }
        ],
        "responses": {
          "200": {
            "description": "Signed rate.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "value": {
                      "type": "object",
                      "properties": {
                        "pair": {
                          "type": "string"
                        },
                        "base": {
                          "type": "string"
                        },
                        "quote": {
                          "type": "string"
                        },
                        "rate": {
                          "type": "number"
                        },
                        "reference_date": {
                          "type": "string"
                        },
                        "derivation": {
                          "type": "string"
                        },
                        "note": {
                          "type": "string"
                        }
                      }
                    },
                    "source": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "url": {
                          "type": "string"
                        },
                        "licence": {
                          "type": "string"
                        },
                        "notice": {
                          "type": "string"
                        }
                      }
                    },
                    "provenance": {
                      "type": "object",
                      "properties": {
                        "resource": {
                          "type": "string"
                        },
                        "upstream_sha256": {
                          "type": "string"
                        },
                        "observed_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "signature": {
                          "type": "object",
                          "properties": {
                            "algorithm": {
                              "type": "string"
                            },
                            "over": {
                              "type": "string"
                            },
                            "value": {
                              "type": "string"
                            },
                            "public_key": {
                              "type": "string"
                            }
                          }
                        },
                        "verify": {
                          "type": "string"
                        }
                      }
                    },
                    "attestation": {
                      "type": "string"
                    },
                    "settlement": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or malformed pair. Refused before payment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "402": {
            "description": "Payment required. Requirements in the PAYMENT-REQUIRED header (x402 v2) and the body (v1).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Currency not in the ECB reference set.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "The ECB did not answer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/oracle/random": {
      "get": {
        "operationId": "oracleRandom",
        "summary": "Signed, publicly verifiable randomness from drand",
        "description": "A round from the drand League of Entropy beacon, attested and timestamped. One signed value every 30 seconds from a distributed network; no single participant, including Soren, can choose or predict it. Soren recomputes SHA-256 of the BLS signature and refuses to attest to a round that does not verify. The same round can be fetched free from api.drand.sh — what is sold is the signed evidence that Soren committed to this value at this moment.",
        "parameters": [
          {
            "name": "round",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Specific drand round. Omit for the latest.",
            "example": 6391052
          }
        ],
        "responses": {
          "200": {
            "description": "Verified round, attested.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "value": {
                      "type": "object",
                      "properties": {
                        "round": {
                          "type": "integer"
                        },
                        "randomness": {
                          "type": "string"
                        },
                        "signature": {
                          "type": "string"
                        },
                        "as_uint53": {
                          "type": "integer"
                        },
                        "as_float": {
                          "type": "number"
                        },
                        "verify_yourself": {
                          "type": "string"
                        }
                      }
                    },
                    "source": {
                      "type": "object"
                    },
                    "provenance": {
                      "type": "object"
                    },
                    "attestation": {
                      "type": "string"
                    },
                    "settlement": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "round must be a positive integer. Refused before payment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "402": {
            "description": "Payment required.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such round.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "502": {
            "description": "The beacon failed its own self-check and was not attested.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "The beacon did not answer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/oracle/stat": {
      "get": {
        "operationId": "oracleStat",
        "summary": "Signed US economic statistic from the BLS",
        "description": "Latest value plus about a year of history for a named BLS series, with a SHA-256 of the exact upstream bytes, the read time and an Ed25519 signature. Public domain data (17 U.S.C. 105); the attestation is what is sold, and it matters because BLS revises its series.",
        "parameters": [
          {
            "name": "series",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "unemployment_rate",
                "cpi",
                "nonfarm_payroll",
                "labor_force_participation",
                "avg_hourly_earnings"
              ]
            },
            "description": "Which series to read.",
            "example": "unemployment_rate"
          }
        ],
        "responses": {
          "200": {
            "description": "Signed statistic.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "value": {
                      "type": "object"
                    },
                    "source": {
                      "type": "object"
                    },
                    "provenance": {
                      "type": "object"
                    },
                    "attestation": {
                      "type": "string"
                    },
                    "settlement": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or unknown series. Refused before payment; lists the supported series.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "402": {
            "description": "Payment required.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "BLS returned no observations.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "BLS unavailable or declined to process.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conformance": {
      "get": {
        "operationId": "conformanceIndex",
        "summary": "Index of the free x402 conformance cases",
        "description": "Machine-readable list of every failure case, what it injects and what a correct client should do. Free.",
        "responses": {
          "200": {
            "description": "The suite.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "suite": {
                      "type": "string"
                    },
                    "free": {
                      "type": "boolean"
                    },
                    "cases": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "case": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "expect": {
                            "type": "string"
                          },
                          "client_should": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conformance/{case}": {
      "get": {
        "operationId": "conformanceCase",
        "summary": "Inject one documented x402 failure",
        "description": "Returns the documented failure for the named case. Nothing is verified, settled or charged — every response carries simulated:true, charged:false and the guidance for that case.",
        "parameters": [
          {
            "name": "case",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "underpaid",
                "overpaid",
                "expired",
                "replayed-nonce",
                "wrong-network",
                "wrong-asset",
                "malformed-header",
                "settle-timeout",
                "double-402"
              ]
            },
            "description": "Which failure to inject."
          }
        ],
        "responses": {
          "402": {
            "description": "The injected failure, with expect/client_should guidance.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Unknown case; lists the valid ones.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/echo-payment": {
      "get": {
        "operationId": "echoPayment",
        "summary": "Decode and echo your own signed authorization ($0.02)",
        "description": "Decodes YOUR signed EIP-3009 authorization and returns it field by field — payer, value, recipient, validity window, nonce — for debugging a signer that keeps being rejected.",
        "responses": {
          "200": {
            "description": "Settled and served.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "402": {
            "description": "Payment required.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (unpaid requests only).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/paid-ping": {
      "get": {
        "operationId": "paidPing",
        "summary": "Paid liveness beacon ($0.01)",
        "description": "A heartbeat that costs money, proving verification and settlement are working end to end rather than merely that the server answers.",
        "responses": {
          "200": {
            "description": "Settled and served.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "402": {
            "description": "Payment required.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (unpaid requests only).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/roundtrip": {
      "get": {
        "operationId": "roundtrip",
        "summary": "Paid ping with measured settlement latency ($0.01)",
        "description": "As paid-ping, plus how long verification and settlement actually took server-side, for choosing client timeouts from evidence.",
        "responses": {
          "200": {
            "description": "Settled and served.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "402": {
            "description": "Payment required.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (unpaid requests only).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/oracle/onchain": {
      "get": {
        "operationId": "oracleOnchain",
        "summary": "Signed read from Base mainnet",
        "description": "Whether a transaction landed and in which block, the ETH and USDC balance of an address, or current block height and gas price — each with a SHA-256 of the exact node response, the read time and an Ed25519 signature. The tx query resolves an x402 settlement_pending: found:false means not in a block (pending, dropped or never broadcast), which is not the same as failed.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "tx",
                "balance",
                "block"
              ]
            },
            "description": "Which query.",
            "example": "tx"
          },
          {
            "name": "hash",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "With q=tx, the 32-byte transaction hash."
          },
          {
            "name": "address",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "With q=balance, the address to read."
          }
        ],
        "responses": {
          "200": {
            "description": "Signed read.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Unknown query or malformed hash/address. Refused before payment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "402": {
            "description": "Payment required.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "503": {
            "description": "The node did not answer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/attestation": {
      "get": {
        "operationId": "attestation",
        "summary": "Signed proof a hash existed by a given moment ($0.02)",
        "description": "Returns a signed, timestamped statement that a SHA-256 was presented to Soren at a specific moment. You hash your own content and send only the hash — Soren never receives it. Proves existence no later than the timestamp; not authorship, not truth, not legal notarisation.",
        "parameters": [
          {
            "name": "hash",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            },
            "description": "SHA-256 of your content, 64 lowercase hex characters."
          },
          {
            "name": "note",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Optional text included in the signed statement."
          }
        ],
        "responses": {
          "200": {
            "description": "Signed attestation.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "attested": {
                      "type": "boolean"
                    },
                    "hash": {
                      "type": "string"
                    },
                    "observed_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "statement": {
                      "type": "string"
                    },
                    "signature": {
                      "type": "object"
                    },
                    "what_this_proves": {
                      "type": "string"
                    },
                    "what_this_does_not_prove": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or malformed hash. Refused before payment.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "402": {
            "description": "Payment required.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/secret": {
      "post": {
        "operationId": "secretStore",
        "summary": "Store a one-read secret ($0.03)",
        "description": "POST the secret as the raw body. Returns a URL readable exactly once — the read destroys it before responding. The reader pays nothing. Max 4096 bytes; ttl 60s to 24h. Encrypted at rest with an AES-256-GCM key returned to you once and not kept, so a breach of our storage yields ciphertext; pass blind=true to store your own ciphertext untouched instead.",
        "parameters": [
          {
            "name": "ttl",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 60,
              "maximum": 86400
            },
            "description": "Seconds until it self-destructs unread. Default 3600."
          },
          {
            "name": "blind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Store the body untouched with no server-side encryption. Use when you have already encrypted and want Soren to generate no key at all."
          }
        ],
        "responses": {
          "200": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stored": {
                      "type": "boolean"
                    },
                    "read_url": {
                      "type": "string"
                    },
                    "token": {
                      "type": "string"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "reads_allowed": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Empty body.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "402": {
            "description": "Payment required.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "413": {
            "description": "Over 4096 bytes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/secret/{token}": {
      "get": {
        "operationId": "secretRead",
        "summary": "Read a secret once, destroying it (free)",
        "description": "Returns the stored secret and deletes it before responding. Free — the reader needs no wallet. Ciphertext by default; pass key=<hex> to have Soren decrypt server-side (convenient, but then Soren sees the key). Already-read, expired and never-existed are deliberately indistinguishable.",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            },
            "description": "The one-read token."
          },
          {
            "name": "key",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[0-9a-f]{64}$"
            },
            "description": "Optional decryption key returned to the sender. Supplying it decrypts server-side; omit it to receive ciphertext and decrypt locally."
          }
        ],
        "responses": {
          "200": {
            "description": "The secret. It is now destroyed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "secret": {
                      "type": "string"
                    },
                    "read_at": {
                      "type": "string"
                    },
                    "destroyed": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Already read, expired, or never existed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/v1/coord/session": {
      "post": {
        "operationId": "coordSessionBuy",
        "summary": "Buy a coordination session ($0.05 / $0.25 / $1.00)",
        "description": "Buys an operation budget for the coordination primitives. Paid ONCE, because on-chain settlement takes about 1.9 seconds and a lock that slow is not a lock; every primitive call afterwards runs at ordinary HTTP latency (57ms median, measured). Tiers: 100 ops for $0.05 valid 1h, 1000 for $0.25 valid 24h, 5000 for $1.00 valid 7d. The session token is a bearer credential. Names are namespaced to the paying wallet. Renewing and releasing a lock you already hold never costs an operation.",
        "parameters": [
          {
            "name": "ops",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "enum": [
                100,
                1000,
                5000
              ],
              "default": 1000
            },
            "description": "Which tier to buy."
          }
        ],
        "responses": {
          "200": {
            "description": "Result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Unknown tier."
          },
          "402": {
            "description": "Payment required, or the session budget is exhausted."
          }
        }
      },
      "get": {
        "operationId": "coordSessionState",
        "summary": "Remaining operations and expiry (free)",
        "description": "Costs no operations.",
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Bearer <session_token> from POST /v1/coord/session."
          }
        ],
        "responses": {
          "200": {
            "description": "Result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "No valid session token."
          },
          "402": {
            "description": "Payment required, or the session budget is exhausted."
          }
        }
      }
    },
    "/v1/coord/lock/{name}": {
      "post": {
        "operationId": "coordLockAcquire",
        "summary": "Acquire a lock or semaphore permit (1 operation)",
        "description": "limit=1 is a mutex; limit>1 is a semaphore. Returns a strictly increasing fence. IMPORTANT: a TTL lock alone does NOT give mutual exclusion — a holder stalled past its TTL wakes still believing it holds the lock while another agent has legitimately taken it. Carry the fence into the resource you are protecting and reject lower values, or what you have is an advisory lock. A blocked acquire still costs an operation and returns retry_after; you are never charged when Soren itself fails. Re-acquiring as the same holder extends the permit rather than consuming a second one.",
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Bearer <session_token> from POST /v1/coord/session."
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Your name for this lock/bucket/sequence. Namespaced to your wallet; never persisted."
          },
          {
            "name": "ns",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "default"
            },
            "description": "Optional sub-namespace, for running two independent fleets from one wallet."
          },
          {
            "name": "holder",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A stable id for this agent. Identifies who holds the permit so renew and release work."
          },
          {
            "name": "ttl",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 60,
              "minimum": 1,
              "maximum": 3600
            },
            "description": "Seconds until the permit lapses if not renewed."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1,
              "maximum": 1000
            },
            "description": "Concurrent holders allowed. 1 is a mutex."
          }
        ],
        "responses": {
          "200": {
            "description": "Result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "Missing holder or name."
          },
          "401": {
            "description": "No valid session token."
          },
          "402": {
            "description": "Payment required, or the session budget is exhausted."
          },
          "503": {
            "description": "Coordination object unreachable. No operation charged."
          }
        }
      },
      "get": {
        "operationId": "coordLockPeek",
        "summary": "Who holds it, without acquiring (1 operation)",
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Bearer <session_token> from POST /v1/coord/session."
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Your name for this lock/bucket/sequence. Namespaced to your wallet; never persisted."
          },
          {
            "name": "ns",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "default"
            },
            "description": "Optional sub-namespace, for running two independent fleets from one wallet."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "No valid session token."
          },
          "402": {
            "description": "Payment required, or the session budget is exhausted."
          }
        }
      },
      "delete": {
        "operationId": "coordLockRelease",
        "summary": "Release your permit (free)",
        "description": "Costs no operations, deliberately: a fleet that exhausted its budget must still be able to let go of what it holds.",
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Bearer <session_token> from POST /v1/coord/session."
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Your name for this lock/bucket/sequence. Namespaced to your wallet; never persisted."
          },
          {
            "name": "ns",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "default"
            },
            "description": "Optional sub-namespace, for running two independent fleets from one wallet."
          },
          {
            "name": "holder",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "No valid session token."
          },
          "402": {
            "description": "Payment required, or the session budget is exhausted."
          }
        }
      }
    },
    "/v1/coord/lock/{name}/renew": {
      "post": {
        "operationId": "coordLockRenew",
        "summary": "Extend a permit you hold (free)",
        "description": "Costs no operations. A response of renewed=false with reason not_held means your lease lapsed and someone else may hold it — stop what you are doing rather than re-acquiring blindly.",
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Bearer <session_token> from POST /v1/coord/session."
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Your name for this lock/bucket/sequence. Namespaced to your wallet; never persisted."
          },
          {
            "name": "ns",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "default"
            },
            "description": "Optional sub-namespace, for running two independent fleets from one wallet."
          },
          {
            "name": "holder",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ttl",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 60
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "No valid session token."
          },
          "402": {
            "description": "Payment required, or the session budget is exhausted."
          }
        }
      }
    },
    "/v1/coord/quota/{name}": {
      "post": {
        "operationId": "coordQuota",
        "summary": "Consume from a rate limit shared across the fleet (1 operation)",
        "description": "A token bucket every agent in your fleet draws from, so nine agents can share one upstream API quota. Returns granted with the remainder, or retry_after_ms.",
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Bearer <session_token> from POST /v1/coord/session."
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Your name for this lock/bucket/sequence. Namespaced to your wallet; never persisted."
          },
          {
            "name": "ns",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "default"
            },
            "description": "Optional sub-namespace, for running two independent fleets from one wallet."
          },
          {
            "name": "rate",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 60
            },
            "description": "Tokens refilled per window."
          },
          {
            "name": "per",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 60
            },
            "description": "Window length in seconds."
          },
          {
            "name": "burst",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Bucket capacity. Defaults to rate."
          },
          {
            "name": "n",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            },
            "description": "Tokens to take."
          }
        ],
        "responses": {
          "200": {
            "description": "Result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "No valid session token."
          },
          "402": {
            "description": "Payment required, or the session budget is exhausted."
          }
        }
      }
    },
    "/v1/coord/sequence/{name}": {
      "post": {
        "operationId": "coordSequence",
        "summary": "Next unique, strictly increasing number (1 operation)",
        "description": "Unique and strictly increasing — NOT gapless. A lost response burns a value, so you can legitimately observe a gap; anything needing gapless numbering must reconcile at your end.",
        "parameters": [
          {
            "name": "Authorization",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Bearer <session_token> from POST /v1/coord/session."
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Your name for this lock/bucket/sequence. Namespaced to your wallet; never persisted."
          },
          {
            "name": "ns",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "default"
            },
            "description": "Optional sub-namespace, for running two independent fleets from one wallet."
          },
          {
            "name": "start",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 1
            },
            "description": "First value, used only when the sequence is created."
          }
        ],
        "responses": {
          "200": {
            "description": "Result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "No valid session token."
          },
          "402": {
            "description": "Payment required, or the session budget is exhausted."
          }
        }
      }
    }
  },
  "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 — 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 — \"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 — 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 — 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 — 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 — 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."
    ]
  }
}
