# One checkout, your choice of payment backend Install `npm install @playmos/sdk@0.3.36`. Use `@playmos/sdk/checkout` for the provided interface or your game’s own checkout controls. Choose one path: | Your game | Start here | | --- | --- | | Playmos manages purchases; your server grants items | `playmos-iap.mjs` plus the [manual backend and HTTP adapter](https://test.playmos.io/docs/stablecoin-quickstart#manual-setup). | | Your provider already manages IAP | `custom-provider.mjs`. Keep your existing catalogue and provider. No Playmos payment-service credentials are needed. | | Paid contest admission | `contest-entry.mjs`. This creates an entry, not an IAP item. Server checks payment and admission for the original round and identity. | Pass your existing session-backed backend methods to these integration modules. The examples use Base Munch as their game context. ## Provided interface or your own `mountCheckout(root, {checkout, gameName, testMode:true})` renders the saved quote, one Pay action, recovery and a receipt after server acknowledgement. It prepares the quote without charging. It supports embedded or `mode:'sheet'`; your host owns modal focus and dismissal. Dispose the mount when leaving the screen. For your own interface, subscribe to `checkout.subscribe(state)`, show the immutable `state.record.order`, and call `prepare()`, then `pay()` only after the player reviews the total. `check()` reads the saved payment; `startNew()` explicitly starts another purchase after completion/cancellation/verified failure. Neither a network error nor closing a wallet starts a replacement payment. A permanent unlock is not repeatable. The storage key must include provider, game, player, kind and item/round. Do not clear pending orders on logout; return to that account to recover them. The default browser storage uses a Web Lock across tabs and fails closed where that API is unavailable. A custom host storage adapter must supply `exclusive(operation)` across every context/process sharing its key; the controller also serializes overlapping mounts in one module. Never store credentials in browser storage. A client receipt is display state, not permission to grant inventory or enter a paid game. ## Your callback server Register your provider's HTTPS callback route. Preserve the original bytes; verify the provider's signature, timestamp and replay policy before accepting. Persist the verified event in a durable inbox and acknowledge only after persistence. A worker independently queries the provider/chain under server credentials, matches the original order, player, SKU, currency and amount, and atomically inserts the unique `(provider,paymentId)` grant alongside the inventory change. Retry outages; do not turn an unknown result into failure. The Playmos adapter also calls the authenticated completion route when its intent is prepared, before wallet signing, to bind the payment ID to the order. Keep that binding durable so callbacks can recover even when the game closes. Manual completion and callbacks use that same transaction. The checkout adapter's `complete` method calls your authenticated backend. Only return `{status:'complete',paymentId,transactionId,reference}` after fulfillment or admission succeeds. `reference` is the durable fulfillment/admission identifier. Pending remains `{status:'pending'}`. Provider redirects and browser success callbacks never grant items. ## Hub and standalone Run the checkout in the trusted host which already owns the wallet. An embedded game requests an allowed product/contest through its host bridge; the host validates the exact frame/source, origin, game/build and signed-in player. Do not send a wallet provider into the iframe. A standalone game passes its own wallet or existing checkout provider. The same backend owns server verification in either case. The sample does not install a Hub bridge or activate paid publication. ## Contest and prize acceptance Save the plain-string `roundKey` and exact `identity` used for payment. Persist intent, signing uncertainty and wallet batch before the next action. Recover with `resumeEntryPayment`, not a new `enterRound`. An epoch integration similarly saves `epochs.enter({onPrepared,onSigning,onWalletCalls})` and calls `epochs.resumeEntry` with its saved entry. Do not interchange legacy rounds and epoch pools. The backend must independently verify chain, pool, round/epoch, identity, asset, amount, successful transaction and admission before returning complete. A confirmed entry is not proof of prize settlement. Test round lock, authoritative scoring, settlement and the actual winner payout or withdrawable credit separately; never invent winners or treat a credit as a transferred prize. ## Self-hosted contest checkout Call showCustomCheckout({root, gameId, playerId, kind: "contest-entry", itemId: savedAttemptId, backend, openCheckout}). createOrder must return that same kind/itemId with a frozen fee and original round identity. completeOrder must verify admission, not just payment. Use showCustomPurchase for an item. Neither path requires Playmos payment-service credentials. For the Playmos contest adapter, implement backend.bindPayment({orderId,paymentId}) as an authenticated durable order/payment association before signing. Enforce the session player, exact original game/round/identity, unique payment binding and idempotent repeats; a bind is never an admission. Its callback worker uses this association to recover after the browser closes. The adapter refuses to sign when binding fails.