# Paid matches Your player picks a tier, **your page confirms and charges them**, and your server seats the match. You collect the entry and pay your own winner; we supply the game, decide the result, and bill a fee for the seat. **A paid match pairs two of your own players.** ## How it starts 1. Your player taps a tier card, priced with **your** number — see [Managing your account](/docs/portal.md). 2. The frame posts to **your page**: `{ type: "seatRequest", nonce, gameId, tierId, attempt }`. It carries no amount — price from your own copy of the ladder, never a browser. 3. **You run your own confirm dialog**: your price, your currency, your balance. 4. Your server debits the player and calls the seating endpoint below. 5. The frame enters the match on its own, through our API; it does not wait for an answer from your page, and no message from it can seat anybody. > **The confirm step is required, not advisory** — we place no limit on seat requests; your dialog is what stands between repeated taps and repeated charges. Derive your `idempotencyKey` from `attempt`: same for that player, game and tier across a double tap or reload, so those collapse onto one charge and one seat. ## Who plays whom **Today a paid match seats two players of the same operator** — when nobody is waiting, they wait for another of *your* players at that game and tier, not one of ours or another platform’s. A tier nobody else on your platform is playing does not fill. ## What the numbers are - **The prize is ours, fixed per tier** — what your winner takes. You do not set it. - **The entry is yours to set,** from the tier’s floor to its prize — below is refused, and so is above the prize. Anything above the floor you keep; it never reaches us. You set it per tier **in the portal**, the number your player sees on the card. The seating call must send that same number — a seat that disagrees is refused. - **The fee is per seat**, our share of a fixed base under the split agreed with your account — quoted before the match and pinned to the seat, so renegotiating a split does not restate an earlier one. > **The tier cards do not appear unless your handshake asks for them** — your `init` message must carry `seating: "host"`. Leave it out and the embed has practice and no match entry — see [Embedding a game](/docs/embedding.md). ## Read the ladder first ``` GET /partner/tiers?gameId=streakpeaks Authorization: Bearer 200 { "tiers": [ { "tierId": "usd_1", "prizeUsdCents": 100, "minStakeUsdCents": 55, "entryUsdCents": 55 }, { "tierId": "usd_20", "prizeUsdCents": 2000, "minStakeUsdCents": 1100, "entryUsdCents": 1400 } ], "pricesUpdatedAt": 1760000000 } ``` `minStakeUsdCents` is the floor your price must clear; the prize is the ceiling. Read this — don’t hard-code it. **`entryUsdCents` is what YOUR player is charged** — your Admin’s portal price, or our floor if none. Price your confirm dialog from this. Read it per launch, not hard-coded: a stale figure is refused at seat time. `pricesUpdatedAt` is when your Admin last saved the price map, in epoch seconds, or `null` if nobody has — a real answer, not missing. Cache the ladder and compare this to know your copy is current. > **It is a live read, not a start-up setting to cache** — an Admin can change the price at any moment and this always answers current. `minStakeUsdCents` is different: ours, the least the seating call accepts. **The card your player sees follows within 60 seconds, not instantly** — the same propagation described for origins on [Embedding a game](/docs/embedding.md). Read it per launch, and for a promotion, change the price before the traffic rather than during it. **The ladder is the tiers you can seat.** A tier we cannot offer is simply absent, not listed and flagged. Read it at start-up, cache it, and refresh — don’t hard-code the list. ## Seat the match > **`stakeUsdCents` must match the price we published.** Read it from `entryUsdCents`, not a constant, so a portal price change cannot leave your backend behind. [Errors and limits](/docs/errors.md) gives the rule, the `stake_does_not_match_price` code, and how an unpriced tier is handled. ``` POST /partner/matches Authorization: Bearer { "playerId": "u_8813", "gameId": "streakpeaks", "tierId": "usd_20", "stakeUsdCents": 1400, "idempotencyKey": "your-own-id-for-this-request" } 200 { "matchId": "m_7f3a…", "uid": "ptr:acme:u_8813", "joined": false } ``` `joined: false` means seated and waiting for an opponent — another of your own players, as above. `true` means one was already waiting and play is under way. Either way the seat exists and the match id is yours. ## Seating a player who is not already in the frame Seating from somewhere else — a lobby, a promotion, an email — does not put anyone in front of the match. Mint a session, embed the frame as on [Embedding a game](/docs/embedding.md), and deep-link it on `init`: ``` frame.postMessage({ type: "init", token, // from POST /partner/sessions, same gameId uid, // the uid POST /partner/matches returned dealerUrl: "https://api.peritusgames.com", gameId: "streakpeaks", route: "/play/" + matchId, }, GAME_ORIGIN) ``` Send `route` to land your player straight in the match; without it they land in the lobby, where the seat is waiting — sorted above finished matches in their list, and counted on the Matches tab. ## The idempotency key, and the one answer that lies `idempotencyKey` is yours to choose — 1 to 128 characters of `A-Z a-z 0-9 _ - .` The same key for the same request returns the same seat; for a *different* request it returns `409 idempotency_key_conflict` instead. [Errors and limits](/docs/errors.md) gives the rule and the code. > **A 503 does not mean the seat was not taken** — the answer can be lost after we have already seated, so `settlement_unavailable` means only that we could not tell you. Retry with the **same** key: you get the existing seat back, or a new one. A fresh key opens a second paid match beside the one you already have. ## When nobody joins A match that expires without an opponent is `no_contest`, billed a stated zero. **Return the entry to your player.** A player who paid but did not play is different: `ended`, with `played: false` — no refund; the result stands by score. ## What happens next [Match results](/docs/results.md) carries the tier, prize, your stake echoed back, and the fee — from the `started` row, not just the end. A draw and a `no_contest` are both billed a stated zero. Every refusal this page can produce is catalogued under [Errors and limits](/docs/errors.md).