Gamehub
A little more you.

Sign in to save your favorites and make yourself at home.

For developers and publishers
Browse docs Your studio pool
On this page

Run your studio’s contest pool.

Try it first

Inspect your pool before admitting entries#

You need Studio test keys, Base Sepolia admin/operator access and a registered pool or a local setup to create one.

  1. Inspect the pool address, chain and confirmed ownership.
  2. Read the immutable series terms and wait for created=true.
  3. Reconcile entry and settlement receipts against those exact identifiers.

Check your result Your studio, pool, series, price and operator all match the intended test contest.

Deploy and register your own EpochPrizePool, create an immutable series, then admit entries and settle past windows.

Keep admin and operator signing local#

IdentityRole
Studio API secretServer-only prepare / register / studio reads.
Admin EOADeploy, prove ownership, create series and manage roles.
Operator EOASign the results for completed epochs. Playmos does not hold this key.
Player walletPay entry and withdraw credited winnings. Use a publishable API key in the player client.

These Base Sepolia examples use normal EOAs for admin and operator. Mainnet contests require the approved multisig configuration and a written legal decision before launch. Smart-wallet contract creation and operator ECDSA recovery have different requirements; do not assume a Base app smart wallet can deploy this pool or act as its operator. Protect the local key file and keep it out of source control. For an existing pool deployed with the admin as operator, the admin calls grantRole(OPERATOR_ROLE, operatorAddress) and verifies hasRole on-chain before using a new dedicated operator. For a new pool, pass the operator at prepare so no later role grant is needed.

1. Create the local setup#

Studio CLITerminal
  1. export PLAYMOS_API_URL=https://api.sandbox.playmos.io/v1
  2. npx --yes --package=@playmos/sdk@0.3.38 playmos-studio init
  3. # Fund the admin EOA with Base Sepolia ETH.
  4. # Set PLAYMOS_SECRET securely in this process environment.
  5. npx --yes --package=@playmos/sdk@0.3.38 playmos-studio pool create \
  6. --series your-series --entry 250000 --epoch 3600
  7. npx --yes --package=@playmos/sdk@0.3.38 playmos-studio pool own

The CLI is shipped inside @playmos/sdk; there is no separate playmos-studio npm package. init writes local admin/operator material. pool create prepares, signs locally, deploys and records the pool/transaction. pool own completes or resumes the ownership handshake. Inspect command output before proceeding.

2. Confirm ownership#

The prepare API returns unsigned transaction data and deployment bytecode. Preparing is not broadcasting. After the deployed address has a successful receipt, registration first returns a proof message. Inspect its studio, pool, wallet and chain, sign it locally with the admin, then register the signature. Continue only when ownership === 'confirmed'.

Ownership handshakeTypeScript
  1. import { Playmos } from '@playmos/sdk';
  2. export async function registerYourPool(
  3. ops: Playmos, prepared: import('@playmos/sdk').EpochPreparedPool,
  4. poolAddress: `0x${string}`,
  5. signMessage: (message: string) => Promise<`0x${string}`>,
  6. ) {
  7. // Preserve the same dedicated operator and sinks used for deployment.
  8. const input = { studioWallet: prepared.admin, admin: prepared.admin,
  9. operator: prepared.operator, feeSink: prepared.feeSink, poolAddress };
  10. const pending = await ops.epochs.registerPool(input);
  11. if (pending.ownership === 'confirmed') return pending;
  12. if (!pending.proof) throw new Error('No ownership challenge returned.');
  13. // Inspect the studio, pool, wallet and chain named in this message.
  14. const signature = await signMessage(pending.proof.message);
  15. const owned = await ops.epochs.registerPool({
  16. ...input, walletProof: { signature },
  17. });
  18. if (owned.ownership !== 'confirmed') throw new Error('Ownership is not confirmed.');
  19. return owned;
  20. }

3. Create the first series#

Prepare and review the first seriesTerminal
  1. npx --yes --package=@playmos/sdk@0.3.38 playmos-studio series create \
  2. --name your-series --entry 250000 --epoch 3600 --dry-run
  3. # Review chain, roles and split; remove --dry-run to sign and send.

The CLI proves the RPC chain, checks admin/operator roles and avoids resending a recorded series transaction. Programmatically, call epochs.prepareSeries, inspect and sign unsignedTx, then call epochs.getSeries until created === true. A prepare response alone does not open a series.

Use the actual series terms#

BucketDefault basis points
Current epoch prize6000 (60%).
Next-window seed3000 (30%).
Studio sink900 (9%).
Playmos fee sink100 (1%), locked.

All four buckets sum to 10000. Seed can be zero and the first three shares can change at series creation; Playmos’s 100 bps remains fixed. Entry is integer micro-USDC: 250000 means 0.25 USDC. A new clock, entry or split requires a new series. New immutable sink addresses require a new pool. Read addresses and fee terms from the prepare/chain response rather than copying them from another game.

4. Enter the current window#

Players call epochs.enter with their wallet provider, pool, series and a persisted attempt identity. The returned epochId comes from the mined entry event when confirmed. Preserve it across admission and scoring; the clock may advance before the transaction mines. Re-entry is supported and identityEntryCount records the paid count.

Player entryTypeScript
  1. import { Playmos } from '@playmos/sdk';
  2. export async function enterYourEpoch(
  3. publishableKey: string, provider: import('@playmos/sdk').Eip1193Provider,
  4. pool: `0x${string}`, series: string, attemptIdentity: string,
  5. ) {
  6. const player = new Playmos({
  7. apiKey: publishableKey,
  8. wallet: { provider },
  9. contracts: { epochPrizePool: pool },
  10. rpcUrl: 'https://sepolia.base.org',
  11. });
  12. const entry = await player.epochs.enter({ series, identity: attemptIdentity });
  13. // Persist the returned epochId, identity and txHash for server admission.
  14. // Confirmed entries use the mined event's epochId, not a guessed clock window.
  15. return entry;
  16. }

A client-supplied transaction hash or browser confirmed flag does not authorize a paid round. The trusted backend must match a successful receipt from the expected chain, pool and series, with the exact attempt identity and the authenticated player's event payer. Refuse bytes32-shaped proof identities; the SDK's low-level compatibility hasher passes those through. Identity strings can be predictable and are received/echoed by the service, so they are not authentication.

Your backend must atomically consume each paid event once, using chain + pool + transaction hash + logIndex; a smart-wallet bundle can contain several entries. Persist the paid epoch for scoring and settlement. Define sale, admission and score deadlines explicitly; the contract's epoch end is not a studio grace-period policy. The Hub uses its paid-seat horizon for score/settlement eligibility.

The published SDK 0.3.38 includes RPC-backed epoch views, backend verifyEntry, exported event/error ABIs and read-only epochs.resumeEntry. Bind verification to the authenticated payer and atomically consume the returned event replay key on your backend. Retain the existing payment's hash/identity when a read fails; do not charge again to retry admission. Both the original seven-field and studio-share eight-field Entered signatures are supported.

Keep purchases and attempts within the competition#

For paid replay, block a new entry purchase during an active run and reuse an unused paid attempt before selling another. Allow another paid attempt after completion while the competition is open. The server checks the exact competition ID and closing time at quote creation, immediately before signing and when admitting a new run. Unused entries expire at closing; they remain in payment history and never carry admission into the next competition. Reload resumes the server-owned active attempt. A previously started run follows your published bounded finish and scoring policy, so an ongoing turn does not silently hold another competition open.

Cancellation before submission creates no paid admission. Pending or uncertain payments remain recoverable with their original key; define a late-confirmation and refund policy before accepting entry fees. See the in-game checkout workflow.

5. Settle a completed window#

Your trusted backend validates scores and selects winners. The operator signs settlementTypedData. In SDK 0.3.38, epochs.executeSettlement sends the signed result directly through the client’s configured wallet; that wallet pays gas. The separate server HTTP route POST /v1/epochs/:epochId/settle/signed accepts a studio secret and relays the operator signature through the service when that capability is configured. Do not treat these as the same transport. For an own pool, do not use the service-signer epochs.settle path as a substitute for your operator. The current window is still accepting entries; settle a past one.

Operator-signed settlementTypeScript
  1. import { Playmos } from '@playmos/sdk';
  2. import { settlementTypedData } from '@playmos/sdk';
  3. export function operatorSettlement(
  4. pool: `0x${string}`, series: string, pastEpochId: string,
  5. winners: `0x${string}`[], amountsMicro: string[],
  6. ) {
  7. return settlementTypedData({
  8. pool, chainId: 84532, series, epochId: pastEpochId,
  9. winners, amounts: amountsMicro, // Integer micro-USDC; never USD decimals.
  10. });
  11. }
  12. export async function executeSignedSettlement(
  13. client: Playmos, pool: `0x${string}`, series: string, pastEpochId: string,
  14. winners: `0x${string}`[], amountsMicro: string[], signature: `0x${string}`,
  15. ) {
  16. // Your operator EOA signs operatorSettlement(...) locally.
  17. // client needs a wallet provider: this SDK method sends from that wallet.
  18. // The separate HTTP /settle/signed service relay is not called here.
  19. const receipt = await client.epochs.executeSettlement({
  20. epochPrizePool: pool, series, epochId: pastEpochId,
  21. winners, amounts: amountsMicro, signature,
  22. });
  23. // A broadcast is not terminal settlement. Read epochs.get for this window.
  24. return receipt;
  25. }

A transaction hash or pending response is not terminal success. Read epochs.get, its terminal state and read provenance. Empty-window roll and refund are different outcomes from a winner payment.

6. Withdraw or refund#

epochs.prize reads the contest prize pool. Read the pool's withdrawable(wallet) for a wallet's credited balance and use epochs.withdraw for its wallet-signed pull. A timeout does not activate refunds automatically: the protocol's permissionless refundEpoch must first make the oldest eligible paid window Refunded. Then claimableRefund / claimRefund credit the retained pool share; the player withdraws afterward. Fee, studio revenue and seed already carried onward are excluded. This protocol escape hatch is separate from discretionary refunds. Missing amountMicro is unknown, not zero. See payout and withdrawal states.

Current pools move seed into carry at entry time. It can attach to a later paid epoch before the source settles; settlement pays pool + incomingSeed. outgoingSeed is a compatibility slot that stays zero.

The CLI supports init, pool create, pool own and series create. There is no operator run command; the operator loop remains your backend/script.

If it did not work#

Prepared unsigned data is not a deployed pool. Resume the recorded deployment or ownership handshake before creating another.

Try one variation Compare the rehearsal terms with the registered series. Explain which immutable on-chain terms govern an actual entry.

Continue with the next guide →