Playmos
Gamehub
Make yourself at home.

Sign in to your Playmos account.

For developers
and publishers
Developers Your studio pool
Documentation Your studio pool

Run your studio’s contest pool.

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.

Use normal EOAs for admin and operator. 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. npx --yes --package=@playmos/sdk@0.3.20 playmos-studio init
  2. # Fund the admin EOA with Base Sepolia ETH.
  3. # Set PLAYMOS_SECRET securely in this process environment.
  4. npx --yes --package=@playmos/sdk@0.3.20 playmos-studio pool create \
  5. --series your-series --entry 250000 --epoch 3600
  6. npx --yes --package=@playmos/sdk@0.3.20 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.20 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.

SDK 0.3.20 uses the service for epoch views and planning. RPC-backed views, backend verifyEntry, exported event/error ABIs and read-only epochs.resumeEntry are in the next SDK patch, pending publication. Until that release, do direct receipt verification on your backend and retain the existing payment's hash/identity when a read fails; do not charge again to retry admission. Support both the original seven-field and studio-share eight-field Entered signatures.

5. Settle a completed window#

Your trusted backend validates scores and selects winners. The operator signs settlementTypedData. In SDK 0.3.20, 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 pot. 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.

Keep buildingYour first payment