Quickstart
Quickstart
Keep your game code in external scripts. Declare storage in the manifest for this example.
// game.js, loaded with <script type="module" src="./game.js">
const session = await uniplays.ready()
console.log('Playing version', session.version)
if (uniplays.can('storage')) {
try {
const save = await uniplays.storage.get('progress')
const progress = save.value ?? { level: 1 }
await uniplays.storage.set('progress', progress, {
ifMatch: save.etag
})
} catch (error) {
// On version_conflict, reload and reconcile the save.
console.warn('Could not save:', error.code)
}
}The treasury, the item desk and chance
The treasury, the item desk and chance
The SDK binds all of these and the shell fulfils all of them. The contracts behind them are not deployed yet, so each call refuses with a typed error naming the binding it needs rather than hanging or returning an empty answer. Write against them now and handle that refusal; nothing about the call changes when the binding lands.
can('economy') and can('chance') read a manifest declaration, not a deployed runtime. A game that declares neither never reaches any of this. chance.verify() is local and pure: it re-derives an outcome from the values a round published and checks no signature, so a game must not present its result as verified randomness.
economy.treasury: This game's treasury balance, effective policy and published ceilings.economy.awards: Awards already paid to this player, and what the caps leave today.npc.offers: Standing treasury bids for declared item types, with the units left today.npc.sell: Sell owned items into a standing treasury bid. The player signs the listing.chance.rounds: Open rounds of a declared chance game, with the odds armed at launch.chance.result: The state of one entry and every input needed to verify its outcome.chance.enter: Enter an open round at the published entry price.chance.claim: Claim the prize or the refund a terminal entry is owed.
Optional hosted backend
1 methodShort authenticated functions, protected state and prepaid usage. Availability depends on the configured environment.
uniplays.backend.call(name, input, { idempotencyKey })
Promise<{ result, replayed }>backendPersist a UUID for each logical action and reuse it on every retry. The shell binds game and frontend version. hosting_suspended means the online service is unavailable; the developer dashboard offers recharge. A backend receives verified player context; NFT vouchers require wallet consent plus explicit developer signer delegation.
Getting ready
3 methodsWait for the UniPlays session before calling platform methods. can() and session.on() are synchronous; other methods return promises.
uniplays.ready()
SessionResolves with gameId, version, bundleHash, locale, isGuest, capabilities, entitlements, protocol, and the pinned manifest.
uniplays.can(capability)
booleanChecks the pinned manifest’s declarations. Throws not_ready before ready() resolves. A declaration is not a promise the call will succeed: a declared method whose runtime this deployment does not serve refuses by name rather than answering an empty result.
uniplays.game.version()
{ version, bundleHash, publishedAt, changelog }Player identity
4 methodsuniplays.player.id()
stringplayer.identityA player ID scoped to this game.
uniplays.player.displayName()
string | nullplayer.identityThe display name, or null for a guest.
uniplays.player.wallet()
stringplayer.walletThe player’s wallet address after a UniPlays consent dialog. Handle consent_required and wallet_required.
uniplays.player.token()
{ token, expiresAt }player.identityA short-lived JWT for your backend. Verify its signature, issuer, audience (your gameId), and expiry on your server.
Saves & progress
5 methodsAccount storage allows 64 keys and 256 KiB per player per game; guest storage allows 16 keys and 64 KiB with a 30-day lifetime.
uniplays.storage.get(key)
{ value, savedAtVersion, etag, rev }storageRead a save and its concurrency token. A missing key has a null value.
uniplays.storage.set(key, value, { ifMatch? })
{ etag, rev, savedAtVersion }storageWrite JSON data. Pass the last etag as ifMatch, and omit ifMatch while the etag is null, because the shell refuses a null one. On version_conflict, reload and reconcile before writing again.
uniplays.storage.list({ prefix?, limit? })
StorageEntry[]storageList keys, optionally filtered by prefix. Metadata fields are currently placeholders; use storage.get for a key’s etag and savedAtVersion.
uniplays.storage.remove(key, { ifMatch? })
booleanstorageDelete a key, optionally only if its etag still matches.
uniplays.storage.migrate(fromVersion, { key? })
numberstorageRe-tag saves with the running version. This does not transform data: read, convert, and save the new shape yourself.
Leaderboards
4 methodsBrowser-submitted scores are unverified.
uniplays.leaderboard.submit(score, { seasonId?, board? })
{ accepted, best, rank, verification }leaderboardSubmit an integer score to the selected season; fractional values are truncated. A game that declares several leaderboards is refused with not_available until the platform keeps them.
uniplays.leaderboard.top(limit?, { seasonId?, board? })
{ seasonId, board, order, verification, entries }leaderboarduniplays.leaderboard.boards()
LeaderboardBoard[]leaderboardThe boards the pinned manifest declares, first board first, each with its order and bound. A game without a leaderboards block has one board, default, highest first.
uniplays.leaderboard.seasons()
Season[]leaderboardList this game’s leaderboard seasons. The developer starts a new one from the manage page or with a publish-scoped key, so a rebalanced game does not compete with its old scores.
Game token
5 methodsThese methods refer to your game’s token.
uniplays.token.price()
{ weth, usd, source, updatedAt }Price in WETH. The usd field currently returns null.
uniplays.token.symbol()
stringuniplays.token.decimals()
number18 decimals.
uniplays.token.balance()
stringtoken.gateBalance in whole tokens.
uniplays.token.gate(id)
booleantoken.gateTest a gate declared in token.gates. This is advisory and locally bypassable; enforce valuable access on your backend.
Store & rewards
4 methodsuniplays.store.items()
{ pricing, items }storeCatalog from the pinned manifest. Requires the store manifest block.
uniplays.store.quote(itemId)
{ itemId, priceUsd, priceTokens, tokenSymbol, expiresAt, pricedAt, source }storeAn expiring price quote for a declared item. Show each USD item as its dollar price beside the token amount, for example '$2.00 · 480 PLAY', and quote again when expiresAt passes. A USD amount is exactly what the chain charges for the priced minute, pricedAt, until expiresAt; source names the market it was averaged over.
uniplays.store.purchase(itemId)
PurchaseReceiptstoreReview signed checkout terms, approve if needed and confirm the actual payment. Deployment and signer configuration are required; a quote is not a receipt.
uniplays.rewards.grant(amount, { reason?, authorization })
{ granted, remainingToday, txHash, awardHash, status }rewardsRedeem a supplied signed treasury award after wallet review. authorization contains award and signature; missing authorization refuses before a wallet prompt. This never grants the game issuance authority.
Presence & session
4 methodsuniplays.presence.count()
{ signedIn, guests }presenceObserved online player counts for this game. An unavailable service refuses with not_available rather than inventing zero counts.
uniplays.session.state()
{ visible, focused, paused }Pause when paused is true or visible is false, never on focused: a tap into the game can move focus into the frame.
uniplays.session.on(event, callback)
() => voidSubscribe to pause, resume, visibility, entitlements, or audio. Returns an unsubscribe function. Pause on pause, never on focused. Resume audio on resume with activation true and again from the game's own first pointerup, touchend or keydown, because only the in-frame gesture always counts.
uniplays.session.audioPolicy()
allowed | allowed-muted | disallowed | unknownThe browser’s autoplay policy. Audio may still require a user gesture.
Assets and marketplace
9 methodsRead game items and listings. Value actions require shell review and wallet approval. One manifest version declares at most 256 item types and a collection holds at most 4,096 over the game’s life.
uniplays.assets.inventory({ cursor?, limit? })
{ items, cursor }assetsRead owned items in this game.
uniplays.assets.item(type)
AssetTypeassetsuniplays.assets.balance(type)
stringassetsRead owned units of an item type.
uniplays.assets.burn({ purpose, items })
AssetBurnReceiptassets.burnBurn owned items for a purpose declared in assets.burns. Only the purpose and each tokenId and amount are sent. The shell reviews and sends the burn in its own dialog once the player approves it, and answers not_available when the network has no item collection configured, item burns are turned off or the chain reads it checks fail, so branch on can('assets.burn') and handle not_available. Recipes change only through a game update; grant the reward from your server once it has verified the burn.
uniplays.market.listings({ type?, cursor?, limit? })
{ listings, cursor, tokenSymbol }marketuniplays.market.quote(listingId)
ListingQuotemarketRead the price, royalty, and seller proceeds.
uniplays.market.list({ tokenId, amount, price, expiresIn? })
ListingReceiptmarketThe market capability also requires assets.
uniplays.market.buy(listingId, { maxTotal, amount? })
MarketPurchaseReceiptmarketBuy a listed item with the game’s token.
uniplays.market.cancel(listingId)
CancelReceiptmarketTreasury & awards
2 methodsReads only: the caps, the split and the rates are the treasury’s, and nothing here moves a token. While a network leaves the treasury contracts unbound the API refuses these reads by name rather than answering a zero balance.
uniplays.economy.treasury()
TreasuryStateeconomyThe balance, the live purchase split, the caps in force, what today’s cap leaves and the open rounds. The trading fee that funds the treasury is fixed at launch.
uniplays.economy.awards({ limit? })
AwardHistoryeconomyAwards already paid to this player, what the per-player cap leaves today and the most one voucher may carry.
Paid prize draws
4 methodsThe draws the manifest declares, priced in whole tokens or in USD converted on chain at a 30-minute average price. play and refund open the shell confirmation with the exact token amount; the other calls are reads. A contract is a draw only when the pinned draw factory lists it, and a holder rewards contract counts only when it is the game’s registered fee sink; a lookalike is never read from or paid.
uniplays.draws.status(id?)
DrawStatusListdrawsEach declared draw with its state, the price for the current minute, the odds, the prizes left, and this wallet’s plays and pending plays.
uniplays.draws.play({ id, maxTokens? })
DrawPlayReceiptdrawsPays the amount the player approved, burned or split as the manifest declares. maxTokens is an optional ceiling the game sets. The outcome comes from a later drand round, after the play confirms.
uniplays.draws.result(id, playId)
DrawResultdrawsWhether a play has settled, whether it won, its prize, and whether that prize still waits for its claim. The platform claims every win for its player, who can also claim it on the game page.
uniplays.draws.refund(id, playId)
DrawRefundReceiptdrawsReturns the entry of a play whose randomness never arrived, once its refund time has passed, or at once for a play that draws.status lists in myPending with refundOnly true.
Treasury item desk
2 methodsThe standing bids the treasury holds for declared item types. A sale is a shell confirmation: the player approves the decoded terms and their own wallet signs.
uniplays.npc.offers(type?)
NpcOfferListeconomyEvery standing bid, or the bids for one declared item type, with the units left today, the expiry and whether the bid is firm. A bid is not a floor and a read is not a quote bound to a seller.
uniplays.npc.sell(tokenId, { amount, minUnitPrice })
NpcSaleReceipteconomytokenId is an id from assets.inventory, not a type slug. minUnitPrice is required and is the unit price the player was shown, for the reason market.buy requires maxTotal.
Chance rounds
6 methodsChance is closed for now: no round exists, so rounds lists none and enter and claim answer not_available for the round they name. Rounds run against a table armed at launch. rounds, result, verify and verifyPool are reads; enter and claim open the shell’s confirmation, which shows the disclosure before a player’s first entry.
uniplays.chance.rounds(id?)
ChanceRoundListchanceOpen rounds, or the rounds of one declared chance game, with the entry price, the entries left, the close time and the rules hash of the table armed at launch. The round carries the hash, not the table itself.
uniplays.chance.result(roundId, entryIndex)
ChanceResultchanceOne entry’s state and amount, with the transcript verify() re-derives from. A pending entry has no outcome and none is invented.
uniplays.chance.enter({ id, roundId, rulesHash, maxEntryPrice })
ChanceEntryReceiptchanceAll four fields are required. rulesHash names the armed table the player consented to and maxEntryPrice the ceiling they will pay; the shell supplies neither on their behalf.
uniplays.chance.claim(roundId, entryIndex)
ChanceClaimReceiptchanceDraw the prize or the refund a terminal entry is owed.
uniplays.chance.verify(transcript)
ChanceVerificationRe-derives one entry’s outcome from a chance.result transcript, locally and with no round trip. It re-derives; it does not prove: pairingChecked is always false, so never present the answer as verified randomness.
uniplays.chance.verifyPool(transcript)
ChancePoolVerificationThe same re-derivation for a pool round, where one winner is drawn over the entries. A pool round reads no row of the armed table, so verify() refuses it and this call answers for it.
Staked matches
6 methodsSigned terms and trusted results are required. Value-moving actions need wallet review and a configured deployment; unsigned creation or results return authorization_required.
uniplays.match.create({ stake, maxPlayers, authorization })
{ id, stake, maxPlayers, txHash, status }matchauthorization contains complete terms, authorizer and signature. Review exact approval and deposit separately. Never use creation as an availability probe.
uniplays.match.join(id)
{ id, txHash, status }matchJoin an open match using its current signed terms and a separately reviewed stake deposit.
uniplays.match.report(id, result)
{ id, accepted, txHash, status }matchresult contains outcome, resultHash, resolver signature and winner for a win. A client claim alone cannot settle a match.
uniplays.match.state(id)
MatchStatematchRead anchored terms, membership, claimable credit and the next action. Settlement is distinct from withdrawal.
uniplays.match.cancel(id)
{ id, txHash, status }matchCancel an eligible open match. Use claim separately to withdraw refund credit.
uniplays.match.claim(id)
{ id, amount, txHash, refundTxHash, status }matchReview any timeout-refund transition and the actual withdrawal separately. Pending transactions retain their original hashes for recovery.