# Player Two > A game store and a launchpad for games you play in the browser: games for sale (or free) to play in the browser or download, and raises where players back a game being made, paid to its developer stage by stage as they post proof. For developers, a game SDK (multiplayer rooms with no server of their own, trophies, leaderboards, cloud saves) and hosting for web games. Player Two (playertwo.lol) is a store and launchpad for browser games and the game SDK that comes with it. It is not the Player 2 app for finding people to play with, nor the Player2 company that makes AI characters for games. Player Two is at https://playertwo.plus. Its own channels: X https://x.com/PlayerTwoPad, Telegram https://t.me/PlayerTwoPad. Email hello@playertwo.lol. ## Games on Player Two Every game in the store and on the launchpad now, each with its page (play it there) and its page as Markdown: - [Impact](https://playertwo.plus/game/impact): 100 fighters. One arena. Awaken, go ultimate, and be the last one standing. (free, by Dude Games; Markdown: https://playertwo.plus/game/impact.md) - [Hoop It](https://playertwo.plus/game/hoop-it): Big heads. Bigger dunks. Streetball with your friends. (free, by Dude Games; Markdown: https://playertwo.plus/game/hoop-it.md) - [MECHA / RAMPAGE RUSH](https://playertwo.plus/game/mecha): Small frame. Big headlines. (free, by Dude Games; Markdown: https://playertwo.plus/game/mecha.md) - [Thrustline](https://playertwo.plus/game/thrustline): Tether racing at the edge of the world. (free, by Dude Games; Markdown: https://playertwo.plus/game/thrustline.md) - [On The Cruise](https://playertwo.plus/game/cruise): Broadsides, bounties and one more voyage on the Brightwater. (free, by Dude Games; Markdown: https://playertwo.plus/game/cruise.md) - [Knead for Speed](https://playertwo.plus/game/knead-for-speed): Cat kart racing where the race stops for staring contests and nose boops. (free, by DevFMD; Markdown: https://playertwo.plus/game/knead-for-speed.md) - [BloodRoot](https://playertwo.plus/game/bloodroot): Plant what bleeds. Sell to what reaches out. Don't look back. It is only a garden. (free, by 3Ps Studio; Markdown: https://playertwo.plus/game/bloodroot.md) - [Netstorm](https://playertwo.plus/game/netstorm): Read it. Set it. SEND IT. Storm-powered volleyball, free in your browser. (free, by Netstorm Studio; Markdown: https://playertwo.plus/game/netstorm.md) The same list as JSON: https://playertwo.plus/api/v1/games. A game's page: https://playertwo.plus/game/; as Markdown: https://playertwo.plus/game/.md. Search the store: https://playertwo.plus/?q=. ## Pages as Markdown Every page of the help, the docs and the rules has its words as Markdown at its address with .md after it (made from the page itself, so they are the same), and all of them together with every game's are in https://playertwo.plus/llms-full.txt: - https://playertwo.plus/help (Markdown: https://playertwo.plus/help.md): questions and answers for players, backers and developers: balance, buying, free games, backing, fees, refunds, passes, payouts, downloads, your account - https://playertwo.plus/docs (Markdown: https://playertwo.plus/docs.md): how the platform works, part by part, and for developers: listing, editing, updates, playtests, raising, builds, agents - https://playertwo.plus/docs/sdk (Markdown: https://playertwo.plus/docs/sdk.md): the game SDK: trophies, levels, stats, leaderboards, cloud saves, presence, in-game purchases, multiplayer rooms and worlds, voice - https://playertwo.plus/docs/sdk/travel (Markdown: https://playertwo.plus/docs/sdk/travel.md): the player's avatar and travel between games with the party - https://playertwo.plus/docs/sdk/connect (Markdown: https://playertwo.plus/docs/sdk/connect.md): Player Two Connect, for a game on its own site - https://playertwo.plus/docs/hosting (Markdown: https://playertwo.plus/docs/hosting.md): hosting a web game here, pushing builds, GitHub, a game served from its own site - https://playertwo.plus/docs/webhooks (Markdown: https://playertwo.plus/docs/webhooks.md): webhooks for a studio's own service or Discord - https://playertwo.plus/developers/compare (Markdown: https://playertwo.plus/developers/compare.md): where to publish a web game: Player Two beside itch.io, Poki, CrazyGames, Steam and Discord Activities, from each platform's own pages, dated - https://playertwo.plus/policy (Markdown: https://playertwo.plus/policy.md): the content and moderation policy - https://playertwo.plus/terms (Markdown: https://playertwo.plus/terms.md): the terms of service - https://playertwo.plus/privacy (Markdown: https://playertwo.plus/privacy.md): the privacy policy - https://playertwo.plus/developer-terms (Markdown: https://playertwo.plus/developer-terms.md): the developer terms - https://playertwo.plus/developers (Markdown: https://playertwo.plus/developers.md): everything a game gets, on one page ## Asking Player Two, as any agent The MCP server https://playertwo.plus/api/mcp (lol.playertwo/mcp in the official MCP Registry; its card at https://playertwo.plus/.well-known/mcp.json) answers with no key: search_games, list_games, get_game_facts (a game's facts by slug), get_sdk_docs (a part of the docs in Markdown) and where_to_publish (the offer to developers). A studio's tools take the player's agent key, or the player signs in when the client asks (OAuth: https://playertwo.plus/.well-known/oauth-protected-resource/api/mcp). https://playertwo.plus/docs#agents-public ## Submitting a game as an agent An agent can submit its player's game (for the store, or as a raise), upload its pictures, trailers and web builds, and edit its page, with a key the player makes in their studio (https://playertwo.plus/studio, under Agents). Send it as `Authorization: Bearer p2_agent_...`. - MCP server (Streamable HTTP): https://playertwo.plus/api/mcp. Tools: get_submission_guide, check_game, upload_media, upload_web_build, submit_game, update_game, get_game, my_studio, list_trophies, define_trophy, list_boards, define_board, list_builds, make_build_live, name_build (a build's version: each build is called what its developer named it, up to 32 characters of letters, digits, dots, dashes, plus signs and spaces, or else by its number in its game's builds, "12"; also PATCH https://playertwo.plus/api/v1/agent/games//web/builds { key, version }), list_products, define_product, playtest_link (a game's "anyone with the link" playtest before it is out: on, off or reset; also PUT https://playertwo.plus/api/v1/agent/games//testers/link), post_update (an update on a game's page: a title up to 80 characters, Markdown words up to 10,000, up to 4 pictures or a video and 3 pictures uploaded with upload_media's game; its players told in plain words; also POST https://playertwo.plus/api/v1/agent/games//posts; https://playertwo.plus/docs#dev-updates), list_updates, edit_update, pin_update and delete_update (post_update with at, 5 minutes to 90 days ahead, posts it later: only the team sees it and nobody is told until it goes up, as if posted then; edit_update with at moves it, with now: true posts it now; a game's posts with their ids; edit a developer post's title, words or attachments by the same rules, nobody told again, "Edited" on it; pin one first, one a game, any post; delete one, its notices taken back and its files dropped a day later; Player Two's own posts only pinned; also GET https://playertwo.plus/api/v1/agent/games//posts and PATCH and DELETE https://playertwo.plus/api/v1/agent/games//posts/), list_comments, hide_comment, lock_comments and mark_comments_read (players' comments on a game's posts: read them, with which are new for the key's player, take one down, lock a post's, mark a post's or every post's read; agents don't comment; also GET and PATCH https://playertwo.plus/api/v1/agent/games//posts//comments, DELETE .../comments/, POST https://playertwo.plus/api/v1/agent/games//comments/read), and get_rooms (a game's multiplayer rooms now and their players' connections: ping, p90, jitter, bytes a second, corrections, drops; read-only, also GET https://playertwo.plus/api/v1/agent/games//rooms). - Agent API: https://playertwo.plus/api/v1/agent/guide explains every field, its limits, the rules and an example of each kind. Submit with POST https://playertwo.plus/api/v1/agent/games {"draft": {...}, "publish": true}. - API spec: https://playertwo.plus/openapi.json (OpenAPI 3.1): every call of the agent API, the game SDK (a play session's, rooms' and a game server's), a game's services for its developer and the public reads, with the four ways a call says who it is (an agent key p2_agent_, a play session p2s_, a game key p2g_, a signed-in player). - A web build bigger than a zip (up to 5 GB and 20,000 files): once the game is on Player Two, push its folder with https://playertwo.plus/cli/p2.mjs (Node 20 or later, nothing to install): `P2_AGENT_KEY=p2_agent_... node p2.mjs push --game [--version 1.4.0]` (--version names the build; left out, it is its number in the game's builds). It plays at https://.playertwo.cc, with no code changes. The three calls it makes: https://playertwo.plus/docs/hosting#push-calls. A game on its own site that Player Two serves pushes its rooms' code alone: `node p2.mjs push --game --room` (its room.js or room/ folder, never served): https://playertwo.plus/docs/hosting#room-code. - A raise is live at once but takes no money until the player (a human) checks their identity in their studio. Agents can't do that, or pay, claim money or change accounts. ## Reading a game's page Every game's page (https://playertwo.plus/game/) has an "At a glance" part, shown to players and agents alike, `
`: what the game is, who made it, when it came out and when it was last updated, where to play it and whether a guest can, how to play (its controls and goals, in its developer's words), whether it is multiplayer, its players and reviews, the Player Two services it has, and how to report a bug. It is in the page's HTML even when the page asks a player's age first (there, visually hidden). The same game as JSON: https://playertwo.plus/api/v1/games/, and as Markdown (its words, features, price, these facts and its updates): https://playertwo.plus/game/.md. ## Everything a game and its developer get Each with its part of the docs and, for the game SDK, its calls: `p2.*` and `PlayerTwo.*` in the game's page, `room.*` in a room it joined, `script.*` in its room.js on Player Two's servers, `server.*` from its own server with its game key (https://playertwo.plus/api/v1/sdk/server/...). ### On Player Two - A store page, free or paid: https://playertwo.plus/docs#dev-listing - A raise on the launchpad: https://playertwo.plus/docs#dev-raising - Stages, proof and votes: https://playertwo.plus/docs#stages - Web builds and downloads: https://playertwo.plus/docs#dev-builds - Pushing a build: https://playertwo.plus/docs/hosting#push - Testers first, history and rollback: https://playertwo.plus/docs/hosting#push - From GitHub: https://playertwo.plus/docs/hosting#github - Your own site, served: https://playertwo.plus/docs/hosting#follow - Room code for your own site: https://playertwo.plus/docs/hosting#room-code - Saves with no code: https://playertwo.plus/docs/hosting#saves - A free trial: https://playertwo.plus/docs/hosting#trial - Editing a live page: https://playertwo.plus/docs#dev-editing - Earnings and payouts: https://playertwo.plus/docs#money-payouts - Webhooks: https://playertwo.plus/docs/webhooks - MCP and the agent API: https://playertwo.plus/docs#agents - Your rooms' connections, for your agent: https://playertwo.plus/docs/sdk#rooms-stats ### In your game - One script tag, or npm: https://playertwo.plus/docs/sdk#call-init (PlayerTwo.init, PlayerTwo.ready, PlayerTwo.env, PlayerTwo.v, p2.env, p2.player, p2.game, p2.call) - Building without Player Two: https://playertwo.plus/docs/sdk#start-local - Who is playing: https://playertwo.plus/docs/sdk#call-me (p2.me) - Trophies and levels: https://playertwo.plus/docs/sdk#call-trophies (p2.trophies, p2.trophies.list, p2.trophies.unlock) - Leaderboards: https://playertwo.plus/docs/sdk#call-scores (p2.scores, p2.scores.submit, p2.scores.list) - Stats: https://playertwo.plus/docs/sdk#call-stats (p2.stats, p2.stats.get, p2.stats.set, p2.stats.add) - Cloud saves: https://playertwo.plus/docs/sdk#call-saves (p2.saves, p2.saves.list, p2.saves.get, p2.saves.put, p2.saves.delete) - Add-ons: https://playertwo.plus/docs/sdk#addons (p2.addons, p2.addons.list, p2.addons.has) - In-game purchases: your studio's products, or your own prices: https://playertwo.plus/docs/sdk#purchases (p2.store, p2.store.list, p2.store.owned, p2.store.buy, p2.store.pay) - Presence: https://playertwo.plus/docs/sdk#call-presence (p2.presence, p2.presence.set) - Identity and invites: https://playertwo.plus/docs/sdk#call-identity (p2.identity, p2.invite, p2.params) - Loading and play events: https://playertwo.plus/docs/sdk#call-gameplay (p2.loading, p2.loading.done, p2.gameplay, p2.gameplay.start, p2.gameplay.stop) - The overlay: https://playertwo.plus/docs/sdk#call-overlay (p2.on) - On air in the Plaza: https://playertwo.plus/docs/sdk#call-air (p2.onAir) - Test mode and demos: https://playertwo.plus/docs/sdk#test (p2.test, p2.demo) - The player's avatar, 2D and 3D: https://playertwo.plus/docs/sdk/travel#avatar (p2.avatar) - What they have from the Plaza shop: https://playertwo.plus/docs/sdk/travel#avatar-items (p2.items, p2.items.list) - Travel with the party: https://playertwo.plus/docs/sdk/travel#travel (p2.travel, p2.travel.to, p2.travel.home, p2.travel.links) - Played with recently: https://playertwo.plus/docs/sdk/travel#played - Trophies across games: https://playertwo.plus/docs/sdk/travel#trophies ### Your server - Game keys and checks: https://playertwo.plus/docs/sdk#server-keys (server.session, server.trophies, server.boards, server.addons) - Signed identity: https://playertwo.plus/docs/sdk/connect#identity (server.identity) - Charges at your own prices, purchase receipts and consumables: https://playertwo.plus/docs/sdk#store-server (server.store) - Match reports: https://playertwo.plus/docs/sdk/travel#played-match (server.match) - Your own accounts: https://playertwo.plus/docs/sdk/connect#accounts - Desktop launch tickets: https://playertwo.plus/docs/sdk#desktop-tickets - Unity, Godot and the app: https://playertwo.plus/docs/sdk#launcher ### Multiplayer - Matches, codes and parties: https://playertwo.plus/docs/sdk#rooms-join (p2.rooms, p2.rooms.join, p2.rooms.create, p2.rooms.party, room.id, room.code, room.mode, room.size, room.me, room.host, room.isHost, room.players, room.leave) - Messages and shared state: https://playertwo.plus/docs/sdk#rooms-talk (room.send, room.set, room.get, room.state, room.owners, room.on) - Smooth movement: https://playertwo.plus/docs/sdk#rooms-sync (room.sync) - The fast lane: https://playertwo.plus/docs/sdk#rooms-fast (room.stats, room.ping, room.now) - Timing, each player's ping in room.js, and where a room runs: https://playertwo.plus/docs/sdk#rooms-network - Your server code: https://playertwo.plus/docs/sdk#rooms-scripted (room.scripted, script.defineRoom, script.id, script.tickRate, script.onCreate, script.onStart, script.onJoin, script.onLeave, script.onMessage, script.onTick, script.players, script.player, script.send, script.state, script.state.get, script.state.set, script.kick, script.close, script.data, script.store, script.store.get, script.store.set, script.store.delete, script.store.keys, script.save, script.now, script.log) - Input-run rooms: https://playertwo.plus/docs/sdk#rooms-inputs (room.inputs, script.confirm) - Corrections: https://playertwo.plus/docs/sdk#rooms-corrections (script.onSync, script.correct, script.past) - Server-moved objects: https://playertwo.plus/docs/sdk#rooms-objects (room.objects, script.objects, script.objects.set, script.objects.get, script.objects.delete, script.objects.keys) - Physics engines: https://playertwo.plus/docs/sdk#rooms-engines - Teams and party matches: https://playertwo.plus/docs/sdk#rooms-teams (room.teams, script.teams, script.team) - Matching by skill, rated by mode: https://playertwo.plus/docs/sdk#rooms-matching (script.result) - A ranked leaderboard from your room code: https://playertwo.plus/docs/sdk#rooms-ranked (script.score, script.trophy) - Watching and replays: https://playertwo.plus/docs/sdk#rooms-watch (p2.rooms.watch, p2.rooms.live, p2.rooms.replays, p2.rooms.replay, room.watching, room.replay, script.open, script.record) - Worlds of 256: https://playertwo.plus/docs/sdk#rooms-worlds (p2.rooms.world, room.world, room.gated, script.near, script.between) - Voice chat: https://playertwo.plus/docs/sdk#rooms-voice (room.voice, room.voice.who, room.voice.join, room.voice.leave, room.voice.mute, room.voice.isMuted, room.voice.volume, room.voice.speaking, room.voice.viaParty) - A bad connection, on purpose: https://playertwo.plus/docs/sdk#rooms-simulate (p2.rooms.simulate) - Releases that never cut a match: https://playertwo.plus/docs/sdk#rooms-releases ## More - Docs: https://playertwo.plus/docs - Everything a game gets, on one page to share (and save as an image): https://playertwo.plus/developers - Where to publish a web game: Player Two beside itch.io, Poki, CrazyGames, Steam and Discord Activities, from each platform's own pages (checked November 8, 2026): https://playertwo.plus/developers/compare - The game SDK (trophies, stats, leaderboards, saves, presence, and rooms for multiplayer with no server of the game's own, for hosted games): https://playertwo.plus/docs/sdk - Travel and avatars (the player's critter as pixels and a 3D model, doors to other games with the party, played with recently): https://playertwo.plus/docs/sdk/travel - On npm: playertwo-sdk (init() for bundled code, and the SDK's TypeScript types, a room's code included: https://www.npmjs.com/package/playertwo-sdk), and a new game from a starter: `npm create playertwo-game@latest my-game` (https://www.npmjs.com/package/create-playertwo-game) - Hosting a web game here, or playing it from its own site: https://playertwo.plus/docs/hosting - Help: https://playertwo.plus/help - Content policy: https://playertwo.plus/policy --- --- title: "Help · Player Two" url: https://playertwo.plus/help description: "Answers about Player Two (playertwo.lol), the store for browser games: signing in, your balance and adding funds, buying games, pre-orders, free games and your library, refunds, downloads, raises and fees, payouts and your account." source: Player Two (https://playertwo.plus), made from the page itself --- Help center # How can we help? Short answers about your balance, your games, backing a raise and getting paid. If you don’t find what you need, write to us at hello@playertwo.lol. - [**What Player Two is** A game store and a launchpad in one.](https://playertwo.plus/help#about) - [**Balance and top-ups** Your balance, adding funds, paying.](https://playertwo.plus/help#balance) - [**Buying a game** Paying, and playing what you bought.](https://playertwo.plus/help#buying) - [**Free games and your library** Adding, hiding and wishlisting games.](https://playertwo.plus/help#library) - [**Backing a game** Putting money into a raise on the launchpad.](https://playertwo.plus/help#backing) - [**Fees** What Player Two takes, and when.](https://playertwo.plus/help#fees) - [**Stages, proof and votes** How raised money reaches the developer.](https://playertwo.plus/help#stages) - [**Refunds** When money comes back to you.](https://playertwo.plus/help#refunds) - [**Backer passes and the player pass** What they are, and how rarity works.](https://playertwo.plus/help#passes) - [**Claiming earnings** Your share of sales, paid to your balance.](https://playertwo.plus/help#earnings) - [**Payouts to developers** Stage payouts, sales, and the checks.](https://playertwo.plus/help#payouts) - [**Downloads** Windows, Mac and Linux builds, and demos.](https://playertwo.plus/help#downloads) - [**Your account** Emails, your data, deleting your account.](https://playertwo.plus/help#account) - [**Contact** Write to us, or report a game.](https://playertwo.plus/help#contact) ## What Player Two is ### What is Player Two? Player Two is a game store and a launchpad in one place. In the store you buy games, or add free ones to your library, and play them. On the launchpad you back games still being made; once out, backers get a part of each sale, as the developer set. ### How does backing work, in a minute? A developer asks for money to make their game: a goal, split into stages of work. You put in what you like, from the smallest stake up. Player Two holds the money and pays each stage to the developer only once they show proof that it’s done. If a raise falls short of its goal, everyone gets their money back in full. Once the game sells, its backers share a part of every sale. ### Who can put a game on Player Two? Any developer with an account. A game goes live the moment it’s submitted. Before a raise can take money, its developer proves who they are with Didit (a photo of an official ID and a selfie); developers who only sell their games or give them away aren’t asked. Every game has to follow our [content policy](https://playertwo.plus/policy). ### What do I need to start? Nothing, to look around and play free web games. To buy, back, wishlist or add games to your library, sign in with email, Google, Discord or X. Your balance is set up for you the first time you sign in. ## Balance and top-ups ### What is my Player Two balance? When you first sign in, you get a balance of your own, kept for you by Privy, the company that runs sign-in for us. There’s nothing to install and no recovery phrase to keep. Everything you pay on Player Two comes from your balance, and everything paid to you lands in it. It shows at the foot of your profile. ### What money is my balance in? Dollars. Behind the scenes they are held as USDC, a digital dollar meant to be worth one US dollar each, on the Base network; you never need to deal with that. Every price on Player Two is in dollars. ### How do I add funds? Press **Add funds** at the foot of your profile, or on any game when you’re short. Pick an amount and pay by card, Apple Pay, Google Pay or bank; it lands in your balance in a few minutes, and a purchase you started goes on by itself. Have funds somewhere else already? Under **Transfer in**, the same window shows your own address to send USDC on Base to. ### Do I pay network fees? No. Player Two pays the network fees on every payment and every payout. A card or bank top-up runs through a payment provider, which may charge its own fee for that. Player Two adds no fee to top-ups. ### What happens when I pay? It shows each step: getting it ready, paying from your balance, making it yours. Nothing leaves your balance until you press Buy or Back it, and a payment can only be made once, however many times or windows you press it in. ## Buying a game ### How do I buy a game? Open the game’s page and press **Buy**, then **Buy now**. A moment later the game is in your library and yours to play, and your receipt is in your email. ### Where does my money go? Player Two keeps 10% of every sale. If the game raised money here, its backers share the part of each sale its developer promised them. The developer gets the rest. See [Fees](https://playertwo.plus/help#fees) for a worked example. ### How do I play a game I bought? Press **Play** on its page. A paid web game opens from a play link made just for you, which lasts 6 hours; the page asks for a fresh one each time you play. Some games are played on their developer’s own site: those are the developer’s responsibility, not ours. For games you install, see [Downloads](https://playertwo.plus/help#downloads). ### I paid twice for the same game. What happens? A second purchase of a game you already own is sent straight back to your balance. You don’t need to do anything. Like every payout, it’s checked by Player Two before it goes out. ### Can I try a game before I buy it? Many games that cost money have a demo: **Play demo** beside Buy, wherever the game is (the room’s big screen, the store, its page), says how long (a quarter of an hour to two hours, as its developer chose), and then how long you have left. It plays the whole game in your browser, and the time counts only while it’s open. You can stop and come back until it’s used up; then the game closes and offers to sell it to you, and if you buy it, everything you saved in the demo carries on. One demo a game. Games you install have demo downloads of their own (see [Downloads](https://playertwo.plus/help#downloads)). ### Can I get my money back for a game I bought? There’s no refund button for a purchase. Many games that cost money have a demo to try first. If something went wrong with a game you bought, write to [hello@playertwo.lol](mailto:hello@playertwo.lol?subject=A%20purchase) and tell us what happened. ### What is a pre-order? A paid game that comes out on a set day can be bought ahead: press **Pre-order** on its page. You pay its price now, and it goes into your library at once, marked with its day; you can play it from that day. Player Two holds your money until then, and the game’s developer is paid only once it’s out. Until its day you can cancel it for all your money back: press **Pre-ordered** on its page, then **Cancel the pre-order**. If the game is taken down, archived or made private before its day, every pre-order comes back by itself, in full. If its day moves, your pre-order stands, and we tell you the new day. A pre-order is at the game’s price: sales, codes, gifts and bundles are for games that are out. ### What are add-ons and bundles? **Add-ons** are more of a game sold on their own: a map, a chapter, a pack. They’re listed under a game’s button on its page, for players who have the game, and the game sees the ones you have. **Bundles** are a studio’s games sold together, with a share off: you pay only for the ones you don’t have yet, each at its price now (a sale’s, if one is on) less the bundle’s share. Each game in a bundle is yours for good, as if you’d bought it alone; a bundle’s games aren’t refunded one by one, so write to us if something went wrong. ### What are in-game purchases? Some games sell things inside themselves: coins, skins, boosts, a season pass. The game has its own shop, but you always pay on **Player Two’s card**: the game steps back to one side, and the card beside it shows what it is, its price and your balance before and after. Hold **Hold to buy** to pay (a tap never buys), or press **Not now**. No game can take money from your balance without that. If your balance is too low, the card lets you add funds right there. You need the game first (a free trial isn’t enough), and a Player Two account: nothing is sold to a guest. Buying inside games works in Player Two’s player on the web; the Player Two app on a computer can’t yet. In-game purchases are final: if a payment lands on something that’s already yours, or that the game withdrew a moment before, it comes straight back to your balance by itself. Each one gets a receipt by email. ### I have a code for a game. Where does it go? At checkout: press **Have a code?** under the price, type it and press Apply. A link with a code fills it in for you. A code takes a share off the price; it doesn’t add to a sale, so you pay whichever is lower. Codes are for buying a game for yourself, not as a gift. ## Free games and your library ### How do I get a free game? Open its page and press **Add to library**. You need to be signed in for that. Free web games also play without an account. ### What’s in my library? The games you bought, the free games you added, and the games that came with a backer pass. Games you backed show there too, so you can follow them. ### How do I chat with friends, other players or a studio, or play together? Open your profile (your picture at the top right) and press **Chat**, or **Chat** beside Friends on your profile page; your picture shows a count when messages, requests or a party invitation wait. **Friends** has your friends, with what each is playing and your messages with them (a game they’re in has **Join**), and the other players you talk with. To write to anyone else, press **Message** on their page: if they aren’t your friend yet, your first message waits in their **Requests** until they answer, and until then you can send three. Requests are first messages to you: **Answer** and you can both write freely, **Not now** puts it away (they aren’t told), or **Block**. In your profile’s Privacy you can take first messages from friends only. **Studios**: press **Message** beside a studio’s name on one of its games’ pages, or on the studio’s page, to write to the team behind it. Its team answers as the studio, each answer with the name of who wrote it, and you’re told when an answer comes. If you run a studio, or you’re on one’s team, its **Messages** are in your studio’s console: everyone on the team sees them and can answer, and can block a player from writing to it. A **party** is up to eight friends together: start one, invite friends, and chat; its leader can bring everyone to the game they’re playing, and the others see Join. Press **Join voice** in the Party panel to talk with your party on every page and while you play: a bar at the bottom left shows who’s in it (a ring on whoever is speaking), your microphone, **Deafen** and **Leave**; in the panel, choose an open mic or **push to talk** (hold V, in a game too, or hold the bar’s microphone; your browser remembers which), and how loud each one is. Only your party hears you, nothing is recorded, and leaving the party takes you out of its voice. The Plaza’s own voice, heard by those near you, is the same microphone, mute and push-to-talk. In a game with voice of its own, someone you hear in your party’s voice isn’t heard again through the game’s: you hear each other once. **Block** (on a thread, a player in the Plaza, or someone you played with) works both ways: no messages, invitations or voice either way, a friendship ends, neither of you sees the other in the Plaza or in each other’s Lobby, and if you were in a party together you leave it. Who blocked whom isn’t said. **Report** on a message sends it to Player Two’s moderators. ### What is the Lobby? Everything happening on Player Two, live, at [playertwo.lol/lobby](https://playertwo.plus/lobby): games bought and given, free games added, money put into raises and raises funded, trophies unlocked and levels reached, reviews, new players, and new games, sales and updates from studios. At the top is what came into games in the last 24 hours (or since Player Two opened), and how busy each hour was; beside it, the day’s biggest, its rarest trophy, and who gained the most XP. Filter it by kind, by game, or to your friends and the players and studios you follow. Only players who share their activity with everyone are in it (your profile’s Privacy); a purchase or a backing shows what was paid, as a raise’s page shows its backers. ### Can I hide a game? Yes. In your profile, press the eye on a game’s cover. A hidden game stays yours. To bring it back, open your full profile page, find it under **Hidden**, and show it again. ### What does the player count on a card mean? It’s how many people have that game in their library. ### How does the wishlist work? Press the heart on any game to put it on your wishlist (sign in first), and press it again to take it off. The number beside the heart is how many players have it on theirs. The room has a **Wishlist** tab with all of yours. ### Someone sent me a playtest link. What do I do? Open it: the game’s page opens, even one that isn’t out yet and is in no list. Sign in (or make an account) and press **Playtest**: you’re one of its testers from then on, and its page stays open to you without the link. You play it free, as a test, before it’s out: what you earn there is kept apart, and your scores show only to you. Your playtest ends if its developer turns the link off or makes a new one (unless they invited you themselves), and once the game comes out it’s in the store like any other. A link that says it isn’t on any more was turned off or replaced: ask its developer for the new one. ### I have a key for a game. How do I use it? A key is a free copy of a game, from the people who made it, sent to press and creators. Open the link that came with it, or go to [Redeem a key](https://playertwo.plus/redeem) and type it in, then sign in: the game is in your library for good. Each key works once, and its developer sees who redeemed it. ### What do a game's age and labels mean? Its developer says who it’s for (everyone, or 13, 16 or 18 and up) and what it shows, like violence or strong language. Games for adults stay out of the store until you say you are 18 or older, at a game’s page or in your profile’s settings under **What you see**, where you can also hide games with any label you’d rather not see. A game’s page also lists its languages and what it does for players with a disability, and the store’s search can look for both. ## Backing a game ### What is a raise? A developer’s ask for money to make their game. Every raise sets out: - **a goal**: the whole amount asked for; - **a smallest stake**: the least one backer can put in; - **a closing date**, a set number of days after it opens; - **the backers’ share**: the part of every sale that goes to backers; - **a planned price**: what the game will cost at launch; - **stages**: parts of the work, each paying out its share of the money, adding up to 100%. Once a raise is open, these terms can’t change. ### How do I back a game? Open the game’s page and go to **Your cut**. Choose your stake and see what it could make. Then review it, tick the box that says you understand the risk, and confirm. Approve the payment from your balance. You get the next backer number, and a backer pass if your stake is at least the game’s planned price. ### Is there a limit to how much I can put in? Only what the raise still needs. A stake can’t be less than the smallest stake, or more than what’s left. When less than the smallest stake is left, you can put in exactly what’s left. ### What happens when the goal is reached? The raise closes the moment it’s full, and the first stage of work is under way. Each stage is due a set number of days after that. ### What if a raise doesn’t reach its goal? When its closing date comes and the goal isn’t met, every backer is refunded in full. ### What if my payment arrives after the raise closed or filled? It’s sent straight back to your balance, and we tell you. ### What do I get for backing? It depends on the raise, and its page says which. Most share their sales: a share of every sale the game makes on Player Two (40% or more, as its developer set it), for as long as it sells, split among all backers by what each put in. A rewards raise gives rewards instead, like the game early or your name in the credits, by how much you put in, and no share of sales. Either way, if your stake is at least the game’s planned price, you also get a backer pass, and the game is yours free on launch day. ### Is backing safe? > **Backing is a risk.** You could lose what you put in. Backers are paid only from sales, so a game that sells little earns its backers little. Money already paid out for proven stages can’t come back. Put in only what you can afford to lose. ### Can I take my money back? Not while a raise runs. Money comes back only when a raise closes short, when backers vote for a refund, or when Player Two refunds what’s left. See [Refunds](https://playertwo.plus/help#refunds). ### Can developers back their own game? No. Developers can’t back, object to or vote on their own games. ## Fees Player Two takes a share in three places, and nowhere else. _Fees at a glance_ | When | Player Two takes | | --- | --- | | A game is sold | 10% of the price | | Something is bought inside a game | 20% of what was paid | | A stage of a raise is paid out to the developer | 5% of that stage | | Network fees on payments and payouts | Nothing: Player Two pays them | | Refunds and claims | Nothing | For a game that raised money, its backers share a part of every sale. The developer set that share when raising: 40% in this example. The developer gets the rest. One sale **$15.00** _10%_ _40%_ _50%_ - **Player Two**: $1.50 - **Backers, by stake**: $6.00 - **The developer**: $7.50 A $15.00 sale of a game whose backers share 40%. A game that didn’t raise money pays its developer 90% of each sale. The raise fee works the same way. Say a $10,000 raise has a first stage worth 30%. When that stage is paid out, $3,000, Player Two keeps $150 and the developer gets $2,850. ## Stages, proof and votes Raised money stays with Player Two until each stage of work is shown to be done. 1. **The developer posts proof** A link backers can open and check, like a public build or a video. 2. **3 days for objections** Any backer can object and say what doesn’t match the stage. - **No objection** The stage is paid out, less 5%. The next stage begins. - **An objection** A vote of backers opens. 3. **A 5-day vote** Votes count by the dollars each backer put in. A missed deadline opens one too. - **More dollars to go on** The stage is paid out, less 5%, or a missed one gets 30 more days. - **More dollars to refund** What hasn’t been paid out goes back, by stake. - **Under 20% of the money voted** Player Two reviews the stage and decides. ### What is a stage? A part of the work, with something to show at the end of it, a share of the money, and a deadline. A raise’s stages add up to 100% of what it raised. ### How is a stage paid out? The developer posts proof: a link backers can check, like a public build or a video. Backers then have 3 days to object. With no objection, the stage is paid to the developer, less Player Two’s 5%, and the next stage begins. ### The proof doesn’t match. What can I do? If you backed the game, press **This doesn’t meet the objective** under the proof, say what’s wrong, and send it. Your objection opens a vote of all the game’s backers. ### How does a vote work? It lasts 5 days. Every backer can vote, and you can change your vote while it’s open. Votes count by the dollars each backer put in. On an objection, backers choose between paying the stage out and refunding what’s left. The side with more dollars behind it wins. ### What if a deadline is missed? If a stage’s deadline passes without proof, a vote opens by itself. Backers choose between giving the developer 30 more days and refunding what’s left. If the developer posts proof during that vote, the vote ends and the proof gets its own 3 days for objections. ### How many backers have to vote? For a vote to count, backers holding at least **20% of the money** must vote. If too few vote, Player Two reviews the stage and decides: pay it, give more time, or refund what’s left. ### What does a vote to refund do? It sends back everything that hasn’t been paid out yet, to every backer in proportion to their stake. Stages already paid out stay paid. ## Refunds _When money comes back_ | What happened | What comes back | | --- | --- | | The raise closed short of its goal | Everything you put in | | Backers voted for a refund | Your share of what hasn’t been paid out | | Player Two reviewed a stage and refunded | Your share of what hasn’t been paid out | | Your payment reached a raise after it closed or filled | That payment, in full | | You paid for a game you already own | That payment, in full | | You paid inside a game for something already yours, or that the game withdrew | That payment, in full | | You cancelled a pre-order before the game’s day | What you paid for it | | A game you pre-ordered was taken down, archived or made private before its day | What you paid for it | | Player Two refunded a purchase of yours (something went wrong with it) | What you paid for it, with Player Two’s reason | ### Where does a refund go? To your Player Two balance. Like every payout, it’s checked by a Player Two admin before it’s sent, and you get a notice once it’s on its way. Refunds carry no fee. When Player Two refunds something you bought (a game, an add-on, something inside a game, or an item from the shop), it’s no longer yours: a game leaves your library, an add-on or item is taken back. Your notice says why. ### Do refunds count toward my player pass? No. Dollars that came back don’t count as backed. ## Backer passes and the player pass ### What is a backer pass? A collectible card for backing a game with at least its planned price. It carries your backer number, and it brings the game: it’s in your library, and yours free on launch day. Its rarity comes from your stake: 1. **Common** The planned price 2. **Rare** $100 or more 3. **Epic** $250 or more 4. **Legendary** $1,000 or more 5. **Mythic** $2,500 or more 6. **Divine** $10,000 or more Each stake counts on its own, and its rarity is set when you back: two $100 stakes bring two Rare passes, not one Epic. If a game will be free at launch, every stake brings a pass. ### Can I show my pass off? Yes. Every pass has its own page, and its link shows the card. Press a pass in your profile to bring it up close and share it. ### What is the player pass? Every player has one. It looks like a backer pass and shows your name and your points, and its rarity climbs as your points do. Press it in your profile to see it up close. _How points are earned_ | What you do | Points | | --- | --- | | Add a free game to your library | 1 | | Pay $1 for games | 10 | | Back a raise with $1 (refunds don’t count) | 20 | Each rarity takes ten times the points of the one before. The last two columns show what that comes to if you only back games, or only buy them: _Player pass rarity_ | Rarity | Points | Backed only | Bought only | | --- | --- | --- | --- | | Common | 0 | $0 | $0 | | Rare | 2,500 | $125 | $250 | | Epic | 25,000 | $1,250 | $2,500 | | Legendary | 250,000 | $12,500 | $25,000 | | Mythic | 2,500,000 | $125,000 | $250,000 | | Divine | 25,000,000 | $1.25M | $2.5M | ## Claiming earnings ### How do I get my share of sales? Each sale of a game you backed adds your share to what you’re owed. Once it comes to $1 or more, press **Claim** at the foot of your profile. It’s paid to your Player Two balance. ### How is my share worked out? The backers’ share of each sale is split among backers by what each has in the raise. Say backers share 40% and you hold 1% of all the money backed: each $15.00 sale earns you $0.06, which is 1% of the $6.00 that goes to backers. ### How long does a claim take? For now, a Player Two admin checks every payout before it’s sent, claims included, so it isn’t instant. You get a notice once it’s sent. ## Payouts to developers ### How do developers get paid? Two ways. Each stage of a raise is paid out as it clears, less Player Two’s 5%. And each sale adds to the studio’s balance, which the developer claims from [their studio](https://playertwo.plus/studio), from $1. ### Where is the money sent? To the studio’s payout address: the developer’s own Player Two balance, unless they change it in their studio. Another address has to take USDC on Base. ### Why is my payout waiting? For now, a Player Two admin reviews and approves every payout before it’s sent: stage payouts, refunds and claims. And when a studio changes its payout address, its payouts are held for 2 days, so nobody can quietly send its money somewhere else. You get a notice when the address changes. If it wasn’t you, tell us at once. ### Do I need to verify my identity? Only if you raise money. Before your raise can take any, you photograph an official ID and take a selfie with Didit, our identity partner, from your studio; it takes about two minutes. Selling games or giving them away needs no check. ### How is the money kept safe? Raised money is held in Player Two’s treasury, an account protected by Privy’s rules: our server alone can only send USDC on Base, and only up to a set amount. Anything larger needs an admin’s own key as well. ## Downloads ### What is the Player Two app? A library for the games you install, like the ones you know from other stores. Sign in once and it lists every game you own and every demo; one press installs a game, a download that stops picks up where it left off, a new version from its developer installs with one more press, and Play starts it. Get it on the [app’s page](https://playertwo.plus/desktop), for Mac and Windows. ### Which systems are there downloads for? Developers can upload builds for Windows, Mac and Linux, as a demo, a full version, or both. A game’s page offers what its developer uploaded. ### Who can download? Demos: anyone signed in. Full versions: only the game’s owners, who bought it or got it with a backer pass. ### Why did my download link stop working? Each download link is made for you, and it expires after 10 minutes. Get a fresh one from the game’s page. Please don’t share your links: they’re personal. The Player Two app takes care of this by itself: it asks for a fresh link whenever it needs one and carries on from where it stopped. ### Are downloads checked? Yes. Every new upload is reviewed by Player Two before players can get it. ## Your account ### Which emails will I get? You choose, kind by kind, in your notification settings: - **Game updates**: news posted by the developers of games you back, have in your library or wishlisted, or whose studio you follow (in plain words: its pictures and formatting are on the game’s page). A developer may edit an update later, which tells nobody again (it says Edited on the page), or delete it, which takes its notice out of your list too. Signed in, you can comment on an update (open it, its comments are under its words): 1 to 2,000 characters, replies too, 60 comments and Discussion answers an hour; you’re told when someone replies to you. Take your own down any time; report anyone else’s (Spam, Abuse, Off-topic) and Player Two’s moderators look at it. The game’s team is marked Developer and can take comments down or lock them; a player the studio blocked, or a banned account, can’t comment. - **Raises you back**: funded, proof posted, votes, stage payouts and refunds. - **Your wishlist**: a game on it comes out, goes on sale or gets a demo. Each once per game. - **Friends**: a player asks to be your friend, or says yes to you. - **Money**: payouts to you. These are always on. Emails go to the address you signed in with. If you signed in with X, Discord or an outside account, we have no email address to send to; your notices still show in your profile. ### Can I change my name, handle or picture? Yes: press **Edit profile** in your profile or on your page. A name is 2 to 32 letters, numbers and spaces. Your handle, the name in your page’s address, can change once every 30 days; links to the old one keep leading to you, and nobody else can take it for 90 days. A few names, like Admin, Support and Player Two, are kept for us. Put up a picture (it’s cropped square, shown round), or keep your pixel creature, which stays yours even if you change your handle. Add a few words about yourself, one website, and the accounts you have elsewhere: X, Discord, Twitch, TikTok, Telegram and GitHub are linked through your sign-in, so they show as yours, and you choose which show on your page. ### Can I follow players, or add friends? Both. Follow anyone from a leaderboard, a review or their page, and what they buy, back, review and add shows in the feed in your profile. Never how much. Add a friend the same way, or by their handle in your profile: once they say yes, you see each other’s activity, and they come first when you send a gift. In your profile’s privacy settings, choose who sees your activity and your friends (everyone, only your friends, or only you), and whether your name shows on the leaderboards. ### What do I get for bringing friends? Points on your player pass: 100 for each friend who arrives through a link you share, signs up and buys or backs a game within 30 days. The first link a friend arrives through is the one that counts. Points only, never money. ### Can I get a copy of my data? Yes. From your profile, download everything we hold about you as a file. ### How do I delete my account? From your profile. Claim anything you’re owed first. Your balance stays yours: it lives with Privy, not with us. ## Contact **Talk to a person** Questions, a problem with a payment, anything else: hello@playertwo.lol. A person reads every message. [Write to us](mailto:hello@playertwo.lol) ### How do I report a game? Press **Report this game** at the foot of its page, choose what’s wrong, and add anything that helps us check. You don’t need to be signed in. You can also email [hello@playertwo.lol](mailto:hello@playertwo.lol?subject=A%20report) with a link to the game. Our [content policy](https://playertwo.plus/policy) says what happens next. ### I think you got a decision wrong. Write to [hello@playertwo.lol](mailto:hello@playertwo.lol?subject=An%20appeal) and tell us which game or account, and why. See [appeals](https://playertwo.plus/policy#appeals). --- --- title: "Documentation · Player Two" url: https://playertwo.plus/docs description: "How Player Two (playertwo.lol), a store for browser games, works: the store and your library, your balance and payments, publishing a game, the launchpad's raises paid out stage by stage on proof, fees, payouts, agents, and moderation." source: Player Two (https://playertwo.plus), made from the page itself --- Documentation # How Player Two works Player Two (playertwo.lol) is a store for browser games: players play free and paid games in the browser or download them, play together online, and meet in the Plaza. For developers, the Player Two SDK (`npm i playertwo-sdk`, or one script tag) gives a web game multiplayer rooms with no server of your own, trophies, leaderboards, cloud saves and in-game purchases; Player Two hosts the build, and AI agents can publish it through its MCP server. See [the game SDK](https://playertwo.plus/docs/sdk), [hosting](https://playertwo.plus/docs/hosting) and [how Player Two compares](https://playertwo.plus/developers/compare). Below: every part of the platform, how money moves through it, and the journeys of the people who use it: players, backers, developers, and Player Two’s own admins. For quick answers, see the [help center](https://playertwo.plus/help). 1. **Players pay** From the Balance every player gets at sign-in, in dollars. 2. **Player Two holds and splits** Raised money waits for proof of each stage. Each sale is split by the same fixed rules. 3. **Developers and backers are paid** Developers stage by stage on proof, and from sales. Backers get a part of each sale of a game they backed, as the developer set. ## Overview ### The store and the launchpad Player Two is a game store and a launchpad in one room. - **The store** holds finished games and demos, free or paid. Buy a game and it’s yours; add a free one to your library. - **The launchpad** holds games still being made, raising money from players. Backers put money in, Player Two holds it, and the developer is paid stage by stage as they prove the work is done. Once the game is out, its backers get a part of each sale, as the developer set. Anyone with an account can put a game on Player Two, and it goes live the moment it’s submitted. Payments are real: USDC on Base. ### Three ideas - **Money waits for proof** Raised money stays with Player Two until each stage of work is shown to be done. - **Backers have a say** Backers can object to proof, and vote on what happens when something goes wrong. - **One set of rules** The same fees and rules for every game: 10% of a sale, 5% of each stage paid out. ## Accounts and balances ### Signing in Sign in with email, Google, Discord or X. Privy, the company that runs our sign-in, sets up your balance the first time you sign in. You can look around and play free web games without an account; you need one to buy, back, wishlist, add games to your library or submit a game. Your public name starts as a made-up one, like Hitbox Lawyer or Slightly Used Kazoo, never your email or your Google name. Change it in your profile: 2 to 32 letters, numbers and spaces. A few names, like Admin, Support and Player Two, are kept for us. ### Your balance Your balance is held in USDC, a digital dollar (one USDC is meant to be worth one US dollar), on Base, the network it moves on. You pay from it, and everything paid to you lands in it. There’s nothing to install and no recovery phrase to keep. It lives with Privy, so it stays yours even if you leave Player Two. It shows at the foot of your profile. ### Adding funds **Add funds**, at the foot of your profile, opens Privy’s top-up window. Pay by card or bank, or transfer funds in from another account or app: whatever you use arrives as USDC on Base. Player Two pays the network fees. A card or bank provider may charge its own fee; Player Two adds none. ### Paying Every payment goes through the same steps, and the button you pressed says which one it’s on: 1. **Set up** Player Two makes the payment: what it’s for, how much, where it goes. 2. **Check your balance** Short of funds? You’re offered a top-up first. 3. **Approve** You approve the transfer from your balance, within 30 minutes. 4. **Confirmed on Base** It counts once the transfer is confirmed: from your balance, the right amount, to Player Two. Nothing leaves your balance until you approve. Our server also watches the treasury on Base, so a transfer that arrives without being confirmed the usual way is still matched to its payment: from the same balance, for the same amount, within three days. ## The room Home is the room: a dome lined with screens, a ring of game cards in front of you, and one big screen for the game you’ve chosen. Opening a game flies you into its big screen and onto its page. ### Browsing - **Tabs**: All games, Store, Launchpad, Library and Wishlist. - **Search** by name, studio or genre, and narrow by stage, genre, price and where a game plays. - **Arrows** under the cards step through the ring, with where you are in it. - An empty tab, or a search with no match, says so and offers somewhere to go: free games, the store, or submitting a game. ### Cards and the big screen Each card wears its studio’s name at the top. Its foot shows where the game stands: a beam for a raise, a ring of arcs while it’s being built, and a pulse of daily players once it’s out. Owned games say so on the card. The player count is how many people have the game in their library. The big screen shows the chosen game’s name, its raise if it has one, a heart for your wishlist, and its buttons: **Back this game** for a raise, **Buy** for a game on sale, **Play free** for a free one. ### Your profile Press your pixel creature at the top of the room. The panel shows your player pass and your figures, your backer passes, your library, your wishlist, your studio if you have one, and your balance with **Add funds** and **Claim**. Open it out into your full profile page for everything, including your notices and payouts, the games you’ve hidden, and your name. ## Game pages Every game has its own page. It opens on the game’s art in depth, then runs in numbered sections; a section shows only when the game has something for it. _The sections of a game’s page_ | Section | What it shows | | --- | --- | | The game | The trailer, screenshots, genres, modes and platforms. Each tag finds games like it. | | The plan | What the money builds, branch by branch, and when each part ships. For a game that’s out: its features. | | The ask | For a raise: the pot, its stages and how each is paid out, the latest proof, and any open vote. | | Your cut | The backers’ share of every sale, the stake calculator, and the backer pass a stake brings. | | The builder | The developer’s note, their figures, their GitHub activity, and their other games. | | Updates | Posts from the developer, and the raise’s own news. | At the foot of every page is **Report this game**. The game’s developer and Player Two’s admins also see **Edit page** in its top bar. ## The store and your library ### Buying Press **Buy** on a game’s page and approve the payment. Once it’s confirmed, the game is in your library. If you hold the game’s backer pass, it’s already yours. A second purchase of a game you already own is sent straight back. ### Free games Press **Add to library** on a free game’s page (sign in first). Free web games also play without an account. ### Your library Your library holds the games you bought, the free games you added and the games that came with a backer pass. Games you backed show there too, so you can follow them. Hide any game with the eye on its cover in your profile; it stays yours, and your full profile page lists hidden games so you can show them again. ### Your wishlist The heart on any game puts it on your wishlist (sign in first); press it again to take it off. The number beside a heart is how many players have that game on their wishlist. The room’s **Wishlist** tab shows yours. ### Playing - **Web games** play on Player Two, in its player, each from its own address on playertwo.cc, which keeps it apart from your account. Its saves follow you to any device. A free one is anyone’s to play. - **A paid web game** plays from a play link made for each of its owners, which lasts 6 hours. The page asks for a fresh link each time you press Play. - **Games played on their developer’s own site** are the developer’s responsibility: Player Two doesn’t host them and can’t control who reaches them. ### Downloads Developers can upload builds for Windows, Mac and Linux, as a demo and as a full version. _Who can download what_ | Build | Who can download it | | --- | --- | | Demo | Anyone signed in | | Full version | The game’s owners: players who bought it or got it with a backer pass | Each download link is personal and expires after 10 minutes; the game’s page makes a new one when you need it. Every new upload is reviewed by Player Two before players can get it. The [Player Two app](https://playertwo.plus/desktop) for Mac and Windows is the easy way: it lists every game you own and every demo, installs one with a press, picks up a stopped download where it left off, offers a developer’s new version as an update, and starts the game with Play. It fetches its own fresh links as it goes. ## The launchpad ### The terms of a raise _What every raise sets_ | Term | What it means | Limits | | --- | --- | --- | | Goal | The whole amount asked for | Up to $10 million | | Smallest stake | The least one backer can put in | From $1 | | Closing date | When the raise ends if it isn’t full | 3 to 120 days after it opens | | What backers get | A share of every sale, or rewards only | A share of 40% to 85%, or up to 8 rewards | | Planned price | What the game will cost at launch. A stake of at least this brings a pass | Up to $500 | | Stages | Parts of the work, each with proof to show, a share of the money and a deadline | 1 to 8, adding up to 100% | Once a raise opens, its terms are fixed: the developer can still change how the page looks and reads, but never the terms backers put money in on. ### Backing On a raise’s page, **Your cut** works out what a stake could make as copies sell. Choose a stake, review it (what it makes if the game sells well, breaks even, or sells nothing), tick the box that says you could lose what you put in, and confirm. - A stake can be anything from the smallest stake up to what the raise still needs. When less than the smallest stake is left, the last backer puts in exactly what’s left. - Each stake gets the next backer number for that game, starting at 1. - A stake of at least the planned price brings a backer pass, and the game: it goes in your library, and it’s yours free on launch day. - Developers can’t back, object to or vote on their own games. ### Funded, or closed short A raise closes the moment it reaches its goal. It’s funded: the first stage is under way, and each stage’s deadline counts from that day. If the closing date comes first, every backer is refunded in full. Money that arrives after a raise has closed or filled is sent straight back. ### The risk > **Backing is a risk.** Backers are paid only from sales, so a game that sells little earns its backers little, and money paid out for proven stages can’t come back. Put in only what you can afford to lose. ## Stages, proof and votes Raised money is paid to the developer one stage at a time, and only once that stage is shown to be done. 1. **The developer posts proof** A link backers can open and check, like a public build or a video. 2. **3 days for objections** Any backer can object and say what doesn’t match the stage. - **No objection** The stage is paid out, less 5%. The next stage begins. - **An objection** A vote of backers opens. 3. **A 5-day vote** Votes count by the dollars each backer put in. A missed deadline opens one too. - **More dollars to go on** The stage is paid out, less 5%, or a missed one gets 30 more days. - **More dollars to refund** What hasn’t been paid out goes back, by stake. - **Under 20% of the money voted** Player Two reviews the stage and decides. ### Proof and objections When a stage is done, the developer posts its proof from their studio: a link backers can open and check, like a public build or a video, with a note if they like. Backers are told, and have **3 days** to object. With no objection, the stage is paid out to the developer, less Player Two’s 5%, and the next stage begins. Any backer can object: under the proof, press **This doesn’t meet the objective** and say, in a sentence or more, what doesn’t match. The first objection opens a vote. ### Votes A vote lasts **5 days**. Every backer can vote, and change their vote while it’s open. Votes count by the dollars each backer has in the game. On an objection, the choice is to pay the stage out or to refund what’s left. The side with more dollars behind it wins; a tie goes the game’s way. While backers vote on a stage’s proof, that proof can’t change. ### Too few votes A vote counts only if backers holding at least **20% of the money** take part. If too few vote, Player Two reviews the stage and decides: pay it, give more time, or refund what’s left. ### Missed deadlines When a stage’s deadline passes without proof, a vote opens by itself, and backers choose between giving the developer **30 more days** and refunding what’s left. If the developer posts proof while that vote is open, the vote ends and the proof gets its own 3 days for objections. ### Refunds A refund sends back everything that hasn’t been paid out, to every backer in proportion to their stake. Stages already paid out stay paid. After the last stage is paid, the raise is complete. ## Money ### The split of a sale Player Two keeps 10% of every sale. For a game that raised money, its backers share the percentage its developer set when raising, split among them by stake. The developer gets the rest. Here is a $15 sale with a 40% backers’ share: One sale **$15.00** _10%_ _40%_ _50%_ - **Player Two**: $1.50 - **Backers, by stake**: $6.00 - **The developer**: $7.50 A game that didn’t raise money pays its developer 90% of each sale. ### The raise fee Player Two keeps 5% of each stage of a raise as it’s paid out to the developer. A $10,000 raise whose first stage is worth 30% pays $3,000 for that stage: $150 to Player Two, $2,850 to the developer. Refunds carry no fee. ### Earnings and claims Sales build up what each backer and each studio is owed. Either claims it once it comes to $1 or more: - **Backers** claim from the foot of their profile. It’s paid to their Player Two balance. - **Developers** claim their studio’s earnings from [their studio](https://playertwo.plus/studio). It’s paid to the studio’s payout address. A backer’s share of a sale is their part of all the money backed. With a 40% share, a backer holding 1% of the raise earns $0.06 from each $15 sale. ### Payouts A payout is money leaving Player Two’s treasury: a stage paid to a developer, a refund, or a claim. For now, every payout is reviewed and approved by a Player Two admin before it’s sent. 1. **Queued** A stage clears, a refund is due, or someone claims. 2. **Reviewed** A Player Two admin checks the amount, where it goes, and why. 3. **Sent** The treasury sends it as USDC on Base. 4. **Confirmed** Confirmed on Base. The person paid gets a notice. A studio’s payouts go to its payout address, which is the developer’s own Player Two balance unless they change it. When a studio changes its payout address, its payouts are held for 2 days and the developer is told, so a stolen session can’t quietly send a studio’s money elsewhere. No payout goes to a studio until its developer’s identity check is approved. ### Money sent back Two kinds of payment are sent straight back to the balance they came from: money that reaches a raise after it has closed or filled, and a second purchase of a game the player already owns. The player is told why. ### The treasury Raised money and sales wait in Player Two’s treasury, an account protected by Privy’s policies: - Our server alone can send only USDC on Base, and only up to a cap on each transfer. - A larger payout needs an admin’s own key as well as the server’s. - What the server sends by itself is also limited each day. - Changing the treasury or its rules takes both keys. ## Passes ### Backer passes A backer pass is a collectible card that comes with a stake of at least the game’s planned price. It carries the backer’s number for that game and brings the game itself: it’s in their library, and theirs free on launch day. Its rarity comes from the stake: 1. **Common** The planned price 2. **Rare** $100 or more 3. **Epic** $250 or more 4. **Legendary** $1,000 or more 5. **Mythic** $2,500 or more 6. **Divine** $10,000 or more Each stake counts on its own, and its rarity is fixed when it’s made: two $100 stakes bring two Rare passes. When a game will be free at launch, every stake brings a pass. Every pass has its own page whose link shows the card, to share anywhere. Passes are records kept by Player Two. ### The player pass Every player has a player pass. It’s made like a backer pass, with the player’s creature and name, and the points they’ve earned where a stake would be. Its rarity climbs with points, each rarity ten times the last. _How points are earned_ | What you do | Points | | --- | --- | | Add a free game to your library | 1 | | Pay $1 for games | 10 | | Back a raise with $1 (refunds don’t count) | 20 | _Player pass rarity_ | Rarity | Points | Backed only | Bought only | | --- | --- | --- | --- | | Common | 0 | $0 | $0 | | Rare | 2,500 | $125 | $250 | | Epic | 25,000 | $1,250 | $2,500 | | Legendary | 250,000 | $12,500 | $25,000 | | Mythic | 2,500,000 | $125,000 | $250,000 | | Divine | 25,000,000 | $1.25M | $2.5M | ## For developers ### Listing a game Press the plus in the room, or go to [Submit a game](https://playertwo.plus/submit), and sign in. First choose a path: put the game in the store, or raise for it. Then fill in the game, its look, where it plays, its price or its raise, the plan (for a raise), and you. The game’s page builds itself beside the form as you type, and the game goes live the moment you finish. - **Needed**: a name, a tagline, what it is in a paragraph, at least one genre, where it plays, a trailer, its key art (one square picture, 2400 by 2400 at best, with what matters in the middle: its card shows the middle two-thirds, its page’s first screen is made 3D from it, and the store’s wide places show a band of it), and your studio’s name. A game for the store also needs a way to play it; a raise needs its stages. - **Price**: free, or up to $500 in USDC. A game that didn’t raise money pays you 90% of every sale. - **Its look**: the launch trailer plays in the form as it will on your page. Up to four gameplay videos can follow it, and the screenshots after them, each in the order you drag them into. Your name’s lettering is one of the templates, a typeface from Google Fonts’ library, or a font of your own (WOFF2, WOFF, TTF or OTF, up to 2 MB; only one you’re allowed to use on the web). Player Two keeps the font’s files and serves them itself, and the font goes with the game if it’s taken down. - **Features**: up to six lines of what players get. Each is a card on your page, with a photo of its own if you give it one, or as words in your game’s colour. - **Where it plays**: a link to your own site, or a web build hosted by Player Two. ### Your studio Your studio is made with your first game (or started on its own), and [your studio](https://playertwo.plus/studio) is where you run everything. It has three columns under one bar: - **The bar**: your studio’s name (its own sections, below), one search, and **Money**, **Analytics** (all your games together), Help and Docs. - **On the left**, your games, each with its cover, where it stands (for sale, free, private, its day, raising), and a badge when something waits on you (a stage’s proof, a vote, and its comments new for you). Under the game that is open, its sections, **Updates** with how many comments are new for you (“3 new”) and **Builds** with its live build’s version: **Grow** (Overview, Updates, Analytics, and Its raise for a raise), **Ship** (Builds, Downloads, Playtests), **Sell** (Price and sales, Codes and keys, Add-ons), **Run** (Rooms, Trophies and boards, Saves and Connect) and **Settings** (Its page, Leave the store). The games of the other studios whose team you are on are listed under each studio’s name. Last, **Put a new game up**. - **In the middle**, one section at a time. **Overview** is the game’s home: its key art with where it stands, its name in its lettering, its build (“Build 1.3.2 live”), downloads, rooms and trial in one line, and four buttons (**Post an update**, **Push a build**, **Edit its page**, **Open it**); then four figures (players in it now, with the new players of its last 30 days; sales in the last 7 days against the 7 before, or new players for a free game; wishlists, with this week’s; and the share of its reviews that recommend it), **Next up** (what waits on you: a post going up later, its comments new for you (“3 new comments on “Patch 1.3””, with Read them), a build out to its testers (“Build 1.4.0 is with your 3 testers”, with Make it live), players asking to playtest it, a stage’s proof, a vote, its release day, a download Player Two is looking at, your identity check for a raise) and its last 30 days, day by day, with its price, trial and sale. Every other section has the game folded to one strip at the top, with the same four buttons. - **On the right**, the game right now: sold today (or new players today), its rooms open, its live build’s version and since when, with **Push a build**, and **New comments**: the comments on its updates new for you (below, [Posting updates](https://playertwo.plus/docs#dev-updates)), to **Reply** to or **Take it down**; a comment’s post name opens that post’s comments in Updates, and **Mark all read** marks every post’s read. With none new it says **All caught up**, and shows the latest. - **Your studio’s own sections** (press its name): **Profile** (its name, what you are, a note to players, your sign-off, your links, its logo and GitHub; it shows on every one of your games’ pages), **Identity** (for a raise), **Messages**, **Money** (what your studio is owed from sales, the Claim button and your payout address), **Team**, **Bundles**, **Webhooks**, **Agents**, **Teams you’re on** and **All games** (searched, narrowed by where each stands, and put in order). - **Search** (⌘K on a Mac, Ctrl K elsewhere, or the search in the bar): type a game, a setting or something to do, like “impact sale”, “trial” or “keys”. It lists what to do (“Start a sale on Impact”), each setting of each game with where it lives (“Sell › Price and sales”), your other games and your studio’s sections, and says what is new (“Read 3 new comments on Impact”, a game’s live build’s version, “Make build 1.4.0 of Impact live” while its testers have it); the arrows move, Enter opens it, Escape closes. Something that can’t be done to a game says why, and opens where to change that (a private game can’t be on sale: release it first). - Each place has its own address, to keep or send: `/studio#game-` for a game’s overview, `/studio#game-/price` for one of its sections (`updates`, `analytics`, `raise`, `builds`, `downloads`, `playtests`, `price`, `codes`, `addons`, `rooms`, `trophies`, `connect`, `page`, `leave`), and `/studio#studio/money` for one of your studio’s. On a phone the left column is a menu at the top (your games and the open one’s sections) and the right one comes under the section. ### Playtests and release days A store game can stay private while you test it, or be listed with the day it comes out: choose in the form’s last step (**When it goes live**), or later in your studio’s **Release**, until it is out. Once it is out, it stays out (archiving takes it out of the store). - **Private**: only you, your team, your testers and Player Two’s admins can open its page and play it. It is in no list, search, sitemap or the room, search engines are told not to index its page, and its page draws no share card. - **On a day**, up to 365 days ahead: in the lists with its day, its page anyone’s with Wishlist in place of Play and a countdown (you can turn it off); a paid game takes pre-orders. Only you, your team and your testers play it until then. At its moment it is out by itself, and its wishlisters and your followers are told. - **Playtesters**, in your studio on a game not out yet: invite a player by their handle (a tester at once; up to 1,000 testers a game, 200 invites a day), say yes or no to players who ask from a coming game’s page (each with a few words, ten asks a day at most, and their hours, games, trophies and latest reviews beside it; a no reads to them as asked, and they can’t ask again), or take one off. Testers play it free and in test mode ([`p2.test`](https://playertwo.plus/docs/sdk#test): what they earn is kept apart), with rooms, voice, saves and trophies working, and get it in the Player Two app. Once it is out, they get the builds you send to testers first ([pushing a build](https://playertwo.plus/docs/hosting#push)). - **Anyone with the link**: turn on **Anyone with the link can playtest it** in Playtesters, and copy the link. As with a shared document, whoever opens it sees the game’s page (a private one’s too), and once signed in is one of its testers from then on, without the link: free and as a test, a paid game too, marked **by link** in your list. Signed out, they see the page, and Playtest asks them to sign in first (a playtest needs an account, for its saves, rooms and test mode); the link holds through the sign-in. The game stays out of every list either way. - **The link’s rules**: off until you turn it on, and only for a game not out yet. Its secret comes after a `#` in the address, which browsers never send to a server or to another site, and the page takes it off its address at once; the browser keeps it 30 days, sent only with that game’s own calls. Turn it off and nobody comes in by it, and every tester who came by it is taken off at once; turn it on again and it is the same link. **Reset the link** for a new one: the old one stops at once, everywhere, and the testers who came by it go too. The testers you invited or said yes to always stay, and so does a link tester you invite by handle. A tester you take off while the link is on stays off (the link won’t bring them back), and a player you said no to isn’t let in by it. Twenty wrong links an hour from one address, then every try from it waits for the hour. Keeping it private again from a day, or giving it a day, leaves the link as it is; once the game is out, the link turns off by itself and the testers who came by it go (the ones you invited stay, for testers-first builds). You or a developer on your team can change it, sixty times an hour; your agent too ([below](https://playertwo.plus/docs#agents)). ### Editing a game Every part of a game’s page can change: open it from your studio, or press **Edit page** on the game’s page. Once a raise is open, its goal, smallest stake, backers’ share, length, planned price and stages stay as backers agreed to them. ### Posting updates News, patch notes, a date: an update goes first in the **Updates** of the game’s page. Its team posts it (the studio’s owner, or a member whose role is developer or admin) from the card first in those Updates, which only the team sees, from **Post an update** in the studio, or through an agent. - **Its title**: 2 to 80 characters. **Its words**: up to 10,000 characters, the marks counted; 2 at least, unless it has a picture or a video. - **Formatting**, from the row of marks over the words or typed as Markdown: **bold** (`**bold**`, `⌘B` or `Ctrl+B`), _italic_ (`*italic*` or `_italic_`, `⌘I`), a heading (a line starting with `##`; `#` and `###` make the same one size), a bulleted list (lines starting with `-` or `*`), a numbered list (`1.`), a quote (`>`) and links (`[words](https://...)`, `⌘K`, or an address written out). Enter on a list’s line starts its next item, and on an empty one ends the list. **Preview** shows it as players will see it. Lists don’t nest, and a backslash keeps a mark as itself (`\*`). - **What isn’t formatting**: HTML shows as the characters you typed; a link goes only to an `http` or `https` address (anything else, `javascript:` included, shows as its words), opens in a new tab and is marked nofollow; pictures come only as attachments, never by Markdown (`![...](...)` is a link to it). - **Pictures and a video**: up to 4 pictures, or a video and 3 pictures, chosen with the picture button, dropped onto the composer or pasted. Pictures are PNG, JPEG or WebP up to 12 MB, a video MP4, WebM or MOV up to 80 MB (a video your browser can’t play is refused, since players’ can’t either). Each shows how far its upload has got and can be taken out; the post waits until they are all up. Uploads count toward everyone’s daily limit of 300 files, 40 of them videos. - **How it shows**: in the feed, its first picture (or its video, without sound, playing while the pointer is on it or once pressed; never by itself if a player asks for less motion) stands where other posts show one of the game’s screenshots, with **+2** and so on for the rest; pressed, the whole post opens over the page with its pictures large. Long words are cut at about nine lines in the feed, with **Read more**. A post without pictures shows one of the game’s screenshots, as before. - **Who hears of it**: its backers, the players who have it in their library, who wishlisted it and who follow your studio, once each, in their notices (and by email and push, unless they turned game updates off), as plain words: the first 240 characters, the marks taken out. A private game’s update is for its page alone. Twenty updates a day a player. - **In other languages**: players read it in their own language, translated as reviews and comments are (they can show the original), a paragraph or a list’s item at a time, with its marks kept. A piece whose translation would change where a link goes, and one over 4,000 characters, shows as you wrote it. Your team always sees it as written. - **Posting later**: **Post later**, beside Post it, takes a day and a time in your own time zone (its name and the same moment in UTC show beside it), 5 minutes to 90 days ahead; the button then says **Schedule**. Until its time only your team sees it, first in the Updates after the composer, marked **Scheduled for** its time, with **Edit** (its words, pictures and time), **Post now** and **Delete**; visitors, the store, the feeds and the API’s public reads don’t see it, and nobody is told. At its time it goes up as if posted then (its date is that moment) and its audience is told then, once, as for any post (none for a private game). Player Two’s clock puts it up within a couple of minutes of its time, and any visit to the game’s page or read of the game puts up one whose time has come. Deleting one before its time sends nothing. Up to 20 wait at once a game; each counts toward the twenty a day when it is made. An edit before it is up isn’t marked Edited, and it can be pinned once it is up. - **Editing one**: under each of the game’s posts its team (whoever may post) sees **Edit**, **Pin** and **Delete**; nobody else does. Edit opens the post in the same composer, in place: its title, words and pictures or video, by the same rules and limits as posting, its attachments still only files uploaded for this game. Saving tells nobody again (no notice, email or push); the notices and emails it already sent keep its first words. The post keeps its place and its date, with **Edited** after the date (the time it was edited when you point at it), and its translations are made again from the new words. The posts Player Two writes about the game (a release such as “Out now”, a raise’s news, a stage, a vote) are edited the same way, from the same **Edit**: their title, words and pictures or video, by the same rules. Each keeps its kind (a release still says **Release**), its date and its place, with **Edited** after the date, and the edit is in the audit log with what it said before. They can’t be deleted, and Player Two’s admins who aren’t on the team can’t edit them (nor any of the team’s posts). A picture or video taken out leaves Player Two’s file store a day later, unless another of the game’s posts shows it. - **Pinning one**: **Pin** puts a post first in the game’s Updates, before the newest, with **Pinned** on it for everyone. One post a game is pinned: pinning another unpins the one before, and **Unpin** puts it back in its place by date. Any post can be pinned, Player Two’s own too (an “Out now”, say); pinning tells nobody. - **Deleting one**: **Delete** asks first (“Delete it? Yes, delete / Cancel”). Then the post is gone from the game’s page, the feeds, the studio, the weekly email and the API, and unpinned; it can’t be brought back. The notices it sent are taken out of players’ notices, so an email or push not sent yet never goes; an email already sent stays sent (its button opens the game’s Updates). Its pictures and video leave the file store a day later, unless another of the game’s posts shows them. Only the updates a team wrote can be deleted. Player Two’s admins can delete one too, after a report, say. - **Comments**: players comment on every update once it is up (not on one waiting to go up). Its card says how many (“12 comments”, or Comment); pressed, the whole post opens with its comments under its words, oldest first and the newest last, and a box to write in. Any signed-in player who isn’t banned may comment, 1 to 2,000 characters in plain words, and reply to a comment (the reply says whom it answers); a player blocked by your studio’s owner (or blocking them), or by the one they reply to, can’t. Comments and Discussion answers share one limit: 60 an hour a player. Your team’s comments are marked **Developer**. Your team (and Player Two’s admins) can take any comment down (kept out of sight, in the audit log) and **Lock comments** on a post (the ones there stay; no more); a player can take their own down and report anyone else’s (Spam, Abuse, Off-topic: into Player Two’s moderation, as Discussion’s are). Comments are translated for each reader, as Discussion is. The post’s author (its studio’s owner for Player Two’s own posts) gets one notice a post that counts up (“5 new comments on…”) while it is unread, emailed and pushed when it first comes; a commenter gets a notice for each reply to them. A post deleted takes its comments and those notices with it. A post’s comments are its own: they aren’t topics in the game’s Discussion. From an agent: `list_comments`, `hide_comment` and `lock_comments` (`GET` and `PATCH /api/v1/agent/games//posts//comments`, `DELETE` `.../comments/`), with `mark_comments_read` (`POST /api/v1/agent/games//comments/read`) to mark them read for the key’s player once they have seen them; agents don’t comment, players do. - **In your studio**: each game’s **Grow › Updates** has the composer at the top, then lists its updates (narrowed to All, Scheduled, Yours, Player Two’s or With new comments): every post still up, the ones waiting to go up first (marked **Scheduled for** their time), then the pinned one, then the newest, each with its kind, its day, its title and first line, its first picture or its video’s still, **Edited** and **Pinned**, and how many comments, gold with how many are new for you (“5 comments · 3 new”). Each has what its page’s own row has: **Edit** (the same composer, in place), **Pin** or **Unpin**, **Delete** (asking first), **Post now** for one waiting, and its comments opened under it, to take one down or **Lock comments**. Player Two’s own posts are listed too (“By Player Two”), with Edit and Pin and no Delete. The latest five show, the rest behind **Show all**; one posted from **Post an update** is listed at once. The signed-in list (`GET /api/v1/games//posts`, and the agent’s `list_updates`) says each post’s `comments`, how many are `new` for whoever asks, and whether they are `locked`. - **New comments**: Player Two keeps, for each of a game’s team (its studio’s owner and the members who said yes), what they have read of each post’s comments. A comment is new for you until you read it: opening a post’s comments in your studio’s Updates, from its live column (a comment’s post name), or on the game’s page signed in, marks that post’s read; answering one from the live column marks it and the ones before it on its post; **Mark all read** marks every post’s. Your own comments and replies are never new for you; a comment taken down isn’t new for anyone; a teammate’s comment is new for you as a player’s is, and what you read stays yours (your teammates’ stay new for them). A post you never opened has all its comments new; a post deleted, or waiting to go up, has none. Player Two’s admins who aren’t on the team have nothing kept. Signed in: `POST /api/v1/games//comments/read` with `{ post }` (its id) or nothing for every post, and `until` (milliseconds) to read only up to a moment; a post’s comments (`GET .../posts//comments`) say `team`, how many are `new` and which (each comment’s `new`), without marking them. Reads: 300 an hour a player. - Every edit, pin and delete is recorded in Player Two’s audit log, with who did it (and which agent key), and what the post said before. Edits, pins and deletes: 120 an hour a player. - Updates posted before 2026-11-07, and the ones Player Two writes itself (a release, a stage paid, a vote), are plain words until they are edited; an edit saves them as Markdown, as every post written since. - **From an agent**: `upload_media` with `game` set to the game’s slug for each picture or video (an update takes only files uploaded for that game, from its page, its studio or that way: any other address is refused), then `post_update` with the slug, `title`, `body` in Markdown and `media`, the addresses in order. `list_updates` lists the game’s posts with their ids and kinds (`devlog`: the team’s own), `editable` (every post) and `deletable` (a `devlog`); `edit_update` with an `id` changes the `title`, `body` or `media` sent, of any of the game’s posts, Player Two’s own too (its kind kept) (what is left out stays; `media` is the whole new list); `pin_update` pins or unpins one (`pinned: false`); `delete_update` deletes one. `post_update` with `at` (milliseconds, or an ISO time with its zone such as `2026-11-08T18:00:00Z`) posts it later; `list_updates` gives its `scheduled` time; `edit_update` with `at` moves it, with `now: true` posts it now. The agent API: `POST /api/v1/agent/media` with `game`, then `POST /api/v1/agent/games//posts`; `GET` there lists them; `PATCH /api/v1/agent/games//posts/` with `title`, `body`, `media` or `pinned` changes one (and `at` or `now` one waiting to go up), and `DELETE` there deletes it. Signed in, the same calls are at `/api/v1/games//posts` and `/posts/`. A Player Two post is 409 to delete; an address not uploaded for the game is 422; another game’s post, or one deleted, is 404. ### Identity checks Only developers who raise money are checked. Before a raise can take any, and before any of its stages is paid out, the developer photographs an official ID and takes a selfie with Didit, our identity partner, from their studio. Didit checks the document is real and the selfie matches it, in seconds; Player Two keeps only the name on the ID and the country that issued it. A check Didit can’t decide is reviewed by a person. Developers who only sell their games or give them away are never asked. ### Raising - Set the goal, the smallest stake, how many days it runs, the backers’ share, the planned price, and the stages: what each will show, its share of the money, and when it’s due, counted from the day the raise is funded. - Post proof for each stage from your studio. Without an objection in 3 days, the stage is paid out, less 5%, once an admin has checked the payout. - Once the raise is funded, you can put the game on sale: set its price in your studio. Its backers share every sale from then on. - **Post an update** from your studio or the game’s page to tell backers how it’s going. It shows on the game’s page and reaches backers as a notice ([updates](https://playertwo.plus/docs#dev-updates)). ### Builds and downloads - **A web build**: a .zip of your export’s folder with index.html at the top (up to 100 MB, and up to 300 MB and 3,000 files once unpacked), or one .html file; or, up to 5 GB, the folder pushed with one command. It plays at its own address, `.playertwo.cc`, with no code changes, and can’t reach anything else on the platform. If the game costs money, only its owners can open it. [Hosting](https://playertwo.plus/docs/hosting) says what hosting here gives, next to playing from your own site, and how to push a build. - **Trophies, leaderboards, stats, cloud saves and presence**: for a game Player Two hosts (a web build or downloads), through one script tag. See [the game SDK](https://playertwo.plus/docs/sdk). A game played on its own site gets them too, in Player Two’s player, with a signed identity for its own servers: see [Player Two Connect](https://playertwo.plus/docs/sdk/connect). - **Downloads**: upload Windows, Mac and Linux builds, as a demo and as a full version: a .zip or .tar.gz of the game’s folder (the Player Two app unpacks it and starts the file you name as its launch file), or an installer (.exe, .msi, .dmg, .pkg) that players run themselves. Every new upload is reviewed by Player Two before players can get it, and a new version reaches players’ apps as an update. ### Through your agent Your own AI agent can submit your games for you, for the store or as a raise, upload their pictures, trailers and web builds, and keep their pages up to date. Make it a key in your studio, under **Agents**; the key is shown once. - **MCP**, for Claude, Cursor and other agents that use tools: the server is `https://playertwo.lol/api/mcp`, with the header `Authorization: Bearer` and your key, or with no key, signing in when the app asks ([Signing in an app](https://playertwo.plus/docs#agents-oauth)); its public tools need neither ([For any agent](https://playertwo.plus/docs#agents-public)). Its tools are get_submission_guide, check_game, upload_media, upload_web_build, submit_game, update_game, get_game, my_studio, and for a hosted game’s trophies and leaderboards list_trophies, define_trophy, list_boards and define_board, for a web game’s builds list_builds, make_build_live (roll back, or forward) and name_build (its version: [the rule](https://playertwo.plus/docs/hosting#push)), for what it sells list_products and define_product, playtest_link for a game’s playtest link (on, off or reset, as its studio’s [Playtesters](https://playertwo.plus/docs#dev-playtests) has it), post_update for an update on a game’s page ([its rules](https://playertwo.plus/docs#dev-updates); its pictures and video from upload_media with `game`) and list_updates, edit_update, pin_update and delete_update after, list_comments, hide_comment, lock_comments and mark_comments_read for their comments (which are new for you, and marking them read), and get_rooms: a game’s multiplayer rooms open now and how its players’ connections were over the last minute ([the figures](https://playertwo.plus/docs/sdk#rooms-stats)), read-only. - **The agent API**, for scripts: `GET /api/v1/agent/guide` explains every field and the rules; `POST /api/v1/agent/games` submits a game (with `publish: false` it waits in your studio as a draft; with `release` it is kept private for your testers, or listed with the day it comes out); `PUT /api/v1/agent/games//release` and `/testers` change that and look after its playtesters, and `PUT /api/v1/agent/games//testers/link` with `{ "on": true }`, `false` or `{ "reset": true }` its playtest link; `PATCH /api/v1/agent/games/` changes one; `POST /api/v1/agent/media` and `/api/v1/agent/builds` take files by https address; `GET /api/v1/agent/games//web/builds` lists a web game’s builds, and `POST` there with a build’s `key` (and a `note`, why) rolls back to it; `GET /api/v1/agent/games//rooms` reads its rooms now and their connections, as get_rooms does; `POST /api/v1/agent/games//posts` posts an update on its page ([its rules](https://playertwo.plus/docs#dev-updates)), as post_update does, `GET` there lists its posts, and `PATCH` and `DELETE /api/v1/agent/games//posts/` edit, pin or delete one. - **The API spec**: [`/openapi.json`](https://playertwo.plus/openapi.json), OpenAPI 3.1, has every call of the agent API, the game SDK and the public reads, with the draft’s every field, its type and its limits. Tools that read OpenAPI can make a client from it. - **How to play, for agents**: in the form’s Where it plays step (or `agentNotes` in the draft), your game’s controls and goals. Players never see them; every game page has a hidden section for agents that visit it (a player’s assistant, a playtester) with them, where to play, whether a guest can, and how to report a bug. - **Reading Player Two as an agent**, no key needed: [`/llms.txt`](https://playertwo.plus/llms.txt) says what Player Two is, lists every game with its page, and leads to the rest; [`/llms-full.txt`](https://playertwo.plus/llms-full.txt) is all of it in one file. Every page of the help, the docs and the rules, and every game’s page, is also Markdown at its address with `.md` after it ([`/help.md`](https://playertwo.plus/help.md), [`/docs/sdk.md`](https://playertwo.plus/docs/sdk.md), `/game/.md`), made from the page itself; each page names its Markdown with `` and carries schema.org JSON-LD (the game, its trailers and price; the help’s questions; the SDK). [`/robots.txt`](https://playertwo.plus/robots.txt) lets every crawler and AI agent read the public pages, scripts included (only the signed-in pages and the API but for its reads of games are out), and says `Content-Signal: search=yes, ai-input=yes, ai-train=yes`; [`/sitemap.xml`](https://playertwo.plus/sitemap.xml) lists every public page with when it last changed. - A key can only submit and edit your games. It can’t pay or claim money, change your account or payout address, or take a game down. - A raise your agent submits is live at once and takes no money until **you** check your identity in your studio. An agent can’t do that step. - Revoke a key in your studio at any time; it stops working at once. - **Found by name**: the server is `lol.playertwo/mcp` in the [official MCP Registry](https://registry.modelcontextprotocol.io/v0.1/servers?search=lol.playertwo), and describes itself at [`/.well-known/mcp.json`](https://playertwo.plus/.well-known/mcp.json) and `/api/mcp/server-card` (its card, listed in [`/.well-known/ai-catalog.json`](https://playertwo.plus/.well-known/ai-catalog.json)). The SDK, the starter, examples and these docs in Markdown are open at [github.com/TheDudeCommits/playertwo-sdk](https://github.com/TheDudeCommits/playertwo-sdk); on npm, [`playertwo-sdk`](https://www.npmjs.com/package/playertwo-sdk) and [`create-playertwo-game`](https://www.npmjs.com/package/create-playertwo-game). ### For any agent, no key Any AI agent can ask Player Two about its games and its docs through the same MCP server, `https://playertwo.lol/api/mcp`, with no key and no sign-in: a player’s assistant answering what to play, a developer’s deciding where to put their game. These tools only read, and only what the site shows everyone: never an unlisted, archived, private or taken-down game. - **search_games**: games by words (every word must be in a game’s name, tagline, description, studio, genres, modes or features), `genre`, `platform`, `price` (`free`, `paid` or `any`), `multiplayer` (by a game’s genres and modes, or its own servers), `kind` (`store` or `raise`), in an `order`, `limit` 1 to 50 (20 if left out) and `offset` for the next page. Each game with its slug, name, tagline, studio, genres, modes, platforms, price, whether it is free, a sale, a free trial, whether it plays with others, where it plays, its players and reviews, and its page. - **list_games**: every game in the store, the same filters, in Player Two’s order (`featured`: featured first, then the most played), `popular`, `new` or `playing` (most playing now). - **get_game_facts**: one game by its slug, with what its page tells agents (what it is, its price, where to play it and whether a guest can, how to play it, whether it is multiplayer, the Player Two services it has, how to report a bug) and its five latest updates. - **get_sdk_docs**: a part of these docs in Markdown, as the pages say it today: `topic` as a page and part (`/docs/sdk#rooms`), a part’s id (`rooms-sync`) or a few words (`leaderboards`, `push`, `fees`). Left out, every topic there is. A part is cut at 40,000 characters, with its link for the rest. - **where_to_publish**: what Player Two offers a game’s developer, each fact with its link: what it keeps of sales (10%), of in-game purchases (20%) and of each stage of a raise (5%), the ways in, and everything a game gets. - **get_submission_guide** and **check_game**: the fields of a game’s page and a draft checked, as for a key. In Claude Code: `claude mcp add --transport http player-two https://playertwo.lol/api/mcp`. `tools/list` names every tool to anyone, the studio’s too, so a client knows what signing in adds. ### Signing in an app An MCP client that signs in (Claude’s and ChatGPT’s connectors, Claude Code, Cursor, VS Code) needs no key: add `https://playertwo.lol/api/mcp`, and the first time it calls one of your studio’s tools, Player Two answers that it needs you, the app sends you to `playertwo.lol/oauth/authorize`, you sign in, see what it will and won’t be able to do, and allow it or not. Allowed, it can do what an agent key does, and no more. It is listed in your studio, under **Agents**, as “Connected by signing in” with where it is from, and Revoke disconnects it at once. A key keeps working beside it. - **How it is found**: a studio tool called without a working key or sign-in is answered `401` with `WWW-Authenticate: Bearer … resource_metadata="https://playertwo.lol/.well-known/oauth-protected-resource/api/mcp", scope="studio"` (RFC 9728); that names Player Two as the authorization server, whose endpoints are at [`/.well-known/oauth-authorization-server`](https://playertwo.plus/.well-known/oauth-authorization-server) (RFC 8414). - **Apps**: an app registers itself at `POST /api/oauth/register` (RFC 7591, with `redirect_uris` and a `client_name`), or uses a Client ID Metadata Document: its `client_id` is the https address of a JSON document naming that same `client_id` and its `redirect_uris`, read when it asks. Public clients only (`token_endpoint_auth_method` `none`): no secret. A redirect address is https, http only to this computer (`127.0.0.1`, `[::1]` or `localhost`, on any port), or an app’s own scheme, never with a `#`; one to ten of them. - **The code**: `response_type=code` with PKCE (`code_challenge_method=S256`) always, the `redirect_uri` exactly as registered, `scope` `studio` (the only one), `resource` if sent `https://playertwo.lol/api/mcp`. The app gets back its `state`, the `code` and `iss` (RFC 9207); a code works once, for ten minutes, and one shown twice disconnects the app it was made for. - **Tokens** (`POST /api/oauth/token`, a form or JSON): an access token (`p2_oauth_…`, sent as `Authorization: Bearer`) for an hour, and a refresh token for 60 days, made new at every refresh (the old one stops). `POST /api/oauth/revoke` with a token disconnects its app (RFC 7009). An access token works on the agent API too, as a key does. - **Its errors**, as OAuth says them (`error` and `error_description`): `invalid_request` (no PKCE, a missing value), `unsupported_response_type`, `invalid_scope`, `invalid_target` (another resource), `access_denied` (you didn’t allow it), `invalid_grant` (a code used, run out, for another app or redirect, a verifier that doesn’t match; a refresh token used, revoked or run out), `unsupported_grant_type`, `invalid_redirect_uri` and `invalid_client_metadata` (registering), `slow_down` (`429`, too many). An app or a redirect address that doesn’t check out is shown on the page and sent nowhere. - **Its limits**: up to 20 apps connected at once (a 21st disconnects the oldest); registering 30 an hour, tokens 300 an hour and revoking 120 an hour from one address; answering an app 60 times an hour. ### MCP limits and errors _What the MCP server answers_ | Case | Answer | | --- | --- | | A tool's answer | HTTP 200, the JSON in `content` as text and in `structuredContent`; a refusal has `isError: true` and `{ error, status? }` (arguments of the wrong shape add `issues`) | | A studio tool with no key or sign-in, or one that doesn't work | HTTP 401, `WWW-Authenticate` with where to sign in, and a JSON-RPC error `-32001` saying so | | A tool that isn't there | JSON-RPC error `-32602` | | A method that isn't there | `-32601` (HTTP 404 in the stateless protocol) | | Not JSON; no message, or more than 20 at once | HTTP 400, `-32700` or `-32600` | | A protocol version it doesn't speak (stateless) | HTTP 400, `-32022`, with the versions it speaks | | Headers that don't say what the body says (stateless) | HTTP 400, `-32020` | | GET or DELETE | HTTP 405: it answers POST only, with no stream and no sessions | | A notification | HTTP 202, nothing to say | - **Protocol versions**: the stateless `2026-07-28` (each request’s version in its `_meta`, with `MCP-Protocol-Version`, `Mcp-Method` and, for a tool, `Mcp-Name` saying the same; `server/discover`), and `2025-11-25`, `2025-06-18`, `2025-03-26` and `2024-11-05` through `initialize`. - **The public tools**: 600 calls an hour from one address, together; past that, a refusal with status 429 until the hour turns. - **A studio’s tools**, each player: post_update 20 a day; edit_update, pin_update and delete_update 120 an hour; make_build_live and name_build 120 an hour; define_product 120 an hour; hide_comment and lock_comments 300 an hour; mark_comments_read 300 an hour. Past one, a refusal with status 429. - **What each tool does**, in its annotations: read-only, or changing things (destructive: overwrites or takes away; idempotent: the same call again changes nothing more), and whether it reaches past your studio (open world: a public page, players told, a file fetched from an address). ### Taking a game down You can take your game down from your studio while no backer’s money is riding on it. If it has backers’ money in it, ask us, and we settle with the backers first. ## Safety and moderation What may be listed, and what isn’t allowed, is set out in our [content policy](https://playertwo.plus/policy). In short: - Anyone can report a game with **Report this game** at the foot of its page, signed in or not, or by email to [hello@playertwo.lol](mailto:hello@playertwo.lol?subject=A%20report). - A person at Player Two looks at every report. - Player Two can warn a developer, take a game down, refund what’s left of a raise, and ban an account. A banned account can no longer buy, back, vote, post or submit. - Decisions can be appealed by email. ## Notices and your account Player Two tells you when something happens to a game you back, a game on your wishlist, or your money. Notices show in your profile, and are sent by email to the address you signed in with, if you signed in with one. You choose which emails you get, kind by kind: _Notification settings_ | Kind | What it covers | | --- | --- | | Game updates | Posts from the developers of games you back | | Raises you back | Funded, proof posted, votes, stage payouts, refunds | | Your wishlist | A game on it comes out, goes on sale or gets a demo (once each) | | Money | Payouts to you. Always on | From your profile you can also change your name, download all your data as a file, and delete your account. Your balance stays yours when you go: it lives with Privy. Claim anything you’re owed first. ## Journeys Four people, from the first step to the last. ### A player who browses and plays 1. **Arrives in the room** Turns the ring, switches between the store and the launchpad, searches by genre. 2. **Opens a game** Watches the trailer, reads what it is and who made it. 3. **Signs in** With email, Google, Discord or X. Their balance is set up on the spot. 4. **Gets the game** Adds a free game to their library, or presses Buy, tops up if short, and approves the payment. 5. **Plays** A web game right there (a paid one from a play link that lasts 6 hours), or a download for Windows, Mac or Linux where there is one. 6. **Keeps a collection** Wishlists games for later, hides the ones they’re done with, and watches their player pass climb with every game. ### A backer who follows a raise to the end 1. **Finds a raise** On the Launchpad tab. Reads the plan, the stages and the terms on the game’s page. 2. **Backs it** Chooses a stake in Your cut, reviews it, ticks the box, confirms and approves. Gets the next backer number, and a pass if the stake is at least the planned price. 3. **Waits for the goal** The raise fills and is funded, and the first stage starts. Or it closes short, and the backer is refunded in full. 4. **Checks each stage** Gets a notice when proof is posted, opens the link, and has 3 days to object if it doesn’t match. 5. **Votes when asked** On an objection or a missed deadline: 5 days, counted by dollars. If too few backers vote, Player Two decides. 6. **Sees it through** Stage after stage is paid out, until the game is made. Or backers vote to stop, and what’s left comes back. 7. **Launch day** With a pass, the game is already theirs. From now on, every sale adds their share to what they’re owed. 8. **Claims** From $1, at the foot of their profile. An admin checks it, and it lands in their balance. ### A developer who raises, proves and gets paid 1. **Submits the game** Chooses to raise. Sets the goal, the smallest stake, the days, the backers’ share, the planned price and the stages. The page goes live. 2. **Verifies their identity** A photo of an official ID and a selfie, with Didit. Then the raise can take money. 3. **Brings backers in** Shares the page and posts updates. The raise fills and is funded; stage 1 is under way. 4. **Posts proof** From their studio, a link backers can check. After 3 days with no objection, the stage is paid out, less 5%, once an admin has approved the payout. 5. **Answers to backers** An objection, or a missed deadline, goes to a vote. Posting proof during a missed-deadline vote ends it. 6. **Launches** Sets a price in their studio once the raise is funded, and uploads builds; downloads are reviewed before players get them. 7. **Gets paid** Stage payouts as each stage clears, and the studio’s share of every sale, claimed from $1 to the payout address. ### How Player Two’s admins review and moderate 1. **Review identity checks** The ones Didit can’t decide, before a raise can take money. 2. **Review every payout** Stage payouts, refunds and claims wait for an admin, who checks the amount, where it’s going and why, then sends it or cancels it. A payout over the server’s cap is signed with the admin’s own key as well. A failed payout can be tried again. 3. **Review new downloads** Every new upload is checked before players can get it. 4. **Decide stages too few voted on** Pay the stage, give more time, or refund what’s left. 5. **Work through reports** Each report arrives with its reason, any note and when it came in. The admin checks the game, then dismisses the report, warns the developer, or takes the game down. 6. **Take a review's picture or clip down** On the game’s page, under a review’s pictures and clip, an admin sees **Take the clip down** (or the picture, or **Take a picture or the clip down** to choose one), may write why (it isn’t needed) and presses **Take it down**. The review keeps its words and stars; the file leaves Player Two’s public store for its private one, so its address stops working (and it can be put back by hand); it leaves the review, its writer’s box and **Shared by players**; its writer is told in their notices, with the reason if one was given; and the log keeps who, any reason and the file’s address. Sending the review again with it never brings it back. 60 an hour an admin; signed in, `DELETE /api/v1/reviews//media` with `{ url, reason }`. 7. **Settle with backers** When a game with backers’ money in it comes down, refund what hasn’t been paid out. 8. **Ban when needed** An account that breaks the rules badly, or again and again, can be banned. 9. **Leave a record** Every admin action is written to a log. ## Rules at a glance _The numbers_ | Rule | Value | | --- | --- | | Player Two’s share of a sale | 10% | | Player Two’s fee on a raise, per stage paid out | 5% | | Backers’ share of sales, set per raise | 40% to 85% | | How long a raise runs | 3 to 120 days | | Stages in a raise | 1 to 8 | | Time to object to proof | 3 days | | How long a vote runs | 5 days | | Money that must vote for a vote to count | 20% | | More time a missed deadline can get | 30 days | | Smallest claim | $1 | | Time to approve a payment | 30 minutes | | A paid web game’s play link lasts | 6 hours | | A download link lasts | 10 minutes | | Payouts held after a payout address changes | 2 days | | A game’s price | Up to $500 | | A raise’s goal | Up to $10 million | ## Words we use - **Backer**: A player who put money into a raise. - **Stake**: The amount a backer put in at one time. Each stake has its own backer number. - **Raise**: A developer’s ask for money to make a game, with a goal, a closing date and stages. - **Stage**: A part of the work, with proof to show, a share of the money and a deadline. - **Proof**: A link backers can check that shows a stage is done. - **Objection**: A backer saying a stage’s proof doesn’t match what was promised. It opens a vote. - **Quorum**: The part of the money backed that has to vote for a vote to count: 20%. - **Backers’ share**: The percentage of every sale that goes to a game’s backers, split by stake. - **Planned price**: What a game will cost at launch. A stake of at least this brings a backer pass. - **Backer pass**: A collectible card for a stake of at least the planned price. It brings the game. - **Player pass**: Every player’s own card, whose rarity climbs with their points. - **Claim**: Asking for what you’re owed from sales to be paid out to you. - **Payout**: Money sent from Player Two’s treasury: a stage payment, a refund or a claim. - **Payout address**: Where a studio’s money is sent. The developer’s own Player Two balance unless they change it. - **Treasury**: Player Two’s own account, which holds raised money and sends every payout. - **Studio**: A developer’s name and profile on Player Two, made with their first game. - **USDC**: A digital dollar: one USDC is meant to be worth one US dollar. - **Base**: The network USDC moves on here. - **Privy**: The company that runs sign-in and players’ balances for Player Two. --- --- title: "Game SDK for web games · Player Two" url: https://playertwo.plus/docs/sdk description: "The Player Two game SDK (npm i playertwo-sdk, or one script tag): multiplayer rooms with no server of your own, trophies, leaderboards, cloud saves, stats, presence and in-game purchases for web games on Player Two (playertwo.lol), every call with an example." source: Player Two (https://playertwo.plus), made from the page itself --- Documentation · Game SDK # Player Two's game SDK The Player Two SDK is a small JavaScript library for web games on Player Two (playertwo.lol). It gives a game multiplayer rooms with no server of your own, trophies, XP and levels, stats, leaderboards, cloud saves, presence and in-game purchases, all through each player’s one Player Two account. Player Two hosts the game at its own address, and AI agents can publish and update it through the MCP server. Install it with `npm i playertwo-sdk`, or add one script tag (below). Back to [the documentation](https://playertwo.plus/docs), or see [how Player Two compares](https://playertwo.plus/developers/compare) with other places to publish a web game. 1. **Your game asks** It calls the SDK: `p2.trophies.unlock("first-win")`. It never holds the player’s sign-in. 2. **Player Two's host carries it** The player's page around the game, or the Player Two app, sends it with the play session only it knows. 3. **The player sees it** A trophy's toast over the game, their level, their friends seeing what they play. ## Quick start Put the script on your game’s page and call `PlayerTwo.init()` once, early. It answers with who is playing and every call below. index.html ``` ``` ### Where it works The services are for games Player Two hosts: a web build kept here (one .html, or a .zip of your export), or downloads players get from Player Two and play in its app. A game played on its own site gets them in Player Two’s player once its developer turns on [Player Two Connect](https://playertwo.plus/docs/sdk/connect), with a signed identity its own servers can trust. - **On Player Two** (`PlayerTwo.env` is `"p2"`): your game runs in Player Two’s player, at its own address on the games host ([hosting](https://playertwo.plus/docs/hosting)), where Player Two puts this script in its page for you. The SDK says hello to the page around it, takes a private channel from it, and every call goes over that channel. In the Player Two app, the same calls go to the app’s local bridge. - **On localhost** (`"local"`): a stand-in answers every call from memory, so you can build without Player Two. - **Anywhere else** (`"disabled"`): `init()` rejects after 1.5 seconds with code `not-on-player-two`, and your game plays on without services. Or, with `{ required: true }`, the page says “Play this on Player Two” with a link, and stops. ### Building without Player Two On localhost the stand-in knows no trophies unless you tell it. Give it yours, and it refuses a key that isn’t among them, as Player Two would: ``` const p2 = await PlayerTwo.init({ local: { trophies: [{ key: "first-win", name: "First win", tier: 1 }] }, }); // on localhost: answered from memory; on Player Two: the real thing // local: true (or { force: true, trophies }) uses the stand-in wherever the game runs ``` ## Every call Every call returns a promise. A refusal rejects with an Error that has a code and a status. ### init ``` const p2 = await PlayerTwo.init({ required: false, timeout: 1500 }); p2.player // { id, handle, name, picture, creature, level } id: theirs for your game only p2.game // { slug, title } p2.test // true while its developer (or an admin) plays: see Test mode p2.demo // true in a demo: demos earn no trophies p2.params // the invite params the player came with, or null p2.env // the same as PlayerTwo.env: "p2" | "local" | "disabled" p2.call("trophies.list") // any call by its name: the helpers here are these PlayerTwo.ready // a promise of the same p2, once init has run (for code that starts before or apart from it) PlayerTwo.v // 1: this script's version ``` The player’s `id` is the same every time for your game, and different in every other game, so games can’t line up their players with each other’s. Their handle and name are public. Player Two never tells a game a player’s email or anything about their balance. ### me ``` const { player, game, session, test, demo, from } = await p2.me(); // from: "web" (Player Two's player) or "desktop" (the app) ``` ### Trophies ``` const { trophies, xp, level } = await p2.trophies.list(); // each: { key, name, how, tier, xp, hidden, art, earned: { at, n } | null, share, rarity } // a hidden one the player hasn't earned: name, how and art are empty const won = await p2.trophies.unlock("flawless-duel"); // { trophy, new, n, xp: { gained, total }, level, complete? } // new: false if they had it; n: which player to earn it they are (No. 1 is the first) // complete: the completion trophy, earned with this one // xp.pending: XP that waits until your game counts toward levels ``` The player’s host shows the trophy arriving over your game; your game doesn’t have to. Define trophies in your studio (each game’s card, Trophies) or through your agent: a key your code uses, a name, how to earn it, a tier, hidden or not, its sign, optionally a stat and target, optionally “only my server can give it”. ### Add-ons ``` const list = await p2.addons.list(); // each add-on on sale: { key, name, about, price, owned } if (await p2.addons.has("night-map")) unlockNightMap(); // true once the player has bought it; one taken off sale stays theirs ``` Add-ons are more of your game sold on their own (a map, a chapter, a skin pack) to players who have it. Make them in your studio (each game’s card, Add-ons): a key your code asks for, a name, a line about it, a price. Players buy them on your game’s page; ask again when your game opens, or when the player comes back to it. Building without Player Two, the stand-in takes `local: { addons: [{ key, name, owned }] }`. Where it matters that the player can’t fake it, ask from your server (`GET /api/v1/sdk/server/addons`, below). On a computer, the Player Two app answers these calls once it is updated. ### In-game purchases ``` // products you made in your studio const sold = await p2.store.list(); // [{ key, kind, name, about, price, qty?, contents?, owned, count? }] const mine = await p2.store.owned(); // [{ key, kind, count?, test? }] const r = await p2.store.buy("blaze-ball"); if (r.ok) equip(r.product.key); // r.receipt: signed, for your server // or your own shop at your own prices: a charge your server made const paid = await p2.store.pay(chargeId); if (paid.ok) show(paid.charge.ref); // your server gives it (paid.receipt, or its webhook) else if (paid.reason === "cancelled") {} // they said not now ``` Sell inside your game through Player Two’s payments, two ways: **products** you make in your studio (`p2.store.list`, `p2.store.owned`, `p2.store.buy`), or **charges** your own server makes at your own prices, for what your game keeps itself (`p2.store.pay`). Either way Player Two’s own card takes the player’s yes and the money, and you get 80%. How each works, every limit and every refusal: [In-game purchases](https://playertwo.plus/docs/sdk#purchases). ### Stats ``` await p2.stats.add("duels-won"); // by 1 await p2.stats.add("gold", -50); // or any whole number await p2.stats.set("highest-floor", 12); const { stats } = await p2.stats.get(); // { "duels-won": 3, ... } const floor = await p2.stats.get("highest-floor"); // set and add answer { stats, unlocked }: a trophy waiting on a stat is earned // by itself when the stat reaches its target, and comes back in unlocked ``` ### Leaderboards ``` const s = await p2.scores.submit("time-attack", 83400); // { value, best, new, previous, rank, total }: only a player's best is kept await p2.scores.list("time-attack"); // the top 10 await p2.scores.list("time-attack", { around: "me" }); // around the player await p2.scores.list("time-attack", { friends: true, limit: 25 }); // { board, rows: [{ rank, value, at, player: { id, handle, name, picture }, me? }], total, me } ``` Define boards in your studio: higher or lower is better, a number, a time in milliseconds or points, the lowest and highest score it takes, how many seconds a player waits between scores, and whether only your server sends its scores (trusted). A player who keeps off leaderboards shows as “A player”. ### Saves ``` const { slots } = await p2.saves.list(); // [{ slot, size, version, at }] const save = await p2.saves.get("slot-1"); // .json(), .text(), .bytes(), .version await p2.saves.put("slot-1", { level: 3 }, { version: save.version }); await p2.saves.delete("slot-1"); // put takes text, an object (kept as JSON) or bytes, up to 1 MB // { version }: refused (409, e.version is the one there now) if another device saved since ``` A slot that holds a save is only written over once your game has read it in this session (`get`, or `list`), so a game starting blank on a new device can’t wipe a player’s progress. A new slot needs no read. ### Presence ``` p2.presence.set("Chapter 3, The Docks"); // up to 80 characters p2.presence.set(null); // just the game's name p2.presence.set("In a party", { join: { room: "482913" } }); // friends get Join ``` The player’s friends see that they are playing your game, and this line, unless the player keeps their activity private. Time played counts by itself while the game is open. With `join`, they also get Join beside it, which opens your game with those params ([invites](https://playertwo.plus/docs/sdk/connect#invites)). ### Identity and invites ``` const token = await p2.identity(); // who is playing, signed for your own servers (ten minutes) const link = await p2.invite({ room: "482913" }); // playertwo.lol/game/?join=... p2.params // the params the player came with, or null ``` An identity is a JWT signed with Player Two’s key, for your game only, which your servers check before they trust who it names; an invite is a link to your game on Player Two that starts it with the params. Both, and how to carry your own accounts and multiplayer over: [Player Two Connect](https://playertwo.plus/docs/sdk/connect). The player’s critter for your character (`p2.avatar()`), and doors to other games with their party (`p2.travel`): [Travel and avatars](https://playertwo.plus/docs/sdk/travel). ### Loading and gameplay ``` p2.loading.done(); // the game is ready to play p2.gameplay.start(); // a round, a level, a run begins p2.gameplay.stop(); // ... and ends, or the game pauses ``` Player Two’s player and its app use these for what they show over the game. Nothing is kept of them yet. ### The overlay ``` const off = p2.on("overlay", (open) => (open ? pause() : resume())); // on the web: Player Two's bar over the game; in the app: its window over the game ``` ### On air in the Plaza ``` p2.onAir // true while a small live picture of the game goes to the Plaza's screens ``` While an online game is played in Player Two’s player on a computer, its screen in [the Plaza](https://playertwo.plus/plaza) can show it live: your game’s biggest canvas, scaled down to about 480 lines at up to 15 frames a second, so passers-by see it being played before they walk in. Player Two does it, with nothing to add to your game: nothing of the game is read (the browser encodes what the canvas already shows), nothing is recorded, it starts a few seconds into play and pauses while the tab is hidden. Never on a phone, in a test, or for a player who keeps their activity private or turned it off; the player’s On air mark stops it. `p2.onAir` says whether this play may go on air, should your game want to say so itself. ### When a call is refused _Codes a refusal carries_ | Code | Status | What it means | | --- | --- | --- | | `not-on-player-two` | | init only: no Player Two around the game. | | `signed-out` | 401 | The player isn't signed in (a demo played signed out): play on without services. | | `too-soon` | 409 | Nothing is earned in a session's first ten seconds. | | `unknown-trophy` | 404 | No trophy by that key, or it is retired. | | `server-only` | 403 | Only your server gives this trophy. | | `demo` | 403 | Trophies come with the full game. | | `revoked` | 403 | Player Two took this trophy back from this player. | | `read-first` | 409 | Read the slot before saving over it. | | `version` | 409 | Another device saved in between; e.version is the one there now. | | `full` | 409 | Eight save slots (or 200 stats) already. | | `too-big` | 413 | A save is up to 1 MB. | | `out-of-range` | 422 | A score outside the board's bounds. | | `cooldown` | 429 | Too soon after this player's last score on this board. | | `trusted` | 403 | Only your server sends this board's scores. | | `not-connected` | 409 | init only: a game on its own site whose developer hasn't turned on Connect. | | `trial-over` | 402 | A free trial's time is used up: Player Two's player closes the game with Buy, and what was saved carries on if they buy it. | | `trial-paused` | 409 | A free trial's time isn't being counted (the player's page stopped checking in): Player Two's player stops the game. Play on without services. | ## The rules ### Trophies - 1 to 100 a game. Keys are lowercase letters, numbers and dashes, up to 40; names up to 40 characters; how to earn one up to 120. - Tiers 1 to 4 are worth 10, 25, 50 and 100 XP, and a game’s add up to 1,000 XP at most. Player Two adds the completion trophy (key `complete`, 200 XP) once a game has one: it is earned by holding every other trophy that isn’t retired. - An unlock needs a live session at least ten seconds old, and a session earns twenty a minute at most. Demos earn none. - Every trophy is a relic, a small sculpture of cubes built from its sign: up to 16 by 16 cells you draw in the studio (or pick), sent by an agent as rows of `#` and `.` (`..#../.###./#####`). Without one, Player Two picks one by its key; the completion trophy is a crown until you give it your game’s own sign. Its cubes are cut from the finish of how rare it turns out to be, from titanium for Common to pearl mended with gold for Divine. - Once a player has earned a trophy, its key, tier, stat, target and server-only setting stay; its name, how to earn it, sign and whether it is hidden can change. It can be retired (nobody earns it from then on; those who have it keep it), never deleted. - The first player to earn a trophy is No. 1, and every unlock keeps its number. ### XP and levels A player’s level comes from the XP of every trophy they hold, from every game that counts, and from Player Two’s own trophies. XP from level n to n+1 is 100 × n/10 rounded up: 100 a level up to 11, 200 a level up to 21, and so on. A game’s trophies count toward levels once it is listed and 20 players have played it ten minutes or more each, or once Player Two says so. Until then they are earned and shown, and their XP waits; it arrives when the game starts counting. Player Two can take a game’s trophies out of levels (an XP farm, say), and take back what a cheating player earned. ### Rarity Each trophy’s share of the game’s players (everyone who has played it, not counting tests) is worked out daily, in hundredths of a percent (`share: 425` is 4.25%), and named from Player Two’s ladder: Common at 50% and over, Rare 20%, Epic 10%, Legendary 5%, Mythic 1%, and Divine under 1%. Until a game has 10 players, every trophy is New. ### Limits _What a game, a session and a player may do_ | What | Limit | | --- | --- | | Trophies | 100 a game, 1,000 XP of tiers 1 to 4 | | Unlocks | 20 a minute per session | | Stats | 200 per player per game, 20 in one call, 120 calls a minute; whole numbers | | Leaderboards | 20 a game; 60 scores a minute per session, and each board's own wait | | Saves | 8 slots per player per game, 1 MB each; 60 saves a minute per session | | Reading | 240 reads a minute per session (trophies, stats, scores, saves) | | A game key | 1,200 requests a minute | ## Test mode You play your own game in test mode, and so do Player Two’s admins, and so do your playtesters before it’s out (a game kept private, or set to come out on a day: you invite them in your studio, or say yes to players who ask from its page): `p2.test` is true. Rooms, voice, saves and every call work for them on Player Two’s own servers, free. What you earn is kept apart, unnumbered and without XP; your scores show only to you. In your studio, each game’s Testing section wipes your test unlocks, stats and scores, so you can play it again as a new player would. ## Rooms Multiplayer with no server of your own, for games hosted on Player Two: Player Two seats your players in rooms, near them, and your game's pages talk through the room. For games where cheating matters, the room runs your own code. ### Joining ``` const room = await p2.rooms.join({ mode: "duel", size: 2 }); // a match: the fullest open room near you, or a new one const mine = await p2.rooms.create({ mode: "duel", size: 4 }); // a private room; friends join it by mine.code const theirs = await p2.rooms.join({ code: "K7Q2PX" }); const party = await p2.rooms.party(); // the player's party, and only them const friends = await p2.rooms.join({ room: friendsRoomId }); // a friend's room by its id (from presence or a message) room.id; room.code; room.mode; room.size // its name (join({ room: room.id }) comes back to it), a private one's code, its mode and size room.me; room.host // your id in it, and the host's room.leave(); // out of it: your seat goes to someone else ``` Rooms are for games hosted on Player Two (a build you push, or your site served through Player Two), with Player Two rooms picked as the game’s multiplayer (the default). A match puts the player in the fullest open room of that mode and size near them (anywhere when none is near), so rooms fill before more open and players meet those nearest first; a room runs in the Cloudflare location nearest its first player. Modes are names you choose (`"duel"`, `"coop-hard"`); sizes are 1 to 64 players. Every room’s players are its game’s, signed in to Player Two: `room.players` has each one’s id (the same as `p2.player.id`), handle, name, picture, critter and level, their `team` in a room with teams ([below](https://playertwo.plus/docs/sdk#rooms-teams)) and their `rating` in a scripted room. Any room can be joined by its id, a matched one too: pass a friend’s `room.id` (put it in `p2.presence`, or send it) to `join({ room })`. If it can’t be had, the promise rejects with an error whose `code` says why, so the game can fall back to an ordinary match: ``` const room = await p2.rooms.join({ room: friend.room }).catch((e) => { if (["no-such-room", "full", "not-open", "other-build", "not-in-party"].includes(e.code)) return p2.rooms.join({ mode: "casual", size: 4 }); throw e; }); ``` `no-such-room`: it closed (or never was). `full`: every seat is taken. `not-open`: its code closed it to matching (`room.open(false)`: a match under way takes no one new; a player who had a seat in it still comes back). `other-build`: it is for another version of the game (reload, or match). `not-in-party`: a party’s own room. `bad-room`: that isn’t one of this game’s rooms. A guest on X joins by id like anyone else. To look on without a seat, `watch({ room })` (below) works on a closed match too. The full list is under [limits](https://playertwo.plus/docs/sdk#rooms-limits). ### Teams and parties ``` const room = await p2.rooms.join({ mode: "ranked-3v3", size: 6, teams: 2, together: true, skill: true }); room.teams // 2 room.players.filter((p) => p.team === me.team) // your side: team is 1 or 2 room.on("team", (id, team) => moveToSide(id, team)); ``` room.js ``` export function onJoin(room, player) { spawnOnSide(player.id, player.team); } // room.team(id, 2) moves a player to team 2; every page hears "team" ``` `teams` splits a match’s size evenly (2 to 16 teams; 6 seats in 2 teams is 3v3), and is part of what a match is, as its mode and size are. Each player is put on a team as they are seated, and `team` is on each of `room.players` and on the players your `room.js` sees; a player on their own goes on the team with the most seats free, and one who joins by the room’s id on the one with the fewest players. Your code can move anyone with `room.team(id, n)`. `together: true` matches the player’s whole Player Two party: the first member to ask finds a match with a team that has a seat for every member, the other members’ seats are held there on that team for 30 seconds, and each member who then asks with `together` (the same mode, size and teams) gets theirs, beside the rest. Players on their own fill the other team. Without `teams`, the party is simply in one match together. With `skill`, the party is matched on its members’ mean rating. It rejects with `no-party` when the player isn’t in one, and `party-too-big` when the party has more members than a team has seats. Each member’s page asks for itself: tell the party when to look (a message in the party, or each starts when its leader does), since a member who asks after the 30 seconds is matched as a party again, beside any members already in. `p2.rooms.party()` is still the party’s own private room. ### Messages and state ``` room.send("move", { x: 3, y: 1 }); // to everyone else room.send("whisper", "gg", { to: otherId }); // to one (or a list) room.on("message:move", (move, from) => apply(from, move)); room.set("score:" + room.me, 120); // the shared state, a key at a time room.on("state", (key, value, by) => redraw(key, value)); room.get("round") // what the room has now room.state; room.owners // all of it, and whose each key is if (room.isHost()) startRound(); // the host: whoever has been here longest const ms = await room.ping(); // there and back room.now() // the room's clock, the same for everyone in it room.stats() // { ping, jitter, lane, up, down }: for a connection meter ``` Messages are relayed as they are sent and never kept; what a scripted room’s code sends a page at once arrives as one packet. A connection that goes quiet is noticed within about 8 seconds (a ping every 2 seconds, answered by the network without waking the room) and your seat is got back by itself (`reconnecting`, `reconnected`). `room.stats()` says the ping and its jitter, measured all the while, which way messages are going, and the bytes a second each way. The shared state is kept by the room: a player who joins late, or comes back, gets all of it. A key belongs to the player who first wrote it, and the host may write any key; a leaving player’s keys become the host’s. A write to someone else’s key is undone and refused (`error` with code `not-yours`). Your rules run in your game: on the host’s page, or on every page. ### Smooth movement ``` const sync = room.sync(me.mesh); // a THREE.Object3D: position.x, .y, .z and rotation.y sync.on("join", (id) => sync.bind(id, makeAvatar())); // another player's first update: bind their object sync.on("leave", (id) => removeAvatar(id)); // in your render loop: sync.step(); // sends yours, and moves everyone else's ``` Your object’s fields go out 30 times a second while they change (15 in a match of more than 16 players, and once a second while they don’t), stamped with the room’s clock, as bytes rather than text where they go through the room: a move of four values is 18 bytes instead of about 70, and each player in an update about two fifths of its JSON, every value to a thousandth. Your game’s own messages and the shared state go as bytes too: a message is about a third of its JSON (who it is from in a byte or two, not their id), a list of objects with the same keys about half, and a message may carry bytes of its own (an `ArrayBuffer` or a typed array) as they are. Every other player’s object is drawn a moment in the past, on a smooth curve through their updates that never overshoots, so it glides instead of jumping; if an update is late it carries on a little the way it was going. That moment is each player’s own, measured as you play: as late as their updates come and the time between two, so a player nearby is drawn about 60 to 90 ms behind and one across the world as far as it takes to stay smooth, eased as it changes. A player who stood still and moves again starts when they started, not before. Options: `{ fields, rate, delay, angles, snap, near }`: `fields` are paths on the object (`["x", "y"]` for a 2D sprite), `delay` fixes the moment in ms for everyone, `angles` turn the short way round, a jump longer than `snap` is a teleport, and `near` is how far this page sees (below). `sync.values(id)` gives a player’s smoothed values without moving anything, and `sync.delay(id)` how far behind they are drawn. ### The fast lane ``` room.send("aim", { x, y }, { fast: true }); // lossy and quick: the next update replaces a lost one ``` Fast messages and `room.sync` go player to player over a WebRTC data channel through Cloudflare’s Realtime network, close to every player, unordered and never resent, so one lost packet never holds up the next. Use it for what the next update replaces (where a player is, where they aim), and plain messages for what must arrive (a hit, a score). It is set up by itself the first time you use it, in rooms of up to 16 players; until every other player has one, in a scripted room and in a bigger room, fast messages go the ordinary way. The ordinary way is a WebSocket, where one lost packet holds up everything after it until it is sent again (a browser can’t send UDP to a server). So where moves go through the room, `room.sync` opens a second socket by itself and sends its moves by the two in turn, and the room sends its updates the same way: a lost packet holds up only half of them. On a connection losing a packet every second and a half, the time the latest move was over 100 ms old fell from 13% to 2.5%. A fast message that can’t go player to player (in a scripted room, where it goes to your code, or a room over 16) goes by the same two sockets in turn, numbered: one held up on one socket doesn’t hold up the next on the other, and an older one of its type that arrives after a newer one is dropped, so your code only ever hears the newest. For a player’s controls, use [input-run rooms](https://playertwo.plus/docs/sdk#rooms-inputs): each input goes by both sockets with the two before it again, and none is ever dropped. Cloudflare’s Realtime network carries data channels only between browsers, not to a room’s server (checked 2026-11-06), so the room’s own lane is these two sockets. ### Scripted rooms room.js, at your build's root ``` export const tickRate = 20; // ticks a second (1 to 60) export function onCreate(room) { room.state.set("round", 1); } export function onStart(room) {} // each time the code starts (after onCreate the first time) export function onJoin(room, player) { room.send("welcome", { id: player.id }); } export function onMessage(room, player, type, data) { if (type === "shoot" && canHit(room, player, data)) room.send("hit", { by: player.id, on: data.target }); } export function onSync(room, player, values) { // optional: check movement; false drops it return tooFast(room, player, values) ? false : values; } export function onTick(room, dt) { stepTheWorld(room, dt); } export function onLeave(room, player, why) {} ``` Put a `room.js` (one ES module, bundled, up to 1 MB) at the root of the build you push, and every room of that build runs it on Player Two’s servers: in a sandbox of your build’s own with no network, 100 ms of CPU a call. Each room has its own `room`, `room.data` and `room.store`, but rooms of your build that run on the same machine share your module’s own variables (what you declare at its top level): keep what is one room’s in `room.data`, or in a `Map` by `room.id` (the same id its pages have). In a scripted room your pages’ messages go to your code, not to each other, and only your code writes the shared state, so a player can’t fake a hit or a score. `room` has `id`, `players`, `player(id)`, `send(type, data, { to })`, `state.get` and `state.set`, `kick(id, why)`, `close(why)`, `between()`, `past(id, at)` and `objects` (below), `data` (yours to keep things in, kept with the room), `store` (a key-value store for what doesn’t fit in `data`, read and written at once: `store.get`, `store.set`, `store.delete` and `store.keys(prefix, limit)`; a world’s is kept for good, through every new build), `save(id, data)` (what you keep of a player, below), `now()` and `log()`. Each of `room.players` has the player’s `id`, `handle`, `name`, `level`, `saved`, `rating`, `team`, and `ping` and `jitter`, their round trip as the room measures it ([below](https://playertwo.plus/docs/sdk#rooms-network)). `room.scripted` is true in your pages. Bundled from TypeScript, `defineRoom` from `playertwo-sdk/room` types every hook at once; plain JavaScript gets the same types with `// @ts-check` and the package’s `Room` and `Player`. `room.save(player.id, data)` keeps what your game knows of a player on Player Two’s servers, for good (up to 64 KB: their level, their gear, where they were): it comes back as `player.saved` when they join any of your game’s rooms or worlds, on any device. Players can’t write it; only your code can. Hits judged as the shooter saw them ``` // the page room.send("shoot", { at: sync.seen(targetId), target: targetId, aim }); // room.js export function onMessage(room, player, type, data) { const where = room.past(data.target, data.at); // the target where the shooter saw them if (type === "shoot" && where && hits(data.aim, where)) room.send("hit", { by: player.id, on: data.target }); } ``` Each page draws the others a moment in the past, so a shot that hits on the shooter’s screen can miss where the target is now. `sync.seen(id)` on the page is the room’s time that player is drawn at; `room.past(id, at)` in your code is where they were then (by field name, from their moves of the last second), so you judge the hit as the shooter saw it, the way shooters do it on their own servers. A room opens near its first player, so when your code decides hits, the player nearest it has the edge. Call `room.between()` as a match starts: when its players are far apart (Bangkok and Toronto, say), the room is opened again in the region nearest the furthest of them and every page moves there by itself in about a second (“reconnecting”, “reconnected”, then “moved”). Your code starts again there as after a restart, with `room.data` and the shared state: `onCreate` doesn’t run again, and what lives only in your module’s own variables doesn’t come along. Players near each other stay where they are. The file is private: Player Two takes it out of your build as the build goes live and never serves it to players. Each build’s code is its own sandbox, so a new build’s code starts with new rooms and never touches the rooms of the one before; what your code throws shows in your studio. On localhost a page that serves `/room.js` gets a scripted room too: the first tab runs it, and when that tab closes the next one carries on with its `room.data` (one that can’t be copied between tabs, with functions in it, starts again). ### Corrections room.js ``` export function onSync(room, player, values) { if (values.x > 100) return { ...values, x: 100 }; // a wall: corrected return values; } export function onMessage(room, player, type, data) { if (type === "hit") room.correct(data.target, { x: room.past(data.target).x + 5 }); // knocked back } ``` your page ``` sync.on("fix", (values, { by }) => { player.velocity.x = 0; }); // put right: reset your own speed here ``` When your code corrects a move, or moves a player itself with `room.correct(id, values)`, their page is put right the way Unreal puts a character right: as of the move your code corrected, with everything the player has done since played on top. So what shows is only how far off they were (a knock-back of 5 moves them 5), not a jump back by the distance they covered while the correction was on its way. The moves a player makes before their page hears of it are moved by it too, by your code, so your code, the player and everyone watching see them in the same place, and never going back. Pushed 5 along while moving, with 200 ms of lag: the page moved 5.00, and the other player saw them go straight on; put where the room says instead (`{ fix: "snap" }`, as before), the page moved 4.49 and the other player saw them go back 3.5 first. `sync.on("fix", (values, { at, by }))` says what changed; if your game keeps its own copy of the player’s position, copy `values` into it there. ### Input-run rooms room.js ``` export const tickRate = 60; import { step } from "./fight.js"; // your game's own step, the same file your page runs export function onJoin(room, player) { (room.data.fighters ??= {})[player.id] = { x: 0, y: 0, hp: 100 }; } export function onTick(room, dt, inputs) { // one input of each player's a step, in order for (const p of room.players) { const f = room.data.fighters[p.id]; step(f, inputs[p.id] ?? null, dt); // none: theirs didn't come in time room.confirm(p.id, f); // their state now, for their page room.objects.set(p.id, { x: f.x, y: f.y }); // where everyone else draws them } } ``` your page ``` const me = room.inputs({ state: { x: 0, y: 0, hp: 100 }, input: readPad, step, smooth: 100 }); // each frame: me.update(); // this tick's input read, sent, and run here at once draw(me.view()); // you, with no wait; everyone else by room.objects ``` For a fight, or anything a player mustn’t be able to fake, run the room on its players’ inputs, as Unreal and Overwatch do: each page sends only what its player pressed, a tick at a time, and your code runs one of each player’s inputs a step, so where everyone is and who hit whom is your code’s alone. The page runs the same step on its own inputs at once, so its player moves with no wait however far the room is; `room.confirm` says back what your code has, and when that isn’t what the page had after that input, the page starts again from your code’s state and runs its inputs since (`"fix"`), so a correction shows only as the difference, eased off over `smooth` ms. A room holds two of a player’s inputs before it starts on them, so a late packet doesn’t leave it a step without one; it says how many waited, and a page that is ahead ticks slower until the wait is gone (the longer the wait, the faster it goes: a slow start or a stall is made up in a second or two). Inputs go by two sockets, each with the two before it again, so one held up on one comes by the other, and a lost packet holds up only half of them. A missing input is never waited for: when its step comes the next one runs, and the missing one, if it arrives after, is dropped. The steps keep to real time: a stall is made up, up to four steps at once. With 200 ms of lag, a page’s inputs ran at once, the room agreed with all of them and none was played again. ### Moving objects room.js ``` export const tickRate = 30; export function onStart(room) { room.data.orcs ??= [{ x: 0, z: 0 }]; } export function onTick(room, dt) { room.data.orcs.forEach((o, i) => { o.x += dt; // your server moves them room.objects.set("orc:" + i, { x: o.x, y: 0, z: o.z, ry: o.x }); // every page draws them }); } // room.objects.delete("orc:3") ends one; room.objects.get(key), room.objects.keys() ``` your page ``` const orcs = room.objects(); // on a three.js object, x, y, z and ry go to its position and rotation orcs.on("add", (key, values) => orcs.bind(key, makeOrc())); orcs.on("remove", (key) => removeOrc(key)); // in your render loop: orcs.step(); ``` What your server moves (enemies, projectiles, a lift) goes to every page as players’ moves do, and each page draws it smoothly, as late as its updates need, on the same curve. Your code sets each by a key with its values by name; only what changed is sent, and in a room of more than 16 players each page gets those within its `near` every tick and the rest once a second. A page that comes in gets them all with its welcome. `map` puts each value on a path of your object; on a three.js object `x`, `y`, `z`, `qx` to `qw` and `rx` to `rz` go to its position, quaternion and rotation by themselves (anything else: the name itself). `ry`, `yaw`, `angle` and `rotation.*` turn the short way round, and a quaternion turns the short way at an even pace. `objects.seen(key)` with `room.past(key, at)` judges a hit on one as the shooter saw it. Objects live with your code, as its module’s variables do: `onStart` runs each time it starts (after an update, a restart, or `between()`), the place to set them again from `room.data`. Up to 1,000 a room, 32 names for their values. ### Compiled engines room/room.js ``` // node p2.mjs wasm node_modules/@dimforge/rapier3d-compat/dist/rapier.mjs room/ (writes rapier.js and rapier0.wasm) import RAPIER from "./rapier.js"; export const tickRate = 60; const worlds = new Map(); // one physics world a room, by room.id (rooms share the module) async function ready(room) { let w = worlds.get(room.id); if (!w) { await RAPIER.init(); const world = new RAPIER.World({ x: 0, y: -9.81, z: 0 }); w = { world, crates: [0, 1, 2].map((i) => world.createRigidBody(RAPIER.RigidBodyDesc.dynamic().setTranslation(i * 2, 5, 0))) }; worlds.set(room.id, w); // after a restart it starts again } return w; } export async function onCreate(room) { await ready(room); } export async function onTick(room, dt) { const { world, crates } = await ready(room); world.step(); crates.forEach((body, i) => room.objects.set("crate:" + i, body)); // a body as it is: every page draws it } export function onLeave(room) { if (!room.players.length) worlds.delete(room.id); } ``` A physics engine compiled to WebAssembly (Rapier, Box2D, Jolt) runs in your room as it does in the browser. Put your room’s code in a `room/` folder at your build’s root instead of one `room.js`: `room/room.js` is where it starts, and it imports the rest by name, other modules and `.wasm` files (up to 10 MB all told; the folder is taken out of your build and never served, as `room.js` is). Hooks may be async. A `.wasm` file comes in as a compiled `WebAssembly.Module` (`import engine from "./engine.wasm"`) for your code to instantiate: rooms compile no WebAssembly from bytes as they run, so a package that carries its WebAssembly inside it as text, as Rapier’s `-compat` packages do, goes through `node p2.mjs wasm` first, which writes it into your folder with its `.wasm` beside it. Stepping a world of 200 bodies takes a fraction of a millisecond; each call (a tick, a message) has 100 ms. `room.objects.set(key, body)` takes a body as it is (Rapier’s, or anything with a position and a quaternion, as cannon-es’s and three.js’s have): its `x`, `y`, `z` and quaternion `qx` to `qw` (Rapier 2D: `x`, `y` and `angle`), drawn smoothly on every page, so you can set every body every tick: one that hasn’t moved (asleep) is sent once more, then not until it moves. Each room’s code runs on one thread (Cloudflare gives each room one); a world spreads its players over areas, each a room of its own. On localhost the stand-in runs plain JavaScript rooms only (a physics engine in plain JavaScript, cannon-es say, runs there too). ### Matching by skill room.js ``` room.result([winner.id, [second.id, third.id], last.id]); // places, best first (level ones together) room.open(false); // a match under way: no one new room.open(true); // a player left: fill their place (backfill) ``` your page ``` const room = await p2.rooms.join({ mode: "ranked", size: 4, skill: true }); ``` Your code says how a match ended and every player’s rating moves by it (Elo, every two players compared by their places; a new player’s moves faster), kept with the game for good, as `player.rating` in your code and `rating` on each of `room.players`. Ratings are kept by mode: a player’s `"ranked-1v1"` rating and their `"ranked-3v3"` one are apart, and the room’s mode is the one `room.result` moves and `skill` matches by. A player’s first rated match in a mode starts from the rating they had in the game before (1500 if none), counted as new, so it finds its level in a few matches. In a team match, give the places by team: `room.result([winners, losers])`, each a list of ids. With `{ skill: true }` a player is seated in the room whose players are rated nearest them, near them first, and in one a world apart in skill only when there is no other. `room.open(false)` keeps a match under way out of matching (and refuses a seat by its id, `not-open`); `room.open(true)` lets it fill its empty places first. ### A ranked leaderboard, and trophies your server gives room.js ``` export function onTick(room, dt) { if (!matchOver()) return; room.result([winners.map((p) => p.id), losers.map((p) => p.id)]); for (const p of winners) { room.score(p.id, "ranked-3v3", p.rating + 16); // on a board only your server may send to room.trophy(p.id, "first-win"); // a trophy only your server may give } } ``` Your `room.js` has no network of its own, so the room carries what it gives: `room.score(id, board, value)` sends a player’s score to one of your game’s leaderboards (kept if it is their best, as any score is) and `room.trophy(id, key)` gives them one of its trophies, both as your game’s own server would. They come from the room, signed by Player Two’s rooms and checked against your build’s code, so a page can’t fake them. Make the board in your studio **Trusted** (`trusted`) and the trophy **Only my server gives it** (`serverOnly`): then the page’s `p2.scores.submit` and `p2.trophies.unlock` are refused for them, and only your room code (or your own server, with a game key) can put anything there. They go to Player Two within a quarter of a second, a hundred at a time; what is refused (a board that isn’t there, a guest who has never played the game on Player Two) shows in your studio with your code’s errors. ### Watching and replays ``` const rooms = await p2.rooms.live({ mode: "ranked" }); // the game's public rooms open now, fullest first const watching = await p2.rooms.watch({ room: rooms[0].id }); // draw it as you draw a room you play in const [last] = await p2.rooms.replays(); const replay = await p2.rooms.replay(last.id); // a room that plays itself replay.replay.speed(2); replay.replay.play(); // pause(), seek(ms), time(), duration ``` Anyone playing your game can watch a room: they hear and draw everything its players do, with no seat, and can say nothing (`room.watching`), up to 64 a room. A room is kept as a replay when your code calls `room.record()` (or its first player joins with `{ record: true }`): what every page heard, as it happened, until `room.record(false)` or the room closes, up to 30 minutes, the newest 200 a game. A replay is a room that plays itself, on its own clock (`room.replay` is its controls: `play()`, `pause()`, `speed(x)`, `seek(ms)`, `time()`, `duration`, and `on("end")`), so the same `room.sync`, `room.objects` and `room.on` draw it; seek to any moment and the room is as it was then. A replay keeps what every page heard: moves, your code’s objects, the shared state, players coming and going, teams, and messages sent to everyone. A message sent to some players only (`{ to }`, from a page or from your code) isn’t in it, nor are the answers to inputs, so what a replay must show goes to everyone or into the shared state. ### A bad connection, on purpose ``` p2.rooms.simulate({ lag: 150, jitter: 30, loss: 0.02 }); // 150 ms more there and back, give or take 15, 2% lost p2.rooms.simulate(null); // as it is again // or with no code: your page's address with ?p2-net=150,30,0.02 ``` Try your game the way a far-off player on a bad Wi-Fi plays it, as Unreal’s network emulation does: what goes between this page and its rooms waits half of `lag` each way, give or take half of `jitter`, and `loss` (0 to 1) of it is lost. A lost packet on a socket is held up as TCP holds it, a retransmit’s wait (200 ms, or the lag), and everything behind it with it; on the fast lane it is gone. It works on Player Two and on localhost, in this page only; `room.ping()` and `room.stats()` show it. ### Timing and the network room.js ``` export function onTick(room, dt) { for (const p of room.players) { p.ping; p.jitter // ms: their way here and back, and how much it varies (null at first) if (p.ping > 250) room.send("lag", p.ping, { to: p.id }); } } export function onMessage(room, player, type, data) { if (type === "shot") room.send("shot", { by: player.id, late: (player.ping ?? 0) / 2 }); // to every page, the shooter's too const others = room.players.filter((p) => p.id !== player.id).map((p) => p.id); if (type === "ready") room.send("ready", player.id, { to: others }); // to everyone but them } ``` **Each player’s ping, in your code.** Every player your `room.js` sees has `ping`, their way to the room and back in ms, and `jitter`, how much it varies. The room measures them itself: every 2 seconds it sends each page a probe that the SDK sends straight back, so the figure is the room’s own, over the whole way (through a match’s gates too, and with any bad connection [simulated](https://playertwo.plus/docs/sdk#rooms-simulate) on that page), not what the page says. The first comes about half a second after a player joins, plus their round trip, and is taken as it is; each one after moves `ping` and `jitter` a quarter of the way, so a change shows most of the way within about 8 seconds. Until the first, both are `null`. A page whose SDK is older than the room (from before 2026-11-06) doesn’t answer the probe; the connection it reports itself every 10 seconds stands in. A new figure reaches your code with its next call (a tick, a message), never in a call of its own. One way is about half the round trip, though a route can be slower one way than the other. In a room with no code, each page has its own with `room.stats()` and `room.ping()`. On localhost, where the rooms are a browser’s own tabs, both are 0. Rooms opened before 2026-11-06 play on without them. **What leaves when.** A player’s message goes to your code as it arrives, not held for a tick. Calls into your code run one at a time, in order, so a message that arrives while a tick is running waits for that tick’s call to end and goes in the next, with whatever else came meanwhile; inputs ([room.inputs](https://playertwo.plus/docs/sdk#rooms-inputs)) are the one thing that waits for the next step. Nothing is sent while your code runs: everything one call sends a page (from a tick or from a message) leaves as one packet as the call returns (the moves’ frame may come by the page’s second socket beside it). `room.send(type, data)` in your code is one call however many players there are, and goes to every page, the player whose message you are answering included; pass `{ to }`, an id or a list of them, to send to some only (at the same cost), so a player doesn’t hear their own press back. (A page’s `room.send` in a room with no code goes to everyone else.) What the room sends its players isn’t counted, queued, dropped or cut off by Player Two; the bounds are each call’s 100 ms of CPU and 2 seconds, the shared state’s 96 KB and 512 keys, and each page’s own connection. What a player sends is limited: 60 messages a second, bursts of 120, inputs 130 a second of their own, 16 KB each ([limits](https://playertwo.plus/docs/sdk#rooms-limits)). **The way there.** A page’s connection goes to the Cloudflare location nearest it, then over Cloudflare’s own network to the room (a Durable Object in one location), which relays it, or hands it to your code, at once; what goes to another player goes from the room to the location nearest them and on to their page. A match of over 16 seats goes through its gates, one hop more each way. Player Two publishes no measured figure for these hops: measure your own with `room.ping()`, `player.ping` and [your rooms’ connections](https://playertwo.plus/docs/sdk#rooms-stats). The sockets end at Cloudflare’s edge, where Player Two can’t set TCP options such as `TCP_NODELAY`; browsers send a WebSocket’s messages as they are sent, and Cloudflare doesn’t document its side. A browser can’t send UDP to a server, and Cloudflare’s Realtime data channels run only between browsers, so the quick way to the room is two sockets in turn (`room.inputs`, and `{ fast: true }` messages), and the lossy lane is player to player, in rooms with no code of up to 16 ([the fast lane](https://playertwo.plus/docs/sdk#rooms-fast)). **Where a room runs.** A room opens in the Cloudflare location the connection of the player who opened it came in by, near them, and stays there. Matching seats players in the fullest open room whose first player is within 3,000 km of them, anywhere when none is. `room.between()` opens a room again in one of Cloudflare’s regions, Western North America (`wnam`), Eastern North America (`enam`), South America (`sam`), Western Europe (`weur`), Asia-Pacific (`apac`), Oceania (`oc`) or the Middle East (`me`): the one whose furthest player is nearest, and only when that brings the furthest player at least 1,500 km and a fifth nearer than where the room is, at most once a minute. Each seat is held there 20 seconds while its page reconnects by itself. **The room’s clock.** `room.now()` on a page is the room’s time, set the way NTP sets a computer’s: each ping (yours, `room.ping()`, and the SDK’s own, 50 ms after joining, four more 300 ms apart, then every 15 seconds for two minutes and every minute after) gives the room’s time plus half the round trip, and the quickest of the last 16 is the one used (a slow round trip says least about the time). The first is taken as it is; a later change is eased in at a tenth of the time that passes, so `room.now()` never goes back, unless it is a second or more off, when it is set at once. It is off by at most half the quickest round trip, and by a few ms on a route as quick each way. In `room.js`, `room.now()` (and `Date.now()`) is the time the call began: it doesn’t move while your code runs (Cloudflare’s clock moves only between a call’s waits), so every step of one call has the same time. ### Worlds ``` const room = await p2.rooms.world("main"); // the game's world, kept for good const cave = await p2.rooms.world("main", { area: "cave" }); // an area of it: a room of its own room.world; // true const sync = room.sync(me.mesh, { near: 80 }); // in a crowd: those within 80 every tick ``` A world is a room your game keeps for good: it never closes, it sleeps for nothing while no one is in it (no ticks then), and it keeps its shared state, and in a scripted world your code’s `room.data` and `room.store`. Areas split a big world into rooms of their own (a town, a cave), each up to 256 players; players go from one to the next by leaving one and joining the other, and what you keep of each player with `room.save` goes with them. A world plays your live build: as you push a new one, the next player to come in moves it on (everyone in it hears `close` with `"update"`, to reload, and your new code carries on with the same `room.data` and `room.store`; `onCreate` never runs twice), and a page still on the build before is told to reload. `room.close()` in a world lets its players go and keeps everything else. In a room of more than 16 players, everyone’s moves go through the room, which sends each page the moves it hasn’t had 30 times a second in one packet: those within the page’s `near` every time, those further once a second, and at most 64 at a time, the longest waiting first, so a crowd costs each player what they can see. `export const near = 80` in your `room.js` sets it for every page, so a page can’t ask to see further. Distances are measured on the fields named `x` and `z` (or `y`). An area’s connections, and a match’s of more than 16 seats, are held by gates in front of the room (`room.gated` says so) (one for each 16 players of a match, eight for an area), so the room sends and hears a few packets a tick, not one for every player: 256 players moving 30 times a second each kept their updates coming 30 times a second, a move from one to a player near them about 95 ms. ### Voice chat ``` await room.voice.join(); // asks for the microphone; then everyone in voice hears you room.on("speaking", (id, on) => glow(id, on)); room.voice.mute(true); // or push to talk: mute(false) while a key is held room.voice.isMuted(); // whether you are room.voice.speaking(id); // whether they are talking now room.voice.volume(id, 0.3); // how loud one player is, to you room.voice.viaParty(id); // whether they reach you through Player Two's party voice instead await room.voice.join({ mic: stream }); // or a microphone your page has already (a MediaStream) room.voice.leave(); ``` Voice goes from each player’s microphone to everyone else in voice in the room, through Cloudflare’s Realtime network close to every player, in any room (scripted ones and worlds too), up to 24 in voice a room. The browser asks the player for their microphone the first time; `join` fails with `no-mic` when they say no. `room.voice.who` is who is in voice, and `"voice"` (id, in or out) and `"speaking"` (id, on or off) say when that changes; each player can turn anyone down to nothing on their own page. `join({ mic })` sends a `MediaStream` your page has already instead of asking (the room stops it when the player leaves voice: give it a copy, `new MediaStream(stream.getAudioTracks().map((t) => t.clone()))`, to keep your own). Voice needs a room on Player Two; on localhost `join` says `no-voice`. **Blocks:** two players either of whom blocked the other on Player Two never hear each other in your rooms (rooms from r17, and the worlds): neither is subscribed to the other’s voice, though both stay in the room and in `room.voice.who`, and nobody is told who blocked whom. Everything else in your room (players, messages, the state) is as before. **Player Two’s own voice:** a player can be in their party’s voice (in the chat’s Party panel) while they play your game: it’s Player Two’s, on its page, not your room’s, and your game hears nothing of it. **Nobody is heard twice:** while a player reaches another through their party’s voice (both of them in it, the speaker’s microphone on there, the listener not deafened and not having turned them down to nothing there), your room’s voice of the one is silent to the other, on that player’s page only; the moment that stops (either leaves the party’s voice, mutes or deafens there, or on push-to-talk lets go of V), they’re heard in your room again, 300 ms later so the end of what they said isn’t doubled. You do nothing for this: they stay subscribed and in `room.voice.who`, `"speaking"` still says when they talk, as their party’s voice hears them (your name tags light up as ever), and your own `volume()` for them is kept for after. `room.voice.viaParty(id)` says whether it’s so for a player now (to show a party mark, or to explain a slider with no effect); Player Two doesn’t tell your game who is in a party or its voice beyond that. The party’s voice is the party’s, whatever your teams: a party split across your teams still hears each other through it. On push-to-talk the player holds V to talk; your game’s frame has the keyboard, so the SDK in your page tells Player Two when V goes down and up (not while they type in a field of yours). Your game gets the key as well, as ever: if V does something in your game, it happens as they talk. ### Events _What room.on hears_ | Event | With | When | | --- | --- | --- | | `join` | player | Someone came in. | | `leave` | player, why | Someone left ("left") or didn't come back in time ("dropped"). | | `away` | player | Someone's connection dropped: their seat is held 20 seconds. | | `back` | player | They came back to it. | | `host` | id | The host changed. | | `team` | id, team | The room's code moved a player to another team (room.team). | | `state` | key, value, by | A key changed (null: taken away). | | `message` | type, data, from | Any message; message: hears one type. | | `reconnecting` | | Your own connection dropped: the SDK is getting your seat back. | | `reconnected` | | It did; players and state are as the room has them now. | | `moved` | id | The room's code opened it again between far-apart players (room.between()), and you're in it: room.id is new. | | `voice` | id, in | Someone came into voice (true) or left it. | | `speaking` | id, on | Someone in voice (or you) started speaking or stopped. | | `close` | why | You're out: you left, the room closed, or your seat couldn't be got back ("update": a world moved on to your game's newer build; reload). | ### Releases A room is for one build of your game. A page on your new build is only ever seated with others on that build, and one still on the build before keeps playing with its own, so a release never mixes versions in a room: matches running when you push play on until they end. A page that asks for a seat on a build Player Two no longer keeps is told to reload (`old-build`). On localhost, without Player Two, the SDK’s rooms are this browser’s other tabs: open your game in two tabs and they are two players. ### Your rooms' connections your agent, with your agent key ``` curl https://playertwo.lol/api/v1/agent/games/your-game/rooms -H "Authorization: Bearer p2_agent_..." # or the MCP tool get_rooms with { "slug": "your-game" } # { slug, on, multiplayer, stopped, hosted, script, error, units, # now: { rooms, players, builds: [{ build, rooms, players }], # net: { rooms, ping, p90, jitter, down, up, fixes, rewinds, drops }, # top: [{ mode, players, watchers, build, at, pages, ping, p90, jitter, down, up, fixes, rewinds, drops }] } } ``` Your studio’s Rooms panel (in your game’s services) shows your game’s rooms and players now, by build, and how their connections were over the last minute: ping (the middle page’s, and `p90`, nine in ten at or under it, in ms there and back), jitter, data a second a page each way (`down` from the room, `up` to it, in bytes), corrections a minute a page (`fixes`: a move your code put right) and replays of a page’s own inputs (`rewinds`: started again from your code’s state), and connections dropped in the minute; for all your rooms (each room’s figures weighed by its players) and the five busiest. Your agent reads the same with your agent key, with the eight busiest rooms and what each figure is in (`units`): the MCP tool `get_rooms`, or `GET /api/v1/agent/games//rooms`, read-only, 60 a minute. The figures are what each page measures of its own connection (its `room.stats()`) and says every 10 seconds; a room passes them on every 30 seconds at most, so they lag by up to about 40 seconds, and a room whose pages stop reporting drops out of them after two minutes. `net` is `null` until a page has reported, and `now` when the rooms service didn’t answer in time. `player.ping` in your code is the room’s own measure ([above](https://playertwo.plus/docs/sdk#rooms-network)). ### Limits _A room's limits_ | What | Limit | | --- | --- | | Players | 1 to 64 a room; 256 an area of a world | | Messages | 16 KB each; 60 a second a player, bursts of 120 (inputs: 130 a second of their own) | | What the room sends | no limit of its own: a scripted room's code sends what it likes each call, as one packet to each page as the call returns (30 snapshots a second and one-off messages beside them all arrived, in order), bounded only by its 100 ms of CPU and 2 s a call; nothing of it is counted, queued, dropped or cut off by Player Two | | player.ping, player.jitter | measured by the room every 2 s, in room.js; from an older page, its own report every 10 s | | Shared state | 96 KB and 512 keys a room | | A dropped seat | held 20 seconds | | A room | closes a minute after its last player leaves, or after 6 hours; a world never | | Seats | 30 a minute a session; 500 rooms open at once a game | | room.js | 1 MB, or a room/ folder of 10 MB with .wasm; 100 ms of CPU a call (a tick, a message); 1 to 60 ticks a second; no network | | room.save | 64 KB a player, kept for good | | room.objects | 1,000 a room, 32 names for their values | | Voice | 24 in voice a room | | Watching | 64 a room | | Replays | 30 minutes each, the newest 200 a game | | room.score, room.trophy | 500 waiting a room, sent a hundred at a time; 3,000 a minute a game | A room’s messages to its players aren’t counted against anything: the limits above are on what a player sends. What does cost is size: each page gets the bytes your code sends it, so 30 snapshots a second of 1 KB each is 30 KB a second a player, and one player’s slow connection holds up only their own. Matches of over 16 seats go through the room’s gates, which send on what the room says in one call a turn, with no limit of their own either. _Why a seat is refused (the error's code)_ | Code | When | | --- | --- | | `no-such-room` | The room asked for by its id or code has closed, or never was. | | `full` | Every seat in the room is taken (or, to watch, 64 watch it already). | | `not-open` | Its code closed it to matching (room.open(false)): no one new by its id. | | `other-build` | The room is for another version of the game. | | `not-in-party` | It is a party's own room, and the player isn't in that party. | | `no-party` | party or together, and the player isn't in a party. | | `party-too-big` | together, and the party has more members than a team (or the match) has seats. | | `bad-room, bad-code, bad-teams, bad-ask` | What was asked for isn't right: a room of another game, a code that isn't six letters, a size teams don't split evenly, together with a room, code or world. | | `old-build` | The page's build is no longer kept (reload). | | `too-many-rooms` | The game has 500 rooms open already. | | `rooms-off, rooms-hosted-only, rooms-stopped` | The game's multiplayer isn't Player Two's rooms, it isn't served by Player Two, or Player Two stopped its rooms. | | `offline` | The room couldn't be reached. | Your studio shows your game’s rooms and how their connections are ([above](https://playertwo.plus/docs/sdk#rooms-stats); your agent reads them with `get_rooms`), and turns rooms off (rooms open then play on until they empty). ## Your game's server ### Game keys Make a key for your game’s own server in your studio (Game keys). It starts `p2g_`, is shown once, and only a hash of it is kept; revoke it there at any time. Keep it on your server: never in your game. ### What a player can fake > Everything your game does in the player’s browser can be changed by a determined player, so a trophy earned or a score sent from the game itself can be faked. For anything competitive, decide it on your server: mark those trophies “only my server can give it” and those leaderboards trusted, and send them with your game key. Check the session your game says it is in before you trust it. ### Its calls Who is playing, from the session your game's client sends you ``` curl https://playertwo.lol/api/v1/sdk/server/session \ -H "Authorization: Bearer p2g_..." \ -H "x-player-session: p2s_..." # { live, player: { id, handle, name, picture, level }, game, session, test, demo, from, startedAt, beatAt } ``` A trophy, server-only ones too ``` curl -X POST https://playertwo.lol/api/v1/sdk/server/trophies/tournament-winner \ -H "Authorization: Bearer p2g_..." -H "content-type: application/json" \ -d '{ "player": "" }' ``` A score, on any board (a trusted board takes them only this way) ``` curl -X POST https://playertwo.lol/api/v1/sdk/server/boards/ranked \ -H "Authorization: Bearer p2g_..." -H "content-type: application/json" \ -d '{ "player": "", "value": 1840 }' ``` The add-ons a player has ``` curl "https://playertwo.lol/api/v1/sdk/server/addons?player=" \ -H "Authorization: Bearer p2g_..." # { player: { id }, addons: ["night-map", ...] } (or send x-player-session in place of ?player=) ``` Whether an identity your game's client sends is good, and who it names ``` curl -X POST https://playertwo.lol/api/v1/sdk/server/identity \ -H "Authorization: Bearer p2g_..." -H "content-type: application/json" \ -d '{ "token": "" }' # { valid, player: { id, handle, name, picture, level }, kind, live, exp } ``` Your game’s client gets its session token only on a computer (the app hands it to the game); on the web it stays in Player Two’s page, so a web game’s server learns who is playing from an identity the client sends it (`p2.identity()`, checked against Player Two’s keys or with the call above), and names players by their id for the game. The store’s calls (charges, what a player has, consumables used up, receipts checked): [In-game purchases](https://playertwo.plus/docs/sdk#store-server). ## In-game purchases Sell inside your game through Player Two's payments. Your shop, its look and your whole buying flow are yours; Player Two's own card takes the player's yes, the money comes from their Player Two balance, and you get 80%. ### Two ways to sell _Charges and products_ | | Charges | Products | | --- | --- | --- | | Who sets what and how much | Your server, each time, by your own rules | You, ahead, in your studio or through your agent | | Your game asks with | `p2.store.pay(id)` | `p2.store.buy(key)` | | Who keeps what the player has | Your game (Player Two keeps the payment) | Player Two (`p2.store.owned()`; your server uses up consumables) | | Price | $0.10 to $500, to the cent | $0.99 to $500 | | What it can be | Anything: coins, a bundle, a season pass, a price you lowered for this player | A consumable, item, skin, mod or pack (below) | | You need | A server of your own, with your game key | Nothing more: it works from the game alone | | Player Two keeps | 20% | 20% | Both use the same card, the same balance, the same receipts, the same webhook and the same test mode, and a game can use both. A game with a server of its own that wants its own rules uses charges; a game without one uses products. ### Charges: your shop, your prices 1. **Your server makes it** With your game key: what (a name the player reads), how much, for which player, and your own reference. 2. **Your game asks to be paid** It hands the id to `p2.store.pay`. Player Two’s card shows the name and price; the player holds to pay. 3. **Player Two takes the money** From their Player Two balance, once. Your game gets the answer and a signed receipt; your server gets the purchase webhook. 4. **Your server gives it** Once for each charge, after checking it's paid. Your server makes a charge ``` curl -X POST https://playertwo.lol/api/v1/sdk/server/store/charges \ -H "Authorization: Bearer p2g_..." -H "content-type: application/json" \ -d '{ "player": "", "name": "5,000 coins", "about": "For the locker room shop.", "price": 4.99, "ref": "order-8812" }' # 201 { charge: { id, name, about, price, ref, player: { id }, state: "open", test: false, payment: null, created, expires, paid: null } } ``` Your game asks the player to pay it ``` const { id } = await myServer.post("/shop/coins"); // your server made the charge const paid = await p2.store.pay(id); if (paid.ok) myServer.post("/shop/paid", { receipt: paid.receipt }); // or let the webhook tell it else if (paid.reason === "cancelled") closeShop(); // they said not now // paid: { ok: true, charge: { id, name, ref }, amount, receipt, exp, test } ``` Your server checks it's paid ``` curl https://playertwo.lol/api/v1/sdk/server/store/charges/ -H "Authorization: Bearer p2g_..." # { charge: { ..., state: "paid", payment: "", test: false, paid: 1793890000000 } } ``` - **The player** is named by their id for your game: from an identity your client sends (`p2.identity()`, checked with `/api/v1/sdk/server/identity`), or by the session token a computer’s client sends, in `x-player-session` in place of `player`. A guest (a player in a card on X without an account) has no id a server can charge. - **They must have the game**: bought, added free, given, a key or a backer pass. A player on a free trial of a paid game can’t pay until they buy it (409 `no-game`). A player whose latest session is a test (your team, your testers) may. - **The price** is $0.10 to $500 in US dollars, rounded to the cent. **The name** is 2 to 60 characters and **about** up to 280: the card shows both. **ref** is up to 200 characters of your own (an item, an order, a cart): it comes back exactly as given with the charge, its receipt and its webhook. - **Open for 30 minutes**, then `expired`: it can’t be paid from then on (a payment the player started before still counts). Make a new one for a new try. - **Paid at most once.** Make one for each purchase. `p2.store.pay` on a charge already paid answers its receipt again, so a game that lost the answer can ask again. - **Withdraw** an open one with `POST /api/v1/sdk/server/store/charges//cancel`: it can’t be paid from then on, and `p2.store.pay` rejects `withdrawn`. A payment that was already on its way finds it withdrawn and goes straight back to the player’s balance. A paid charge can’t be withdrawn (409 `paid`). - **Its state** is `open`, `paid` (with its payment, or `test: true`), `cancelled` or `expired`. - **A player’s charges**, newest first, come from `GET /api/v1/sdk/server/store/charges?player=`: those paid if you don’t say (`state` open, cancelled, expired or all for others), up to 100 a page, `before=` the answer’s `next` for more. So your server needn’t keep its own record of what each player bought: your `ref` on each says what it was. - **Before your game is out** (kept private, or coming on a day), charges can be made, and only test sessions can pay them. A game sells once it is in the store; a raise without a price can’t (409 `not-in-store`). - Your server learns only the player’s id for your game and what they paid: never their balance or who they are. ### Products: made in your studio _What a product can be_ | Kind | What it is | | --- | --- | | `consumable` | Bought again and again; each purchase gives its number, 1 to 1,000. Player Two keeps the count, and your server uses them up. | | `item` | The player's for good, bought once. | | `skin` | The player's for good, bought once. | | `mod` | The player's for good, bought once: a change to how the game plays. | | `pack` | 2 to 10 of the game's other in-game products at once (no add-ons or other packs); a consumable in it gives its number. It can't be bought once everything in it is theirs. | | `dlc` | An add-on: not in your game’s shop but on its page, split as its sales are (10% Player Two’s). See [Add-ons](https://playertwo.plus/docs/sdk#addons). | - Make them in your studio (each game’s card, Add-ons and in-game products), or through your agent: `POST /api/v1/agent/games//products`, or the MCP tools `define_product` and `list_products`. - A **key** is what your game asks for it by: small letters, numbers and dashes, up to 40, starting with a letter or number. A **name** is 2 to 60 characters, **about** up to 280. The **price** is $0.99 to $500. A game has up to 100 products, add-ons and in-game together. - Your game names a product by its key, never a price: the price is the one you set. - Once a player has one, its key and kind stay. Its name, about, price, a consumable’s number and a pack’s contents can change. Retire one to take it off sale: its owners keep it. Products are never deleted. - A skin, item or mod the player has can’t be bought again: `p2.store.list()` says `owned: true`, and `p2.store.buy` rejects `owned`. - Only a player who has the game can buy (`no-game` otherwise), and a game has products once it is in the store. ### Player Two's step When your game calls `p2.store.buy` or `p2.store.pay`, its frame steps back to one side, dimmed, and Player Two’s card stands beside it. It shows Player Two, your game’s name and what kind of purchase it is; the name and line about it; its price; and the player’s balance now and after. The player holds **Hold to buy** for under a second (a tap never buys; Enter does too), or presses **Not now** (or Escape). On a phone the card sits at the foot of the screen. A test session’s card says “A test buy · nothing is paid”. When their balance is too low, the card offers to add funds right there, and goes on once they arrive. Anything that stops the payment itself (payments paused on Player Two, the player’s balance not ready) is said on the card; when they close it, your game hears `cancelled`. - While the card is open, your game gets no clicks or touches but keeps running. Pause it, or sell only where nothing happens (between rounds, in a menu). - One card at a time: a second `buy` or `pay` meanwhile rejects `busy`. - Your promise waits as long as the player takes, adding funds included, up to 20 minutes (then it rejects `timeout`). Don’t time it out yourself. - Then your game is back, and shows the purchase its own way. ### Staying right > Player Two’s record is the truth, not the answer your game gets. Rarely, a payment goes through but its answer doesn’t reach your game (the tab closed, the connection dropped); the player is told your game has it the next time it looks. So look. - **Products:** read `p2.store.owned()` (or, from your server, `GET /api/v1/sdk/server/store`) when your game starts and after any answer that isn’t `ok`, and show what it says. - **Charges:** your server gives on its own word: the purchase webhook, a charge read as `paid`, or a receipt your client sends. Give each charge once, its id the key. Read the state of charges you made whose answer never came. A server that keeps nothing of its own can ask for everything a player has paid for, each with your `ref`: `GET /api/v1/sdk/server/store/charges?player=`. - **Consumables:** their count is Player Two’s. Use them up with `POST /api/v1/sdk/server/store/use`; don’t keep a count of your own. - **Anything a player mustn’t fake:** decide it on your server. A player can change what your game says it has, but not a receipt, a webhook or your server’s own reads. - A webhook can come more than once (its `x-p2-delivery` id is the same each time): handle each payment once. ### Receipts Every purchase hands your game a receipt for your server: a JWT typed `p2-buy`, signed RS256 with Player Two’s keys at [/.well-known/p2-keys.json](https://playertwo.plus/.well-known/p2-keys.json) (the same keys as identities and launch tickets), good for an hour. Check it yourself, or send it to `POST /api/v1/sdk/server/store/receipt`. After its hour, read the charge or what the player has instead. _What a receipt says_ | Claim | What it is | | --- | --- | | `iss` | `https://playertwo.lol` | | `aud` | Your game's slug: a receipt for another game is refused (wrong-game). | | `typ` | `p2-buy`: an identity is never a receipt, and a receipt never an identity. | | `sub` | The player's id for your game. | | `kind` | A product’s kind, or `charge`. | | `key` | A product's key, or a charge's id. | | `n` | How many it gave: a consumable's number, 1 otherwise. | | `count` | A consumable's: how many the player has now. | | `ref` | A charge's: your reference, as your server gave it. | | `pay` | The payment's id; null for a test. | | `test` | A test purchase: nothing was paid. | | `sid` | The play session it was bought in. | | `iat · exp` | When it was signed, and an hour later (seconds). | The check answers `{ valid: true, player: { id }, product: { key, kind, n }, count?, payment, amount, test, at, exp }`, for a charge `{ valid: true, player: { id }, charge: { id, name, ref }, payment, amount, test, at, exp }`, or `{ valid: false, code, why }` with code `malformed`, `unknown-key`, `bad-signature`, `expired`, `wrong-game`, `not-paid` or `not-a-receipt`. ### Your server's calls Each with your game key (`Authorization: Bearer p2g_...`, made in your studio). The player is `player`, their id for your game, or their session token in `x-player-session`. Make a charge ``` curl -X POST https://playertwo.lol/api/v1/sdk/server/store/charges \ -H "Authorization: Bearer p2g_..." -H "content-type: application/json" \ -d '{ "player": "", "name": "Season pass", "price": 9.99, "ref": "season-4" }' # 201 { charge } (404 unknown-player, 409 no-game or not-in-store, 422 bad-name or bad-price) ``` Everything a player has paid for through your charges ``` curl "https://playertwo.lol/api/v1/sdk/server/store/charges?player=" -H "Authorization: Bearer p2g_..." # { player: { id }, charges: [{ id, name, price, ref, state: "paid", payment, paid, ... }], next, test } # newest first; ?state=paid (if not said), open, cancelled, expired or all; ?limit= up to 100; ?before= for the page after ``` Read a charge ``` curl https://playertwo.lol/api/v1/sdk/server/store/charges/ -H "Authorization: Bearer p2g_..." # { charge: { id, name, about, price, ref, player: { id }, state, test, payment, created, expires, paid } } (404 unknown-charge) ``` Withdraw a charge ``` curl -X POST https://playertwo.lol/api/v1/sdk/server/store/charges//cancel -H "Authorization: Bearer p2g_..." # { charge: { ..., state: "cancelled" } } (409 paid; asked again, the same answer) ``` What a player has of your products ``` curl "https://playertwo.lol/api/v1/sdk/server/store?player=" -H "Authorization: Bearer p2g_..." # { player: { id }, owned: [{ key, kind, count?, test? }], test } (test purchases count if their latest session is a test) ``` Use up a player's consumable ``` curl -X POST https://playertwo.lol/api/v1/sdk/server/store/use \ -H "Authorization: Bearer p2g_..." -H "content-type: application/json" \ -d '{ "player": "", "key": "dunk-boost", "n": 1 }' # { player, key, used, count } (409 not-enough with their count, 409 not-consumable, 404 unknown-player) ``` Check a receipt your client sends ``` curl -X POST https://playertwo.lol/api/v1/sdk/server/store/receipt \ -H "Authorization: Bearer p2g_..." -H "content-type: application/json" \ -d '{ "receipt": "" }' # { valid, player: { id }, product | charge, payment, amount, test, at, exp } ``` ### The purchase webhook Your studio’s [webhooks](https://playertwo.plus/docs/webhooks) hear every purchase as the event `purchase`, test ones too, signed as every event is: a charge paid ``` { "event": "purchase", "at": "2026-11-05T13:20:00.000Z", "game": { "slug": "your-game", "title": "Your Game", "url": "https://playertwo.lol/game/your-game" }, "amount": 4.99, "charge": { "id": "", "name": "5,000 coins", "ref": "order-8812" }, "player": "", "payment": "", "test": false } ``` a product bought ``` { "event": "purchase", "at": "...", "game": { ... }, "amount": 1.99, "product": { "key": "dunk-boost", "kind": "consumable", "name": "Dunk boost", "qty": 10 }, "player": "", "payment": "", "test": false } ``` A test purchase has `"test": true`, `"amount": 0` and no payment: leave it out of your accounts. ### Testing - You, your team and your testers play in test sessions (`p2.test`), and buy and pay for nothing: the card says it’s a test, the receipt and webhook say `test: true`, and nothing is paid or split. - Products bought as a test are seen only by test sessions. A real purchase takes a test one’s place. A test can’t add to real consumables (`owned`). A player’s charges paid as tests are listed only while their latest session is a test. - Before your game is out, only test sessions can buy or pay. - Building without Player Two, the stand-in takes `local: { products: [{ key, kind, name, price, qty }] }` and buys at once, as a test. `p2.store.pay(id)` pays any id at once, as a test (the stand-in can’t know your server’s charges, so its name is empty). ### The money - You get 80% of every in-game purchase, products and charges alike, less your backers’ share if your game raised with a share of its sales; Player Two keeps 20%. A $4.99 charge: $0.998 Player Two’s, $3.992 yours. Add-ons on your game’s page are split as its sales (10% Player Two’s). - Players pay from their Player Two balance, in USDC on Base; Player Two pays the network fees. It’s paid out with your game’s sales: claim it in your studio. Your studio shows what each product sold and made, and your charges paid and what they made. - In-game purchases aren’t refunded. A payment that lands on something the player already has (a skin bought twice at once) or on a charge you withdrew goes straight back to their balance. - Nothing is sold to a guest: every store call rejects `guest` in a card on X. - On a computer, in the Player Two app, buying isn’t there yet: `buy` and `pay` reject `no-store`. Your server can still read what a player has and use up their consumables. ### Refusals _Codes the store's calls reject with_ | Code | Status | From | What it means | | --- | --- | --- | --- | | `no-store` | 409 | buy, pay | Not in Player Two's player on the web (the app on a computer, or a page without Player Two). | | `busy` | 409 | buy, pay | Player Two's card is open already. | | `guest` | 403 | every call | A guest, in a card on X: nothing is sold to them. | | `no-game` | 409 | buy, pay, a charge | The player doesn't have the game yet (a free trial isn't having it). | | `unknown-product` | 404 | buy | Nothing on sale by that key (or it's retired). | | `owned` | 409 | buy | A skin, item or mod they have; for a test, real consumables. | | `unknown-charge` | 404 | pay, a charge | No charge by that id for this player in this game. | | `expired` | 409 | pay | Not paid within 30 minutes: make a new one. | | `withdrawn` | 409 | pay | Your server cancelled it. | | `paid` | 409 | cancel | It's paid: it can't be withdrawn. | | `not-in-store` | 409 | a charge | The game isn't in the store (archived, taken down, or a raise without a price). | | `bad-name · bad-price` | 422 | a charge | A name of 2 to 60 characters; a price of $0.10 to $500. | | (none) | 400 | your server | A field of the wrong type or too long (a charge's about over 280, its ref over 200): the answer's issues say which. | | `unknown-player` | 404 | your server | No player of this game has that id. | | `bad-state` | 422 | a player's charges | state is paid, open, cancelled, expired or all. | | `not-enough · not-consumable` | 409 | use | They have fewer (count says how many); or it isn't a consumable. | | `timeout` | | buy, pay | No answer in 20 minutes. | | `offline` | | buy, pay | Player Two couldn't be reached. | | `busy` | 429 | every call | Past the limits below: wait a moment (your server gets a plain 429). | ### Limits _How often, and how much_ | What | Limit | | --- | --- | | Reading (list, owned, a charge) | 240 a minute per session | | Receipts | 60 a minute per session | | Test buys | 60 a minute per session | | Charges made | 60 a minute for one player, 1,200 a minute for the game | | A player's charges read | 240 a minute for one player, 100 a page | | Consumables used | 600 a minute for one player | | A game key | 1,200 requests a minute | | Products | 100 a game; $0.99 to $500 | | Charges | $0.10 to $500; open 30 minutes; paid once | ## On a computer ### The local bridge The Player Two app starts your game with three settings in its environment: `P2_BRIDGE` (a local address with a secret, alive while the game runs), `P2_TICKET` (its launch ticket) and `P2_GAME` (its address on Player Two). The same calls go to the bridge as JSON over HTTP: `POST ${P2_BRIDGE}/call` with `{ "method": "trophies.unlock", "args": ["first-win"] }`, answered as on the web. A game made with web technology on a computer passes the address to `init({ bridge })`, or sets `P2_BRIDGE` on `globalThis`. ### Launch tickets Each time the app starts a game it asks for a launch ticket: a JWT signed RS256 by Player Two, naming the player (their id for your game), the game, the build, the computer and when it runs out, 14 days on, so the game starts offline for two weeks. Player Two’s public keys are at [/.well-known/p2-keys.json](https://playertwo.plus/.well-known/p2-keys.json). Check the ticket and refuse to start without one, or mark your game DRM-free. ### Checking tickets in your engine The helpers for Unity (C#), Godot (GDScript) and plain HTTP come with the Player Two app; this part will show how to use them. ## The requests underneath Each call becomes one request to Player Two, made by the player’s host with the play session (`Authorization: Bearer p2s_...`). They answer any site, without cookies. _Calls and their requests_ | Call | Request | | --- | --- | | me() | `GET /api/v1/sdk/me` | | trophies.list() | `GET /api/v1/sdk/trophies` | | trophies.unlock(key) | `POST /api/v1/sdk/trophies/` | | stats.get() · set · add | `GET · POST /api/v1/sdk/stats { set } \| { add }` | | scores.submit · list | `POST · GET /api/v1/sdk/boards/` | | saves.list() | `GET /api/v1/sdk/saves` | | saves.get · put · delete | `GET · PUT · DELETE /api/v1/sdk/saves/` | | presence.set(line, { join }) | `POST /api/v1/sdk/beat { status, join }` | | identity() | `GET /api/v1/sdk/identity` | | invite(params) | `POST /api/v1/sdk/invite { params }` | | loading.done · gameplay.start · stop | `POST /api/v1/sdk/event { event }` | | rooms.join · create · party | `POST /api/v1/sdk/rooms/ticket { mode, size, code, create, party }` | | addons.list() · addons.has(key) | `GET /api/v1/sdk/addons · GET /api/v1/sdk/addons/` | | store.list() · store.owned() | `GET /api/v1/sdk/store · GET /api/v1/sdk/store/owned` | | store.buy(key) | Player Two’s card, then `POST /api/v1/sdk/store/receipt { payment }` (a test: `POST /api/v1/sdk/store/test { key }`) | | store.pay(id) | `GET /api/v1/sdk/store/charges/`, Player Two’s card, then `POST /api/v1/sdk/store/receipt { payment }` (a test: `POST /api/v1/sdk/store/test { charge }`) | | items.list() | `GET /api/v1/sdk/items` | --- --- title: "Travel and avatars · Player Two" url: https://playertwo.plus/docs/sdk/travel description: "Player Two's SDK for travel and avatars: dress your game's character as the player's own critter, and send players and their party to other games on Player Two (playertwo.lol) through doors in your game." source: Player Two (https://playertwo.plus), made from the page itself --- Documentation · Game SDK · Travel # Travel and avatars Every player on Player Two has a critter, the same one everywhere. `p2.avatar()` gives it to your game, as pixels and as a 3D model, so your character can be theirs. `p2.travel` puts doors in your game: through one, the player (and their party) goes on to another game on Player Two, or back to the Plaza. Both are opt-in: a game that uses neither is as it was. Back to [the game SDK](https://playertwo.plus/docs/sdk). 1. **Your game opens a door** `p2.travel.to("hoop-it")`: its last saves go up, as they do when the player closes it. 2. **Player Two swaps the game** In the same player, with no new page: the next game opens, given where the player came from. 3. **The party follows** Members playing with the leader are asked to follow, and the party is called to the game. ## The avatar `p2.avatar()` says who is playing, to dress your character with: their name and level, their critter, and what they wear. It never says their email or anything about their balance, as nothing in the SDK does. ``` const me = await p2.avatar(); // { // id: "Xq3...", handle: "amber", name: "Amber", level: 7, picture: null, // critter: { face: "amber", size: 18, tile: "#1c1a26", // pixels: [[6, 3, "#f2c14e"], [7, 3, "hsl(40, 85%, 62%)"], ...], // model: "https://playertwo.lol/api/v1/critters/amber.glb" }, // worn: [], // } ``` - **The critter** is drawn from its `face` (their handle, unless they picked another): the same name always gives the same critter, on every page of Player Two and in every game. Its `pixels` sit in an 18 by 18 box, from its top left; colours are CSS. - **`worn`** is what they have on from the shop in the Plaza: each `{ key, name, slot, model, picture }`, the picture an SVG of it, the model none yet. - Ask once a session: the critter only changes when the player picks another face. ### Drawing the pixels A 2D game ``` // the critter on a canvas, crisp at any size: one rectangle a pixel function drawCritter(ctx, critter, x, y, size) { const u = size / critter.size; for (const [px, py, colour] of critter.pixels) { ctx.fillStyle = colour; ctx.fillRect(Math.round(x + px * u), Math.round(y + py * u), Math.ceil(u), Math.ceil(u)); } } drawCritter(ctx, me.critter, 24, 24, 72); ``` ### What they have from the shop `p2.items.list()` lists everything the player has from Player Two’s shop in the Plaza (avatars, skins, wearables, emotes, effects), each with whether they have it on, so your game can dress its own character in them or let them choose. ``` const items = await p2.items.list(); // [{ key: "halo", name: "Halo", kind: "wear", tier: "legendary", slot: "head", worn: true, number: null, // picture: "https://playertwo.lol/api/v1/items/halo/picture" }, ...] const hat = items.find((i) => i.worn && i.slot === "head"); ``` ### The 3D model `critter.model` is the same critter as the Plaza draws it, in 3D: a glTF binary (.glb), each pixel a cube 7 cm a side, standing on the origin and facing +Z, coloured by its vertices, a few thousand triangles. Anyone may fetch it, from anywhere, and it never changes, so cache it as long as you like. three.js ``` import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; const { scene: critter } = await new GLTFLoader().loadAsync(me.critter.model); critter.traverse((o) => { if (o.isMesh) o.material.vertexColors = true; }); critter.position.set(0, 0, 0); // it stands on the origin, facing +Z, about 0.9 m tall world.add(critter); ``` ## Travel ### Through a door `p2.travel.to(slug, { params, place })` sends the player to another game by its address on Player Two (playertwo.lol/game/_slug_). `p2.travel.home()` sends them back to the Plaza. Each resolves as the player goes: Player Two’s player sends your game’s last saves up, closes it, and opens the next game in its place. Your game ``` // a door in your lobby: the player goes on to Hoop It, and their party with them doorButton.onclick = () => p2.travel.to("hoop-it", { params: { mode: "pickup" }, place: "court" }); // back to the Plaza, where every game on Player Two has its screen homeButton.onclick = () => p2.travel.home(); ``` - The game gone to plays as it would from its page: a free game they don’t have yet asks to be added, and a paid one they don’t own offers its demo, or Buy. - `params` are invite params: up to 16 strings, 512 bytes in all, with `from` and `place`. - Refused: `no-game` (there is none at that address), `not-out`, `not-playable` (it isn’t played in Player Two’s player), `same-game`, `bad-params`, and `no-travel` where there is nowhere to send the player (below). ### Arriving The game reached gets your params as `p2.params`, with `from`, your game’s address, and `place`, if you named one. A game without the SDK finds them in its address’s query, as it finds a join link’s. The game reached ``` const p2 = await PlayerTwo.init(); if (p2.params?.from) { // came through another game's door: p2.params.from is its address on Player Two toast(`Welcome from ${p2.params.from}`); if (p2.params.place === "court") goToCourt(); } ``` > Treat `p2.params` as you treat anything in an address: anyone can make a link with any params. ### A door menu `p2.travel.links()` fills a menu of doors: the Plaza, then up to 12 games, in this order: where the party’s leader is, the games the player’s friends are in now, the ones they played lately, then the busiest. Only games played in Player Two’s player, out and listed, never yours, never one for adults unless the player said they are 18 or older, nor one with a label they hide. ``` const { home, links } = await p2.travel.links(); // home: { title: "The Plaza", url } // links: [{ slug, title, tagline, art, players, friends: [{ handle, name }], why, free, owned }, ...] menu.add(home.title, () => p2.travel.home()); for (const d of links) menu.add(d.title, () => p2.travel.to(d.slug), { art: d.art, note: d.friends.length ? `${d.friends[0].name} is here` : `${d.players} playing` }); ``` ### The party A party moves as one, as it does in the Plaza. When its leader goes through a door, every member playing with them is asked, at once, in Player Two’s own bar over the game: “_Amber_ went into Hoop It · Follow · Not now”. Following, they land where the leader is, with what the leader set friends to join them with there (`p2.presence.set(text, { join })`), and `p2.params.from` their own last game. Members elsewhere on Player Two are called to the game, as the leader’s “Join my game” calls them. Nothing in your game is needed for this. ### Where it works - **In Player Two’s player on the web**, for a game Player Two hosts or serves, or one on its own site with [Connect](https://playertwo.plus/docs/sdk/connect) on. - **In the Player Two app** on a computer, and anywhere else: `travel.to` and `travel.home` reject with `no-travel`. Show your doors only when `PlayerTwo.env` is `"p2"`, and hide them if one is refused. - **On localhost**, the stand-in says where it would have gone and stays. ## Played with recently A player’s profile lists who they played with in the last 14 days, to find them again, and the Party tab of the Plaza’s chat shows those of them who are in the Plaza now (with Walk to, Add friend and Message). Your game adds to it by being played: - **Rooms:** players in the same room for a minute or more ([p2.rooms](https://playertwo.plus/docs/sdk#rooms)). The SDK says who, once a minute; only players in the same game now count. - **Parties:** members of the same party in your game at the same time. - **Your own server’s matches:** report them (below). A player who keeps their activity private is never shown on anyone’s list; blocked players are never shown to each other; guests are never counted. ### Match reports A game with its own servers reports a match when it ends, with its game key (your studio: Game keys). Name each player by their id for your game (the identity’s `sub`, as you name them for trophies: a player of your game in the last six hours), or by the identity their client sent you (`p2.identity()`, taken up to six hours past its time), or by their play session’s token. Each one’s list gets the others. Your server ``` // your game's server, when a match ends: who was in it, by their ids for your game (the identity's sub) await fetch("https://playertwo.lol/api/v1/sdk/server/match", { method: "POST", headers: { authorization: `Bearer ${process.env.P2_GAME_KEY}`, "content-type": "application/json" }, body: JSON.stringify({ id: match.id, // counted once minutes: Math.ceil(match.ms / 60000), players: match.players.map((p) => ({ id: p.p2 })), // or { token }: an identity a client sent you }), }); // { counted: 6, players: [{ id, counted: true }, { id, counted: false, why: "guest" }, ...] } ``` ## Trophies across games Two of Player Two’s own trophies are earned across games, and count toward the player’s level like every other. They show on a profile once earned, never before. _Player Two's trophies across games_ | Trophy | How | | --- | --- | | Through five portals | Go through a door (p2.travel.to), one of the Plaza's screens, or after the party's leader, into five different games. | | Three games with one party | Play three different games with the same party, at the same time as another of its members. | ## Reference _The calls and their requests_ | Call | Request | Answer | | --- | --- | --- | | p2.avatar() | `GET /api/v1/sdk/avatar` | `{ id, handle, name, level, picture, critter, worn }` | | p2.items.list() | `GET /api/v1/sdk/items` | `{ items }` | | p2.travel.to(slug, { params, place }) | `POST /api/v1/sdk/travel { to, params, place }` | `{ to, params, unlocked }` | | p2.travel.home() | `POST /api/v1/sdk/travel { home: true }` | `{ to }` | | p2.travel.links() | `GET /api/v1/sdk/travel` | `{ home, links }` | | p2.params.from, .place | In the player's answer to the game's hello | Strings, or none | | A critter's model | `GET /api/v1/critters/{name}.glb` | model/gltf-binary | | Your server: report a match | `POST /api/v1/sdk/server/match { players, minutes, id }` | `{ counted, players }` | Every route is in the [API spec](https://playertwo.plus/openapi.json). --- --- title: "Player Two Connect · Player Two" url: https://playertwo.plus/docs/sdk/connect description: "Player Two Connect, for a web game on its own site played inside Player Two's player (playertwo.lol): the game SDK, and a signed player identity your servers can trust, so your own accounts, progression and multiplayer carry over." source: Player Two (https://playertwo.plus), made from the page itself --- Documentation · Game SDK · Connect # Player Two Connect Your game is played on its own site, inside Player Two’s player. Turn Connect on and it gets the same [game SDK](https://playertwo.plus/docs/sdk) as a hosted game, plus a signed identity your own servers can trust, so your accounts, progression and multiplayer key off the Player Two player. Served through Player Two ([Hosting](https://playertwo.plus/docs/hosting#follow)), your game has all of this already, with nothing to turn on or add: only your own accounts need the one sign-in route on your server ([below](https://playertwo.plus/docs/sdk/connect#accounts)). Back to [the documentation](https://playertwo.plus/docs). 1. **Your game says hello** In Player Two's player, the SDK finds Player Two around your game and says hello to it, and only to it. 2. **Player Two checks it's your game** Only a hello from your game's frame, from the address it plays at, is answered, and only there. 3. **Your servers know who it is** A short-lived token signed by Player Two: `p2.identity()`. Your accounts, rooms and progress key off it. ## Turning it on **Served through Player Two?** Skip this: Player Two puts the SDK in your pages and answers your game at its address on playertwo.cc ([Hosting](https://playertwo.plus/docs/hosting#follow)). Connect is for a site that plays itself in the player. A new game on its own site has **Player Two Connect** on from the start; it’s in your studio, on your game’s card, to turn off or on again. Player Two talks to your game at the origin of the address where it plays (its “Where it plays”), and nowhere else; change that address, and the origin follows. Add one line to your game’s page and call `PlayerTwo.init()`, as a hosted game does: index.html ``` ``` With Connect on, everything in your studio’s services works for your game as for a hosted one: trophies, leaderboards, game keys and test mode, and in the game every call of [the SDK](https://playertwo.plus/docs/sdk#calls). ### Where it works - **In Player Two’s player** (`PlayerTwo.env` is `"p2"`): your game runs in a frame, at its own address. Players get there from your game’s page on Player Two, a join link, or a friend’s Join. - **At its own address, or framed by any other site** (`"disabled"`): `init()` rejects with `not-on-player-two` at once, and your game runs as it always has, with your own sign-in. - **On localhost** (`"local"`): the stand-in answers every call from memory. Its `p2.identity()` is an unsigned token (`alg` “none”, `iss` “local”) that no check against Player Two’s keys accepts; its `p2.invite()` is a link to your own page with `?p2join=`, which it reads back as `p2.params` (or give it `init({ local: { params } })`). - The address where it plays must be https. Only an address on your own computer (localhost, 127.0.0.1) may be http, to try a build there. ## The handshake Your game and Player Two’s page find each other with `postMessage`, each checking who the other is, and then talk over a private channel: - The SDK says hello only to a Player Two page: playertwo.lol, demo.playertwo.lol, or the Player Two the SDK file came from. It reads the page around it from `location.ancestorOrigins`, or from `document.referrer` where a browser has no such list, and addresses its hello to exactly that origin. Framed by anything else, it says nothing. - Player Two’s page answers a hello only when it comes from your game’s own frame (`event.source`) and from the origin your game plays at (`event.origin`), and addresses every answer to that origin, never to `"*"`. A page your game moves to at another origin isn’t answered; the console says why. - Its answer carries who is playing (`p2.player`), the params of the invite they came by (`p2.params`), and a `MessageChannel` port. Every call after that goes over the port, each with its own nonce. The play session’s token stays in Player Two’s page: your game never holds it, and doesn’t need it. - When there’s no session to give, `init()` rejects with a code: `signed-out` (the player isn’t signed in), `not-yours` (they haven’t added or bought it), `not-connected` (Connect is off). Play on as a guest. - The frame may go full screen, use gamepads, play sound, open popups (outside the sandbox), write to the clipboard and share. It can’t send Player Two’s page anywhere. ## The identity `p2.player` tells your game’s client who is playing, and the client is the player’s: anything in it can be changed. For your servers, ask Player Two to sign it: ``` const token = await p2.identity(); // a JWT, good for ten minutes: ask again for a fresh one // send it to your server, which checks it (below) before it trusts who it names ``` It is a JWT signed RS256 with Player Two’s key. The public keys are at [/.well-known/p2-keys.json](https://playertwo.plus/.well-known/p2-keys.json): find the one named by the token’s `kid` (while Player Two changes keys, the set has both). Hosted games can ask for one too. ### What it says _An identity's claims_ | Claim | What it is | | --- | --- | | `iss` | `https://playertwo.lol` | | `aud` | Your game's slug, its address on Player Two (playertwo.lol/game/). An identity for another game is no identity for yours. | | `sub` | The player's id for your game: the same every time, different in every other game. Player Two's server calls name players by it. | | `typ` | `p2-id`. Launch tickets and licenses are signed with the same key: check the type, and neither ever passes. | | `h`, `name`, `pic`, `lvl` | Their handle, their name, their picture's address (or null) and their level, as their public profile shows them. | | `kind` | `full` (the game is theirs), `demo`, or `test` (you, or Player Two’s admins). | | `sid` | The play session it came from. | | `iat`, `exp` | When it was made, and when it runs out: ten minutes on. | > Check the signature, `iss`, `aud`, `typ` and `exp` every time. Player Two never tells a game a player’s email or anything about their balance, and neither does the identity. ### Checking it with jose Node, Deno, Bun, workers: npm install jose ``` import { createRemoteJWKSet, jwtVerify } from "jose"; const PLAYER_TWO = createRemoteJWKSet(new URL("https://playertwo.lol/.well-known/p2-keys.json")); /** The Player Two player behind an identity your game's client sent; throws if it isn't good. */ export async function playerTwo(token) { const { payload } = await jwtVerify(token, PLAYER_TWO, { issuer: "https://playertwo.lol", audience: "your-game", // your game's address on Player Two: playertwo.lol/game/your-game algorithms: ["RS256"], }); if (payload.typ !== "p2-id") throw new Error("That isn't a Player Two identity."); return { id: payload.sub, handle: payload.h, name: payload.name, picture: payload.pic, level: payload.lvl, kind: payload.kind }; } ``` ### With node:crypto No dependency ``` import { createPublicKey, createVerify } from "node:crypto"; const KEYS = "https://playertwo.lol/.well-known/p2-keys.json"; let keys = [], fetched = 0; /* Player Two's public keys, fetched again after an hour, or for a key it doesn't know yet (at most once a minute) */ async function keyFor(kid) { const age = Date.now() - fetched; if (age > 3_600_000 || (!keys.some((k) => k.kid === kid) && age > 60_000)) { keys = (await (await fetch(KEYS)).json()).keys; fetched = Date.now(); } const k = keys.find((x) => x.kid === kid); return k ? createPublicKey({ key: { kty: k.kty, n: k.n, e: k.e }, format: "jwk" }) : null; } /** The Player Two player behind an identity for `game` (your game's address on Player Two); throws if it isn't good. */ export async function playerTwo(token, game) { const [head, body, sig] = String(token).split("."); const h = JSON.parse(Buffer.from(head ?? "", "base64url").toString()); const key = h.alg === "RS256" ? await keyFor(h.kid) : null; if (!key || !sig || !createVerify("RSA-SHA256").update(`${head}.${body}`).verify(key, Buffer.from(sig, "base64url"))) { throw new Error("Player Two didn't sign that."); } const c = JSON.parse(Buffer.from(body, "base64url").toString()); if (c.typ !== "p2-id" || c.iss !== "https://playertwo.lol" || c.aud !== game) throw new Error("That isn't an identity for this game."); if (c.exp * 1000 <= Date.now()) throw new Error("That identity has run out."); return { id: c.sub, handle: c.h, name: c.name, picture: c.pic, level: c.lvl, kind: c.kind }; } ``` ### Or ask Player Two If you’d rather not check JWTs, your server asks with its game key (your studio: Game keys). The answer also says whether the player is still on Player Two, and whether the session is still being played. ``` curl -X POST https://playertwo.lol/api/v1/sdk/server/identity \ -H "Authorization: Bearer p2g_..." -H "content-type: application/json" \ -d '{ "token": "" }' # good: { "valid": true, "player": { "id", "handle", "name", "picture", "level" }, # "kind": "full", "game": { "slug" }, "session", "live": true, "exp": 1792557000000 } # not good: { "valid": false, "code": "expired", "why": "...", "player": null, "kind": null } ``` ## Carrying accounts over ### One more login Treat a Player Two identity as one more way to sign in, like a Google or a Privy one: your server checks it, then finds the account linked to the login `p2:`, or links one. A guest who played before signing in is folded into it, as a guest is into any login’s account. Everything your accounts already hold (progression, items, currency) comes with it. Your server ``` // your server: the game's client sends its identity (and the guest token it kept in the frame, if any) app.post("/auth/player-two", async (req, res) => { const p2 = await playerTwo(req.body.token); // throws: answer 401 const login = `p2:${p2.id}`; // one more login, like a Google or Privy one const guest = req.body.guest ? accounts.byToken(req.body.guest) : null; let account = accounts.byLogin(login); if (!account) { // a first sign-in: the guest who has been playing becomes the account, or a new one starts account = guest && !guest.logins.length ? guest : accounts.create({ name: p2.name }); accounts.link(login, account); } else if (guest && guest !== account && !guest.logins.length) { accounts.fold(guest, account); // as a guest is folded into any login's account } res.json({ session: accounts.sessionFor(account) }); // your own session, used as it is today }); ``` The id is per game: the same player has a different `sub` in each of your games. Link each game’s own. ### Games that use Privy If your game signs players in with Privy, Privy can take the Player Two identity as a sign-in of its own, so your servers go on checking Privy’s tokens as they do today and need nothing new. Privy turns this on for an app on request: its **Custom authentication**. - **In Privy’s dashboard**: ask for Custom authentication (Integrations, Built-in). Then, under JWT-based authentication: the JWKS endpoint `https://playertwo.lol/.well-known/p2-keys.json`, the user’s id in `sub`, and client-side requests. Where Privy asks for the audience, give your game’s address on Player Two (its slug): Player Two signs launch tickets and licenses with the same key. - **Allowed origins**: add `https://playertwo.lol` and the address your game plays at inside the player (`https://your-game.playertwo.cc`, or your own site’s). Privy’s frame refuses a page around it that isn’t on the list. - **In your game**: hand Privy the identity, inside Player Two only. Your game, in React ``` // inside your PrivyProvider: in Player Two's player, Privy signs the player in with their Player Two identity; // anywhere else (p2 is null) Privy's own sign-in works as always. Inside, don't call Privy's login(). import { useSubscribeToJwtAuthWithFlag } from "@privy-io/react-auth"; export function PlayerTwoSignIn({ p2 }) { // p2: what PlayerTwo.init() gave, or null useSubscribeToJwtAuthWithFlag({ enabled: !!p2, isAuthenticated: !!p2, isLoading: false, getExternalJwt: async () => (p2 ? await p2.identity() : undefined), }); return null; } ``` A Player Two player is then a Privy user of their own, known by their `sub` for your game, the same every time. To make them the same as an account they already have on your site, link it once there with Privy’s `useLinkJwtAccount().linkWithCustomJwt(token)`, given an identity, as the [linking popup](https://playertwo.plus/docs/sdk/connect#accounts-popup) below does. Without Custom authentication, use the server’s login above: it works with any sign-in, Privy’s included. ### Browser storage in the frame In Player Two’s player your game is a frame from another site, so the browser gives it a storage box of its own, apart from your site’s. Chrome, Edge and Firefox keep a framed site’s storage by the page around it; Safari blocks its cookies, and clears what it keeps after seven days without a visit. So a player who has played on your site arrives in the frame with nothing your site kept in their browser (a guest token in `localStorage`, a cookie), and a sign-in that redirects (Google, X, Discord) breaks: those pages refuse to be framed, and the frame can’t send Player Two’s page away. - **Keep progress on your server**, keyed by the account, and sign the player in with their identity. Then nothing depends on the browser. - **Or in Player Two’s cloud saves** (`p2.saves`, 8 slots of 1 MB), which follow the player wherever they play. - **The frame’s own storage** still keeps what can be lost (settings, a cache), for your game inside Player Two only. - **A sign-in of your own** inside the frame opens in a popup on a click (`window.open`); a redirect never works there. - **For players whose account lives in their browser on your site**: a one-time linking popup. ### Linking a browser's account A popup is a page of its own, so on your site it sees your site’s storage. The game in the frame opens one with the player’s identity; the popup sends it, with the account the browser kept, to your server, which links the two once. In the frame ``` // in the frame, on a click (browsers block a popup opened any other way) linkButton.onclick = async () => { const token = await p2.identity(); window.open(`https://yourgame.com/link-player-two#token=${token}`, "link", "popup,width=480,height=640"); }; ``` The popup, on your site ``` // https://yourgame.com/link-player-two: a page of your own site, top level, so it sees your site's storage const token = new URLSearchParams(location.hash.slice(1)).get("token"); history.replaceState(null, "", location.pathname); // the token leaves the address bar const kept = localStorage.getItem("account-token"); // the account this browser kept on your site await fetch("/auth/player-two/link", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token, account: kept }), // the server links p2: to that account, once }); window.close(); // the game in the frame signs in again: it's them ``` The server checks the identity as above, finds the kept account by its token, and links `p2:` to it (or folds it into the account the login has already). From then on the player is the same person in the frame and on your site. ## Multiplayer Your servers and rooms stay yours. A player gets into them one of two ways: - **Your own session, as today.** Once the identity has signed the player into your accounts, pass your session to your rooms exactly as you do now. - **The identity itself.** Join with a fresh `p2.identity()` and check it in your room’s auth hook: the room knows who is joining, with no sign-in of yours. ``` // a Colyseus room: the player joins with a fresh identity, and the room checks it import { Room } from "colyseus"; export class Arena extends Room { async onAuth(client, options) { const p2 = await playerTwo(options.p2); // throws: the join is refused return { account: await accounts.byLogin(`p2:${p2.id}`), name: p2.name }; } } // the game's client, in Player Two's player const room = await colyseus.joinOrCreate("arena", { p2: await p2.identity() }); ``` An identity runs out ten minutes after it is made: ask for one for each join, and never keep it as the player’s session. Inviting friends into a room: [presence and invites](https://playertwo.plus/docs/sdk/connect#invites). No server of your own? Player Two’s rooms work here too: `p2.rooms.join()` seats your players in a room near them, and your pages talk through it ([Rooms](https://playertwo.plus/docs/sdk#rooms)). ## Trophies and leaderboards from your server With a game key your server gives trophies, server-only ones too, and sends scores to trusted boards, naming the player by their id for your game: the identity’s `sub`. Whatever your server decides (a match won, a rank) belongs there, never in the client. ``` const P2 = "https://playertwo.lol/api/v1/sdk/server"; const auth = { authorization: `Bearer ${process.env.P2_GAME_KEY}`, "content-type": "application/json" }; // a trophy your server decides (one marked "only my server can give it", or any other) await fetch(`${P2}/trophies/tournament-winner`, { method: "POST", headers: auth, body: JSON.stringify({ player: p2.id }) }); // a score on a trusted board: only your server sends its scores await fetch(`${P2}/boards/ranked`, { method: "POST", headers: auth, body: JSON.stringify({ player: p2.id, value: 1840 }) }); ``` In your studio, mark the trophies only your server decides “only my server can give it”, and make competitive boards trusted, so the game in the player’s hands can’t send them. [More on your game’s server](https://playertwo.plus/docs/sdk#server). ## Presence and invites ``` // a party's link, on Player Two (location.href in the frame is your own site's address, not Player Two's) const link = await p2.invite({ room: party.code, kind: "party" }); // https://playertwo.lol/game/your-game?join=eyJyb29tIjoiNDgyOTEzIiwia2luZCI6InBhcnR5In0 await navigator.clipboard.writeText(link); // friends who may see what the player plays get Join beside "Playing " p2.presence.set("In a party · 2 of 4", { join: { room: party.code, kind: "party" } }); p2.presence.set("In the menu"); // no join: Join goes // whoever opens the link, or presses Join, starts the game with the params const p2 = await PlayerTwo.init(); if (p2.params?.room) joinParty(p2.params.room, p2.params.kind); ``` - `p2.invite(params)` takes up to 16 strings, 512 bytes as JSON, and gives a link to your game’s page on Player Two. Share it instead of your own address: in the frame, `location.href` is your site’s. - Whoever opens it lands on your game’s page on Player Two and the game opens at once (after they add or buy it, if they must), with `p2.params` set. - `p2.presence.set(text, { join })` puts Join beside “Playing your game” in the player’s friends’ lists, opening the same link; `presence.set(text)` takes it away. - Only friends who may see what the player plays see the line and Join. A player who keeps their activity private shows neither. - Anyone can read a link’s params, or make a link: treat them as you treat anything in an address, never as proof. A party code is fine; a secret isn’t. ## Worked through: Impact Impact (impact.dude.work) is played on its own site and keeps its own accounts: a guest token in the browser’s storage, a Privy sign-in its server checks against Privy’s keys (issuer, audience, subject), and an account book that links login ids to accounts, finds an account by its login, and folds a guest into the account of the login it signs in with. Its progression (XP, level, currency, items, feats, missions) lives on its server for its server rooms; its multiplayer is Colyseus rooms that look the player up by the account token they join with; its parties are six-digit codes, shared as `#room=` links made from its own address. With Connect, Player Two is one more login, and very little changes: - **In the studio:** Connect on. Impact plays at impact.dude.work, the one origin Player Two answers. - **The client:** load the SDK; as it starts, in Player Two’s player, send `p2.identity()` (with the guest token kept in the frame) to a Player Two sign-in on its server, and keep the account token it answers, as after a Privy sign-in. Read `p2.params` as it reads `#room=`. - **The server:** check the identity as it checks Privy’s (RS256 from Player Two’s keys instead of ES256 from Privy’s), link `p2:` in the account book, and fold the guest in. - **Rooms:** nothing changes. The account token joins as it does today. - **Invites:** inside Player Two, make party links with `p2.invite`, and set presence with the party’s code as `join`. - **What the browser keeps** (arcade progress, match history): to the server, or to `p2.saves`; and a linking popup for players with progress on impact.dude.work. - **Trophies and leaderboards** (it has none yet): from its server with a game key, naming players by their `sub`. The client ``` // as the game starts const p2 = await PlayerTwo.init().catch(() => null); if (p2) { // Player Two's player: sign in with the identity, bringing along the guest kept in this frame const r = await fetch(`${API}/auth/player-two`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ token: await p2.identity(), guest: localStorage.getItem(ACCOUNT_KEY) }), }); if (r.ok) localStorage.setItem(ACCOUNT_KEY, (await r.json()).session); // the account token rooms take, as today if (p2.params?.room) joinParty(p2.params.room, p2.params.kind); // as #room= is read today } // a party's invite: Player Two's link inside Player Two, the game's own link anywhere else const inviteLink = (code, kind) => (p2 ? p2.invite({ room: code, kind }) : Promise.resolve(`${location.origin}/#room=${code}&kind=${kind}`)); ``` The server, beside its Privy sign-in ``` // beside the Privy sign-in: the same three questions, asked of Player Two's keys // (issuer https://playertwo.lol, audience the game's slug, subject the player's id for it; RS256, typ "p2-id") app.post("/auth/player-two", async (req, res) => { let p2; try { p2 = await playerTwo(req.body.token); } catch { return res.status(401).json({ error: "Not a Player Two identity." }); } // then what a Privy sign-in does, with the login "p2:": the account linked to it (byLogin), // or the guest (or a new account) linked to it (link), and a guest folded into it const account = signIn(`p2:${p2.id}`, { guest: req.body.guest, name: p2.name }); res.json({ session: book.tokenOf(account) }); }); ``` ## Reference _Connect's calls and their requests_ | Call | Request | Answer | | --- | --- | --- | | p2.identity() | `GET /api/v1/sdk/identity` | `{ token, exp }` | | p2.invite(params) | `POST /api/v1/sdk/invite { params }` | `{ url, params }` | | p2.presence.set(text, { join }) | `POST /api/v1/sdk/beat { status, join }` | `{ ok }` | | p2.params | In the player's answer to the game's hello | The params, or null | | Your server: check an identity | `POST /api/v1/sdk/server/identity { token }` | `{ valid, player, kind, live, exp }` | _Codes Connect adds_ | Code | Status | What it means | | --- | --- | --- | | `not-connected` | 409 | The game is played on its own site and Connect is off. | | `moved` | 409 | The game's address changed since the page opened: open it again. | | `bad-params` | 422 | Invite params are up to 16 strings, 512 bytes. | | `malformed · unknown-key · bad-signature` | | Server check: not a token Player Two signed with a key it publishes. | | `expired · not-an-identity · wrong-game · gone` | | Server check: run out, a ticket or a license, for another game, or a player no longer on Player Two. | --- --- title: "Hosting a web game · Player Two" url: https://playertwo.plus/docs/hosting description: "Host a web game on Player Two (playertwo.lol): push a folder of up to 5 GB and 20,000 files and it plays at its own address on playertwo.cc with no code changes, or play it from your own site in Player Two's player." source: Player Two (https://playertwo.plus), made from the page itself --- Documentation · Hosting # Hosting Your web game plays in Player Two’s player either way. Hosted here, it gets its own address on playertwo.cc and needs no code changes; each update goes up with one command, which your deploy script can run. On your own site, your hosting stays yours and every update is live here at once: Player Two serves it through its own address, read from your site as it is, or your site plays in the player itself and [Connect](https://playertwo.plus/docs/sdk/connect) brings the rest. Back to [the documentation](https://playertwo.plus/docs). 1. **You push its folder** One command, or a zip. Only the files that changed go up. 2. **It gets its own address** `your-game.playertwo.cc`, with its build at the top and the SDK put in its page for you. 3. **Players play it here** In Player Two's player. Its saves follow them to any device; a paid game opens only for its owners. ## The two ways You choose when you submit the game, and can switch in your studio (its card, under **Where it plays**). Either way players find it on its page here and play it in Player Two’s player. _What each way gives_ | | On Player Two (recommended) | On your own site | | --- | --- | --- | | How it gets here | Upload a zip, or push the folder with one command. Up to 5 GB. | Paste its address. | | Updates | Each one goes up the same way, a zip or the command. Put the command in your deploy script and every release goes up by itself. | Nothing to send: whatever is live on your site is live here, on the next page load. | | Its address | Its own, on playertwo.cc | Its own on playertwo.cc, read from yours ([served through Player Two](https://playertwo.plus/docs/hosting#follow)), or yours | | Code changes | None. Player Two puts the SDK in its page. | None, served through Player Two. With that off, one script tag and Connect on. | | Saves | Work as in any browser, and follow players to any device. | Served through Player Two, the same. With that off, your site’s, though a frame keeps its own: see [Connect](https://playertwo.plus/docs/sdk/connect#accounts-storage). | | Selling it | Only its owners can play it. Nothing to build. | Your server checks who owns it. | | Trophies, leaderboards, friends, invites | Built in: a few SDK calls. | With Connect, the same calls. | | Serving it | Player Two's, from a CDN. | Yours. | ## Your site, served through Player Two A game on your own site can be served by Player Two from its own address, `https://your-game.playertwo.cc`, read from your site as it is: turn on **Serve it through Player Two** when you submit it, or in your studio (its card, under **Where it plays**). It is on for a new game on its own site. Nothing is copied and nothing needs sending: each page load reads your site as it is then, so every update you put live there is live here at once. - **What it gives**: what a game hosted here has, with nothing added to your site. Player Two puts the [game SDK](https://playertwo.plus/docs/sdk) first in each of its pages, so saves follow players to any device ([below](https://playertwo.plus/docs/hosting#saves)), and trophies, leaderboards, friends, invites, [rooms](https://playertwo.plus/docs/hosting#multiplayer) (with [your room code](https://playertwo.plus/docs/hosting#room-code), pushed on its own) and the [signed identity](https://playertwo.plus/docs/sdk/connect#identity) work. Its own address keeps its storage apart from every other game’s. - **How**: a page is read from your site at the same path and served with Player Two’s script first in its head. Every other file (scripts, models, sounds) comes from your site through the same address, as your site sends it, with its own caching. A redirect within your site is followed at the same path here. - **What it needs**: an https address with a name. Your own servers (an API, a multiplayer server) accept requests from `https://your-game.playertwo.cc`, as for a game hosted here ([your own servers](https://playertwo.plus/docs/hosting#code-servers)). - **What doesn’t go through**: only reading a file goes on to your site. A form posted to it, or an API at your site’s own address, needs your game to call your site by its full address (with CORS), or leave this off. Sign-in pages that redirect (Google, X, Discord) don’t work inside any frame: use [Player Two’s sign-in](https://playertwo.plus/docs/sdk/connect#accounts). - **With it off**: your site itself plays in Player Two’s player, at its own address. Updates are live at once too; for saves that follow players, trophies and Player Two’s sign-in, add one script tag and turn on [Connect](https://playertwo.plus/docs/sdk/connect). ## Code changes ### Hosted here: none Your export plays as it is. Its paths work as they do on any server, root-absolute ones too (`/assets/app.js`), and so do its own storage, workers, WebAssembly and audio. Player Two adds one script first in each of its pages, the [game SDK](https://playertwo.plus/docs/sdk) in auto mode, which finds Player Two around the game and keeps its saves going: what Player Two puts in your page's head ``` ``` If your game already loads the SDK and calls `PlayerTwo.init()`, nothing changes: it gets the same answer. A query on a file’s address (`app.js?v=3`) makes no difference to which file is sent. ### Your own servers A game hosted here talks to your servers (an API, a multiplayer server) from its own origin, `https://your-game.playertwo.cc`. Add it wherever your servers say which origins may call them: your CORS, and your WebSocket server’s check of the Origin header. allowing the game's address ``` // your API (Express with cors): the game calls it from its own address on Player Two app.use(cors({ origin: ["https://yourgame.com", "https://your-game.playertwo.cc"] })); // your WebSocket server: the same address, in its check of where a connection comes from const ALLOWED = new Set(["https://yourgame.com", "https://your-game.playertwo.cc"]); wss.on("connection", (socket, req) => { if (!ALLOWED.has(req.headers.origin)) socket.close(1008, "Not from the game"); }); ``` To know who is playing, your servers check a signed identity from `p2.identity()`, as a game on its own site does: see [the identity](https://playertwo.plus/docs/sdk/connect#identity). ### Trophies and leaderboards A few calls, once you have made the trophies and leaderboards in your studio. The game plays on without them anywhere else. in your game ``` PlayerTwo.init().then(async (p2) => { await p2.trophies.unlock("first-win"); // the player sees the toast await p2.scores.submit("time-attack", 83400); // a leaderboard you made in your studio }).catch(() => { /* played outside Player Two: the game carries on without them */ }); ``` ## Multiplayer A game hosted here can be multiplayer with no server of your own: **Player Two rooms** seat its players together, through the SDK it already has. It’s the choice a hosted game starts with, under **Multiplayer** when you submit it, and you can change it in your studio, under **Rooms**. A game that plays alone says **Single-player**; one with servers of its own says **Its own server**, and its servers learn who is playing from `p2.identity()` ([your own servers](https://playertwo.plus/docs/hosting#code-servers)). 1. Put the game on Player Two, with the submit form or `p2 push`. Hosted games get the SDK automatically. 2. Write `const room = await p2.rooms.join({ mode: "duel", size: 2 })`, then `room.send(...)` and `room.on("message:move", ...)`. For smooth movement, `room.sync(object)`. Private codes and party rooms are one call each. 3. Test with two browser tabs on localhost: no account, no server. 4. Push. The rooms just work, your studio shows rooms and players live, and a release never mixes versions in a room. to start, in your game ``` const p2 = await PlayerTwo.init(); const room = await p2.rooms.join({ mode: "duel", size: 2 }); room.send("move", { x: 3, y: 1 }); room.on("message:move", (move, from) => apply(from, move)); ``` **Scripted rooms**, for games where cheating matters: put a `room.js` at your build’s root, and it runs on Player Two’s servers, one copy per room and per build. It stays private: Player Two takes it out of the build as the build goes live, and never serves it to players. Joining, messages, the shared state, events and limits: [Rooms in the SDK docs](https://playertwo.plus/docs/sdk#rooms). ### Room code for a game on its own site A game on its own site that Player Two serves ([above](https://playertwo.plus/docs/hosting#follow)) has rooms as a game hosted here does, and its rooms can run your code too. Your site is read as it is, so the code doesn’t come from it: keep `room.js` (or your `room/` folder) off your site and push it on its own, from the folder that has it. Nothing else goes, and nothing about your site changes: in a terminal, or your CI ``` P2_AGENT_KEY=p2_agent_... node p2.mjs push dist --game your-game --room ``` It is kept as a build’s is: checked by its hash, private, never served to players. Rooms opened from then on run it; rooms open on the code before play on with theirs until they end. Push again when it changes: each push is its own version, and the one before goes a day later. A game Player Two keeps a build of carries its `room.js` in the build instead (`409 not-followed`). ## Saves A game hosted here keeps its saves where it always does, in the browser’s localStorage and IndexedDB, at its own address. Player Two keeps a copy of both in each player’s cloud saves and puts it back before the game runs, on any device they sign in on. No code needed. - **When it goes up**: a few seconds after the game writes (at most twice a minute), and at once as the player leaves or closes the game. Another device saving in between is answered by sending again on top of it: the newest write wins. At the start of a page load, the cloud’s copy wins. - **What stays on the device**: keys starting `p2_ignore` or `p2-ignore`, the keys and databases you list in your studio, databases named for a cache (Unity’s `UnityCache`), and a database holding files (Blobs), which can’t be copied. - **How much**: up to 2 MB of localStorage and 2 MB of IndexedDB a player. The largest keys and databases are left out until the rest fits, and your studio lists them. - **Your own saves**: `p2.saves` keeps named slots in the player’s cloud saves, from any game ([the SDK](https://playertwo.plus/docs/sdk)). Turn the copy off in your studio if you would rather keep saves that way only. ## Unity, Godot and SharedArrayBuffer ### Unity Export for WebGL and push the folder with `index.html` at its top. Compressed builds work as they are: a file ending `.br` or `.gz` is sent with its encoding, so Unity’s decompression fallback isn’t needed. PlayerPrefs and files under `Application.persistentDataPath` follow the player; Unity’s download cache stays on the device. ### Godot Export for the Web. Files under `user://` follow the player. Godot 4 with threads needs SharedArrayBuffer (below); since Godot 4.3, a build exported without threads needs nothing. ### SharedArrayBuffer A page has SharedArrayBuffer (threads in WebAssembly) only when it is cross-origin isolated. Turn **Cross-origin isolation** on in your studio: your game’s pages then say so in their headers (COOP and COEP, and Document-Isolation-Policy, which isolates the game’s frame on its own). Files your game loads from other sites then need CORS or a Cross-Origin-Resource-Policy header; choose **credentialless** to let plain ones load, without cookies. Chrome and Edge isolate the frame today. In other browsers SharedArrayBuffer isn’t there yet inside a frame, so export without threads where your engine can. ## Pushing a build The tool is one file, [p2.mjs](https://playertwo.plus/cli/p2.mjs), with nothing to install. It sends only the files Player Two doesn’t have yet, eight at a time, picks up where it left off if it is stopped, and makes the build live once every file has arrived. Players get the new build on their next page load; a page already open keeps its own to the end. in a terminal, or your CI ``` # once: the tool (Node 20 or later), and an agent key from your studio curl -fsSLO https://playertwo.lol/cli/p2.mjs # then, each time: the folder your engine exported P2_AGENT_KEY=p2_agent_... node p2.mjs push ./build --game your-game --version 1.4.0 ``` what it says ``` Hashing 2,914 files (412.6 MB)... Build 6f1d0c9a2b7e4f3a8c5d1e0b: sending 38 files (21.4 MB); 2,876 files Player Two has already. sent 38 of 38 files, 21.4 MB of 21.4 MB Live: https://your-game.playertwo.cc/ (build 1.4.0) ``` - `--entry`: the page the game opens at, if it isn’t `index.html`. - `--version 1.4.0`: what the build is called, in your studio (“Live build 1.4.0”), its list of builds, its testers’ notice and the version email its players get. Up to 32 characters: letters, digits, dots, dashes, plus signs and spaces, starting with a letter or a digit (`1.4.0`, `2.0 beta`, `v1.3.2+hotfix`); anything else is refused before anything is sent (and by Player Two, `422 bad-version`). Left out, the build is called by its number in the game’s builds: its first is 1, the next 2, and so on, every build counted once it is ready, never reused (builds from before versions were kept were numbered in the order they came). It needn’t be new or in order: it is a label. The same files pushed again with another version rename that build. Not with `--room`. - `--note "What changed"`: a few words on the build (up to 1,000 characters), shown with it in your studio. Add `--post` to put the note on the game’s Updates too, as a release post, once the build is live; its first line is the post’s title. - `--testers`: to the game’s testers first. Its players keep the build they have; you, your team and your testers play this one from its page, as a test, and each tester is told it’s up. Make it live from the list of its builds when it’s ready (or stop testing it). A newer one takes its place. For a game that is out; before its day, its testers play its build anyway. - Left out: `.git`, `.DS_Store` and the like, and symbolic links (it says which). - The game must be on Player Two first: submit it with a zip, or with its address on your site, then push. The push makes it hosted here. **Its builds**, in your studio under **Where it plays**, list every build it has played that is still kept: each for 30 days after another took its place, and the last 20 always. Each says its version (“Build 1.4.0”, or “Build 12” by its number), who sent it (you, or which agent key), its size, how many files changed against the build before, its commit and note, and how many players are on it now. **Try it** opens a build in a tab of its own without making it live; **Make live** rolls back (or forward) to it, and players get it on their next page load; **Name it** gives it a version, or another one (a GitHub commit’s build comes in by its number; nobody is told). The one out to its testers says **Testers**, with **Make it live** and **Stop testing**. An upload can go to them first too (**To testers first**, as you save the page with a new zip), and so can a linked repository’s commits (**GitHub pushes go to testers first**). Your agent can do the same with its key: the MCP tools list_builds, make_build_live and name_build, or `GET`, `POST` and `PATCH` (`{ key, version }`) `/api/v1/agent/games//web/builds`. A zip from the form takes a version in the field under it (named as the page saves), and an agent’s upload_web_build (`POST /api/v1/agent/builds`) takes `version` too, by the same rule. ### The three calls The tool makes three calls, which your own scripts can make too, with your agent key. First the list of files, each with its size and SHA-256; the answer names the build and the files to send, each with where to send it: 1. the list ``` POST https://playertwo.lol/api/v1/agent/games/your-game/web Authorization: Bearer p2_agent_... { "entry": "index.html", "version": "1.4.0", "files": [{ "path": "index.html", "size": 1841, "sha256": "9f2c...", "type": "text/html" }, ...] } 200 { "build": "6f1d0c9a2b7e4f3a8c5d1e0b", "upload": [{ "sha256": "9f2c...", "method": "PUT", "url": "https://...", "headers": { ... } }, ...] } ``` Then each of those files, as it is, to its `url` with its `method` and `headers`. The addresses last six hours; ask again with the same list for fresh ones (files already sent aren’t asked for again). Last, the build is checked and made live: 3. done ``` POST https://playertwo.lol/api/v1/agent/games/your-game/web/6f1d0c9a2b7e4f3a8c5d1e0b/done 200 { "live": true, "url": "https://your-game.playertwo.cc/", "version": "1.4.0" } 409 { "code": "missing", "missing": ["path", ...] } # some never arrived: ask again 422 { "code": "mismatch", "mismatched": ["path", ...] } # not what the list said: push again ``` ## From GitHub Link a public GitHub repository to a game, and every commit to its branch goes live on Player Two a couple of minutes after you push it, with nothing to run. In your studio, open the game’s **Where it plays**, then **Update it from GitHub**: give the repository (its address, or owner/name), the branch (its default one if left empty) and the folder that holds the built game, the one with its `index.html` (empty for the top). - **At once:** the branch’s latest commit becomes the game’s build, so a wrong folder is said before anything changes. Linking a game played on your own site makes Player Two host it from the repository. - **Then each commit:** Player Two looks at the branch every two minutes, and each new commit becomes the build, as a push does: players get it on their next page load. It is called by its number in the game’s builds; name it in its list of builds (**Name it**), or push from an Action with `--version` (below) to name each one as it goes up. A commit that leaves the game’s files as they were (a README, say) changes nothing. - **A commit that can’t be a build** (no `index.html` in the folder, too big) leaves the game playing the build it had; you’re told in your notices and in the studio, and the next commit is tried as it comes. **Update now** looks at once and tries again. - **Files kept with Git LFS** are only in GitHub’s archive of a commit once _Include Git LFS objects in archives_ is on, in the repository’s settings. ### A game that needs building Commit the built game, or let a GitHub Action build it into a branch of its own, as GitHub Pages does with `gh-pages`, and link that branch: the Action ``` # .github/workflows/player-two.yml: build on every push to main, into the branch Player Two follows name: Build for Player Two on: { push: { branches: [main] } } permissions: { contents: write } jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 22 } - run: npm ci && npm run build # your build: it ends with the game in dist/ - uses: JamesIves/github-pages-deploy-action@v4 with: { branch: player-two, folder: dist } # then link branch player-two, folder empty ``` ### A private repository Player Two reads public repositories only, the way git itself does, with nothing to sign in to. A private one pushes its build from an Action with the tool and an agent key kept as a repository secret: the Action ``` # .github/workflows/player-two.yml, for a private repository: the Action pushes the build itself name: Push to Player Two on: { push: { branches: [main] } } jobs: push: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 22 } - run: npm ci && npm run build - run: curl -fsSO https://playertwo.lol/cli/p2.mjs && node p2.mjs push ./dist --game your-game env: { P2_AGENT_KEY: ${{ secrets.P2_AGENT_KEY }} } # an agent key from your studio, as a repository secret ``` ### Releases as downloads A linked game can take its downloads from the repository’s releases too: tick **Its releases are its downloads**. Within ten minutes of a release being published (not a draft or a pre-release), its file for each system is brought into the game’s downloads as its full game, with the release’s tag as its version (`v1.2.0` becomes 1.2.0): - **Named for the system:** a file with `windows` or `win64` in its name (or an `.exe`, `.msi`) is the Windows download; `mac`, `macos`, `osx` (or a `.dmg`, `.pkg`) the Mac one; `linux` (or an `.AppImage`, `.deb`, `.rpm`) the Linux one. Where several fit, a `.zip` is taken for Windows, a `.dmg` for Mac and an `.AppImage` for Linux. - **Checked as an upload is:** up to 2 GB each, they wait for the virus check and Player Two’s look before players can get them, and you’re told in your notices what came in and what was left out. - **Once each:** a release already brought in isn’t brought in again; the next one is. ## How it's served - **One build a page load.** The page is never cached; each file it asks for comes from that build’s own address (`/~/...`), cached for a year, since a build’s files never change. A new build is a new address, so no page load mixes two. - **Played in Player Two’s player only.** Only Player Two can frame the game, and a visit to its address goes to its page here, where saves, trophies and friends work. A free game can be played at its own address too: turn that on in your studio. - **Its files are its own.** They are sent to the game’s own pages and to tools, never to another site. Ranges are answered, for audio and video. ## A free trial A game that costs money and plays here can let players try it before they buy it: on its page in the editor, **Price**, its **Free trial**, from 15 minutes to 2 hours, half an hour for a new game unless you choose otherwise (also in your studio, with how many tried it and bought it; an admin of your studio can set it too; a developer on your team can’t, as with its price). Players see it as a demo: **Play demo** with its length beside Buy wherever the game is (the room’s big screen, the store’s spotlight, its page and its bar), then the time they have left, until it’s used up; its card in the store says **Demo** and its length, and it joins the store’s Try before you buy. - **The whole game, once.** A player who doesn’t own it plays the build players get, in Player Two’s player, with its saves, trophies and services; one trial a player. - **A game on its own site** can have one too. Served through Player Two, its play link runs out with the trial. Played from its site as it is, the trial is Player Two’s, as its price is: the time counts in Player Two’s player, which closes the game when it’s up, while the site stays as open as you keep it. - **Only the time it’s open counts, and Player Two counts it.** The player’s page checks in every thirty seconds, and Player Two pays for the next minute and a half of play each time; the game plays only as long as it’s paid for, so a page that stops checking in stops the game instead of stretching the trial. A computer left asleep uses nothing, and closing the game gives back what was paid and not played. They can stop and come back until it’s used up. - **When it’s up,** the game’s last saves go up, it closes, and the player is offered Buy. What they saved carries on when they buy it. Its play link and its session run out with the trial: the session answers the game (saves, trophies, scores) only while its time is paid, and a minute after for its last saves. There’s nothing to change in the game. - **In the studio,** how many players tried it, and how many of them have it now. ## Limits _A web build's limits_ | | Up to | | --- | --- | | A pushed folder | 5 GB, 20,000 files | | One file in it | 512 MB | | A zip (the submit form, or an agent's upload) | 100 MB; 300 MB and 3,000 files unpacked | | A linked GitHub repository at a commit | 250 MB; its game's folder 300 MB and 3,000 files | | One .html page | 10 MB | | A player's copy of localStorage, and of IndexedDB | 2 MB each | Paths are the folder’s own: none may go outside it. A build a game no longer plays is kept 30 days after another takes its place, or as one of the last 20 it played, whichever keeps more, to try or make live again (its studio’s Builds, or list_builds); then its files go. --- --- title: "Webhooks · Player Two" url: https://playertwo.plus/docs/webhooks description: "Player Two webhooks for developers (playertwo.lol): tell your own service or a Discord channel when your games sell, a player buys inside one, your games get reviewed or wishlisted, go live with a new build, or your studio gets a follower." source: Player Two (https://playertwo.plus), made from the page itself --- Documentation · Webhooks # Webhooks A webhook tells another service when something happens with your games: a sale, a purchase inside one, a refund, a review, a wishlist, a new build, a new follower of your studio. Add up to five in your [studio](https://playertwo.plus/studio#webhooks), each with the events it hears of. Each event is a signed POST of JSON; a Discord webhook’s address gets a message in plain words instead. ## The events _What each event says_ | Event | When, and what it says | | --- | --- | | sale | A copy is bought or given as a gift: the game, what was paid (dollars), and whether it was a gift. Never who bought it. | | purchase | A player buys inside a game ([in-game purchases](https://playertwo.plus/docs/sdk#purchases)): the game, what was paid (dollars), the player’s id for the game, the payment’s id, whether it was a test (amount 0, no payment), and what it was: a `product` (key, kind, name, qty) or a `charge` (its id, name, and your `ref`). Your server gives it from this. | | refund | Something sold is refunded: the game and how much went back (dollars). When Player Two refunds it (a player can’t refund an add-on or an in-game purchase themselves), it also has `by` `"player-two"` and the `payment`’s id, and, for an in-game purchase, what it was, as in `purchase`: a `product` (key, kind, name, qty) or a `charge` (its id, name and your `ref`), with the `player`’s id for the game; an add-on has its `addon` name. Player Two takes back what it holds (the copy, the add-on, the item, a consumable’s count, while none of it is used); your server takes back what it gave for a charge, and should re-read what the player owns. | | review | A review is written or changed: the game, its stars, whether it recommends the game, its words, and its writer's handle (reviews are public). | | wishlist | The game is put on a wishlist: how many players have it there now. Never who. | | build | A new build of the game is live (pushed, uploaded, rolled back to, or from a GitHub commit): its key and version (what its developer called it, or its number in the game's builds), where it came from, its files and size. | | follow | A player follows your studio: how many follow it now. Never who. | Every event has `event` and `at` (when, ISO 8601), and a `game` (its slug, title and address) except `follow`, which has your `studio`. A `test` event comes when you press Send a test. ## The request a sale ``` POST https://your-service.com/player-two content-type: application/json user-agent: PlayerTwo-Webhooks/1 x-p2-event: sale x-p2-delivery: 6a1f0d0e-5b8a-4f3c-9d2e-1c7b8a9e0f12 x-p2-timestamp: 1793102400 x-p2-signature: t=1793102400,v1=5d0c6e7a... { "event": "sale", "at": "2026-10-27T12:00:00.000Z", "game": { "slug": "your-game", "title": "Your Game", "url": "https://playertwo.lol/game/your-game" }, "amount": 4.99, "gift": false } ``` a purchase inside a game (a charge your server made) ``` { "event": "purchase", "at": "2026-11-05T13:20:00.000Z", "game": { "slug": "your-game", "title": "Your Game", "url": "https://playertwo.lol/game/your-game" }, "amount": 4.99, "charge": { "id": "4b8f2c1e-7a3d-4e9b-8c2f-1d6e5a9b0c3d", "name": "5,000 coins", "ref": "order-8812" }, "player": "Xk3q9V2mA7cT1pLw8eR4sZ", "payment": "9c2e7f4a-1b3d-4a8e-9f6c-2d5b8e1a7c4f", "test": false } ``` - `x-p2-delivery` is the delivery’s own id: a retry has the same one, so handle each id once. - Answer with any 2xx within ten seconds. Redirects aren’t followed. - Webhooks go to public https addresses only, on the standard port. ## Checking the signature `x-p2-signature` is `t=,v1=`: an HMAC-SHA256 of `.` with the webhook’s secret (it starts `p2whsec_`; your studio shows it). Check it on the raw body before parsing it, and turn away anything older than a few minutes. checking it ``` // Node: the raw body, as it arrived (before any JSON parsing), and the webhook's secret from your studio import { createHmac, timingSafeEqual } from "node:crypto"; export function fromPlayerTwo(raw, header, secret) { const [t, v1] = header.split(",").map((p) => p.split("=")[1]); if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // older than five minutes: a replay const want = createHmac("sha256", secret).update(`${t}.${raw}`).digest("hex"); return v1.length === want.length && timingSafeEqual(Buffer.from(v1), Buffer.from(want)); } ``` ## Retries A delivery that isn’t answered with a 2xx is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, then given up. A webhook that fails twenty times running is turned off, and you’re told; turn it back on in your studio once it’s fixed. Your studio shows each webhook’s last deliveries and what they were answered. ## Discord Give a Discord channel’s webhook address (Server Settings, Integrations, Webhooks, then Copy Webhook URL) and each event arrives as a message in plain words, such as “Someone bought **Your Game** ($4.99).”, with nothing in between to run. --- --- title: "Where to publish a web game · Player Two" url: https://playertwo.plus/developers/compare description: "Where to publish a web game, compared from each platform's own pages (checked November 8, 2026): Player Two (playertwo.lol), itch.io, Poki, CrazyGames, Steam, Discord Activities, Newgrounds and Game Jolt. Revenue share, multiplayer servers, payments, SDK, hosting limits and review." source: Player Two (https://playertwo.plus), made from the page itself --- For developers · Compared # Where to publish a web game Player Two (playertwo.lol) is a store for browser games with multiplayer rooms, payments and hosting built in. Here it is beside the other places developers put web games: itch.io, Poki, CrazyGames, Steam and Discord Activities, then Newgrounds and Game Jolt. What each keeps of a sale, whether it runs multiplayer servers, who takes the payments, what its SDK does, how big a build may be, and whether a person reviews a game before it’s listed. Every other platform's facts come from its own public pages, read on November 8, 2026, each linked beside what it says; where a page doesn't say, neither do we. Platforms change their terms, so check the linked page before you decide. ## At a glance _The short answers (checked November 8, 2026)_ | Platform | Developer keeps of a sale | Multiplayer servers | In-game purchases | Review first | | --- | --- | --- | --- | --- | | Player Two | 90% | Included | Yes, you keep 80% | No | | itch.io | You set itch.io's share (10% by default) | Not described | Not described | No | | Poki | Ads: 100% direct, 50/50 from Poki | Peer-to-peer library (beta) | Not described | Hand-picked | | CrazyGames | Ads: a share, not stated | No, bring your own | Xsolla, by invitation | Yes, QA | | Steam | Set in its agreement; $100 fee an app | Matchmaking, P2P, game servers API | Yes, Steam Wallet | Yes, 1 to 5 days | | Discord Activities | A 15% fee on a team's first $1M (2024 post) | No, your own backend | Yes, verified apps | Verified for selling | Newgrounds and Game Jolt are below: their own pages said too little for a row of their own when we read them. ## What the developer keeps Of a sale, and of a purchase made inside the game. _What the developer keeps, from each platform's own pages_ | Platform | What its pages say | Source | | --- | --- | --- | | Player Two | 90% of each sale and 80% of each in-game purchase: Player Two takes 10% and 20%. Nothing to list or host a game. (A game that raised money on Player Two's launchpad shares part of its sales with its backers, as its developer set; 5% of each stage of a raise as it is paid out.) | [Player Two's fees](https://playertwo.plus/help#fees) | | itch.io | The seller chooses itch.io's share, from 0% to 100%; the default is 10%. The payment processor's own fee (around 30 cents and 2.9% a payment) comes on top. itch.io costs nothing to use. | [itch.io: Payments](https://itch.io/docs/creators/payments) · [itch.io: Creator FAQ](https://itch.io/docs/creators/faq) | | Poki | Ad revenue: 100% of what players who arrive directly bring (bookmarks, search, social, your own community), 50/50 for players Poki brings. That is the web-exclusive deal; Poki's pages say other deals are set in the agreement. | [Poki: Working with Poki](https://developers.poki.com/guide/working-with-poki) | | CrazyGames | A share of ad revenue, plus optional in-game purchases; its public pages give no percentage. Ads are on from its Full Launch, not during its Basic Launch. | [CrazyGames: FAQ](https://docs.crazygames.com/faq/) | | Steam | The revenue share is set in the Steam Distribution Agreement; the Steamworks pages we read give no percentage. A $100 Steam Direct fee for each app, recouped once the app has $1,000 in adjusted gross revenue. | [Steamworks: App fee](https://partner.steamgames.com/doc/gettingstarted/appfee) · [Steam Direct](https://partner.steamgames.com/steamdirect) | | Discord Activities | Discord's announcement of June 6, 2024: a 15% platform fee on a team's first $1M in cumulative gross sales, 30% before that change. Its monetization docs give no percentage. | [Discord: developer updates, June 6, 2024](https://discord.com/blog/feast-your-moniteyes-on-todays-exciting-developer-updates) · [Discord: Enabling monetization](https://docs.discord.com/developers/monetization/enabling-monetization) | ## Multiplayer servers Whether the platform runs the servers a multiplayer game needs. _Multiplayer servers, from each platform's own pages_ | Platform | What its pages say | Source | | --- | --- | --- | | Player Two | Yes, included: rooms of 1 to 64 players, and worlds of 256 players to an area, matched near the players, with voice for up to 24 in a room; a room can run your own code on Player Two’s servers, so you need no server of your own. For games hosted on Player Two (a game on its own site can push its room code too). | [The SDK: rooms](https://playertwo.plus/docs/sdk#rooms) · [Their limits](https://playertwo.plus/docs/sdk#rooms-limits) | | itch.io | Not offered: its creator docs and API overview describe no multiplayer servers. | [itch.io: Creator FAQ](https://itch.io/docs/creators/faq) · [itch.io: API overview](https://itch.io/docs/api/overview) | | Poki | Netlib, a peer-to-peer library for web games over WebRTC; Poki hosts its signaling and STUN/TURN relays for free. In beta. | [Poki: Netlib](https://developers.poki.com/guide/netlib) | | CrazyGames | No: “we only host the game files”; a multiplayer game brings its own servers (Photon is the example given). The SDK has invite links. | [CrazyGames: FAQ](https://docs.crazygames.com/faq/) | | Steam | Matchmaking and lobbies, peer-to-peer networking, and a Game Servers API for your own dedicated servers. | [Steamworks: Multiplayer](https://partner.steamgames.com/doc/features/multiplayer) | | Discord Activities | You run your own backend; all traffic goes through the Discord Proxy, and only WebSockets are supported (WebRTC is not). | [Discord: Activities networking](https://docs.discord.com/developers/activities/development-guides/networking) · [Discord: Activities overview](https://docs.discord.com/developers/activities/overview) | ## Payments Who takes the player's money, and how the developer is paid. _Payments, from each platform's own pages_ | Platform | What its pages say | Source | | --- | --- | --- | | Player Two | Player Two sells the game and the purchases inside it (p2.store: your products, or charges at your own prices). Players pay from their Player Two balance, topped up by card, Apple Pay, Google Pay or bank. Developers claim earnings from their studio from $1; money is in US dollars, held as USDC on Base, and each payout is approved by a Player Two admin for now. | [In-game purchases](https://playertwo.plus/docs/sdk#purchases) · [Payouts](https://playertwo.plus/help#payouts) | | itch.io | Sales through PayPal and Stripe. | [itch.io: Payments](https://itch.io/docs/creators/payments) | | Poki | Ad revenue paid by wire transfer or PayPal; no in-game purchases described. | [Poki: Payouts and billing](https://developers.poki.com/guide/payouts-billing) | | CrazyGames | Ad earnings paid monthly once they reach €100. In-game purchases through Xsolla, by invitation only. | [CrazyGames: FAQ](https://docs.crazygames.com/faq/) · [CrazyGames: In-game purchases](https://docs.crazygames.com/sdk/in-game-purchases/) | | Steam | Steam processes in-game purchases; players pay from their Steam Wallet. | [Steamworks: Microtransactions](https://partner.steamgames.com/doc/features/microtransactions) | | Discord Activities | Discord sells one-time items and subscriptions for a verified app owned by a team in the US, the EU or the UK; payouts once the app has made its first $100, after a review. | [Discord: Enabling monetization](https://docs.discord.com/developers/monetization/enabling-monetization) | ## SDK What the platform's own library gives a game. _SDK, from each platform's own pages_ | Platform | What its pages say | Source | | --- | --- | --- | | Player Two | The Player Two SDK (`npm i playertwo-sdk`, or one script tag): the signed-in player, multiplayer rooms, trophies, XP and levels, stats, leaderboards, cloud saves, presence, in-game purchases, the player’s avatar and travel between games; a server API with game keys; an MCP server for AI agents to publish and update the game. | [The game SDK](https://playertwo.plus/docs/sdk) · [Agents and MCP](https://playertwo.plus/docs#agents) | | itch.io | Its API overview describes a server-side API with an API key, OAuth apps, a JavaScript buy button and an embed widget; no in-game library for trophies, saves or multiplayer is described. | [itch.io: API overview](https://itch.io/docs/api/overview) | | Poki | The Poki SDK (HTML5, Unity, Godot, GameMaker, Construct, Phaser and others): ads (commercial and rewarded breaks) and gameplay events. | [Poki: SDK overview](https://developers.poki.com/guide/sdk-overview) | | CrazyGames | The CrazyGames SDK: ads, the player's account, game events, invite links, cloud saves (data) and in-game purchases; needed for Full Launch. | [CrazyGames: FAQ](https://docs.crazygames.com/faq/) · [CrazyGames: Data module](https://docs.crazygames.com/sdk/data/) | | Steam | The Steamworks SDK: achievements and stats, leaderboards, Steam Cloud, matchmaking, networking, inventory and microtransactions. | [Steamworks: Features](https://partner.steamgames.com/doc/features) | | Discord Activities | The Embedded App SDK: sign-in, the channel and its players, voice and layout events, invites, rich presence and purchases. | [Discord: Activities overview](https://docs.discord.com/developers/activities/overview) | ## Hosting a web build Where the game's files live, and how big they may be. _Hosting a web build, from each platform's own pages_ | Platform | What its pages say | Source | | --- | --- | --- | | Player Two | Hosted at its own address on playertwo.cc: a pushed folder up to 5 GB and 20,000 files, with no code changes; builds kept 30 days or the last 20, with rollback. Or play it from your own site, inside Player Two’s player. | [Hosting](https://playertwo.plus/docs/hosting) | | itch.io | An HTML5 zip of up to 1,000 files, 500 MB extracted and 200 MB a file (support can raise them). | [itch.io: Uploading HTML5 games](https://itch.io/docs/creators/html5) | | Poki | Guidance, not a hard limit: an initial download of no more than 5 MB and 8 MB in all. | [Poki: Choosing your web game engine](https://developers.poki.com/guide/web-engine) | | CrazyGames | An initial download of 50 MB or less (20 MB for its mobile homepage), 250 MB and 1,500 files in all. | [CrazyGames: Technical requirements](https://docs.crazygames.com/requirements/technical/) | | Steam | Steam distributes downloaded builds; it doesn't host browser games. | [Steamworks: Features](https://partner.steamgames.com/doc/features) | | Discord Activities | You host it yourself; Discord shows it in an iframe. | [Discord: Activities overview](https://docs.discord.com/developers/activities/overview) | ## Review before it's listed What happens between submitting a game and players finding it. _Review before it's listed, from each platform's own pages_ | Platform | What its pages say | Source | | --- | --- | --- | | Player Two | None before: a game goes live the moment it’s submitted, and every game must follow the content policy, enforced by reports and moderation. A raise takes no money until its developer passes an identity check. | [Content policy](https://playertwo.plus/policy) · [Help: who can put a game up](https://playertwo.plus/help#about-who) | | itch.io | None before publishing; a new page is private until made public, and pages that break its quality guidelines can be removed from browse and search (still open by their link). | [itch.io: Quality guidelines](https://itch.io/docs/creators/quality-guidelines) | | Poki | Curated: “Every game on Poki is hand-picked”, after an application and Poki's own tests. | [Poki: Working with Poki](https://developers.poki.com/guide/working-with-poki) | | CrazyGames | A QA review of every game, then a Basic Launch to a small share of players, then a second review for Full Launch. | [CrazyGames: FAQ](https://docs.crazygames.com/faq/) | | Steam | A review of the build and store page, 1 to 5 days, and 21 days between paying the app fee and a first release. | [Steam Direct](https://partner.steamgames.com/steamdirect) | | Discord Activities | Monetization needs a verified app owned by a team. | [Discord: Enabling monetization](https://docs.discord.com/developers/monetization/enabling-monetization) | ## Newgrounds and Game Jolt - **Newgrounds:** its Newgrounds.io API signs players in with Newgrounds Passport, and any developer can use it free (its help page adds that later features may need a subscription): [Newgrounds.io: Help](https://www.newgrounds.io/help/). Its own pages on revenue, hosting limits and review didn’t load for us on November 8, 2026, so they aren’t compared here. - **Game Jolt:** its help and marketplace pages draw their words with JavaScript, and showed us none on November 8, 2026, so nothing of it is compared here. See [gamejolt.com/help/marketplace](https://gamejolt.com/help/marketplace). ## How this was made Each platform's own documentation, help and terms pages were read on November 8, 2026; nothing comes from third-party write-ups or forums. Player Two's figures are its own: the fees in the help center, the limits in the SDK and hosting docs. > Something here out of date, or wrong? Tell us at hello@playertwo.lol and we’ll check it against the platform’s page and change it. Read on: [everything your game gets on Player Two](https://playertwo.plus/developers), [the game SDK](https://playertwo.plus/docs/sdk), [hosting](https://playertwo.plus/docs/hosting) and [the developer terms](https://playertwo.plus/developer-terms). --- --- title: "Content and moderation policy · Player Two" url: https://playertwo.plus/policy description: "Player Two's content and moderation policy (playertwo.lol): what can be listed and what is not allowed, and how fan games, reports, warnings, takedowns, refunds, bans, developer identity checks and the integrity of raises work, and how to appeal." source: Player Two (https://playertwo.plus), made from the page itself --- Policy # Content and moderation policy What can go on Player Two, what can’t, and what we do when something breaks the rules. It covers every game, page, post, build and account on the platform. Last updated November 7, 2026 ## The short version - List games you made, or have the right to publish and sell. - No malware, scams, stolen or infringing work, hate, harassment, sexual content involving minors, or impersonation. - Fan games of other people’s characters are allowed only if they’re clearly unofficial, and they come down when the owner asks. - Raises must be honest. Backers can object to proof and vote, and Player Two reviews when too few vote. - Anyone can report a game. A person at Player Two looks at every report. - We can warn, take a game down, refund backers and ban accounts. You can appeal any decision by email. ## What may be listed Player Two is open: any developer with an account can list a game, and it goes live the moment it’s submitted. You may list: - Games you made, or have the rights to publish and sell, with everything in them: code, art, music, sound, video and writing. Use other people’s assets only as their licence allows. - Finished games, demos and early builds in the store, free or paid. - Games still being made, raising money on the launchpad, with a plan you mean to deliver. A game’s page has to describe the game honestly. Its pictures, trailer and plan should show what players will actually get. **Labels.** Say who your game is for (everyone, or 13, 16 or 18 and up) and label what it shows: violence, blood and gore, sexual content, nudity, strong language, drugs, alcohol or tobacco, gambling-style play, frightening scenes, or talking with strangers online. A game with sexual content or nudity is for adults: it’s labelled 18+, it stays out of the store and every list until a player says they are 18 or older, and its page asks first. A game missing a label it needs can be labelled by us, or taken down. ## What isn’t allowed - **Malware** Anything that harms players, their devices or their accounts: viruses, spyware, hidden miners, code that steals passwords, keys, money or payment accounts, or a build that tries to break out of its sandbox. - **Scams** Raising money with no plan to make the game, posting fake proof, or a page that misleads players about what they are buying or backing. - **Stolen or infringing work** Someone else’s game, or art, music, code or characters you don’t have the rights to, including a game re-uploaded by someone who didn’t make it. Fan games are covered below. - **Hate** Attacking or demeaning people for who they are: their race, ethnicity, nationality, religion, sex, gender, sexual orientation, disability or a serious illness. - **Harassment** Threats, bullying, singling someone out, or sharing someone’s private information. - **Sexual content involving minors** Never, in any form, drawn or generated included. We remove it at once, close the account for good and report it to the authorities. - **Impersonation** Pretending to be another person, studio or company, or to be Player Two. Names like Admin, Support and Player Two are kept for us. These hold for everything on Player Two: games and their pages, reviews, and players’ names, handles, pictures and bios. A picture or bio that breaks them is taken down, and its player is told why. So is a picture or clip in a review: the review keeps its words and stars, and the file stops being shown or served. ## Fan games A fan game uses characters or worlds that belong to someone else. You may list one only if it’s clearly unofficial: - Say so on its page. The short note after the tagline is made for it, as in “an unofficial fan game”. - Don’t suggest that the owner of those characters made it, backs it or approves of it. If the owner of the characters asks us to take a fan game down, we do. If backers’ money is riding on it, we settle with them first: what hasn’t been paid out goes back to them. ## The integrity of raises Backers trust a raise with real money, so raises run by rules that protect them: - Raised money is held by Player Two and paid to the developer stage by stage, only once each stage is proven. - Proof must be real and checkable: a link backers can open. - Backers have 3 days to object to proof. An objection, or a missed deadline, opens a 5-day vote of backers, weighted by the dollars each backed. - A vote counts only if backers holding at least 20% of the money vote. If too few vote, Player Two reviews the stage and decides: pay it, give more time, or refund what’s left. - Developers can’t back, object to or vote on their own games, from their own account or any other. - For now, a Player Two admin reviews every payout before it’s sent. If we find a raise is a scam, we take it down and refund backers what hasn’t been paid out. How raises work, step by step, is in the [documentation](https://playertwo.plus/docs#stages). ## Developer identity checks Only developers who raise money are checked. Before a raise can take any, and before any of its stages is paid out, its developer proves who they are with Didit, our identity partner. Players, and developers who only sell their games or give them away, are never asked. The check takes: - A photo of an official ID - A selfie - 18 or older Didit checks the document is real and the selfie matches it. Player Two keeps only the name on the ID and the country that issued it; the photos stay with Didit. A check Didit can’t decide is reviewed by a person. Until it passes, the raise can’t take money. ## How reports work Anyone can report a game, signed in or not. Press **Report this game** at the foot of its page, choose what’s wrong, and add anything that helps us check. The choices are: - A scam, or money taken without making it - Someone else’s game or art - It doesn’t work - Hateful or harmful - Spam - Something else You can also email [hello@playertwo.lol](mailto:hello@playertwo.lol?subject=A%20report) with a link and what you saw. A person at Player Two looks at every report. > **Own the rights to something listed here?** Email [hello@playertwo.lol](mailto:hello@playertwo.lol?subject=A%20rights%20request) with a link to the game, what’s yours in it, and how we can reach you. ## What Player Two does We choose the step that fits what happened. The most serious cases, like malware, scams and sexual content involving minors, go straight to a takedown and a ban. _What we can do_ | Step | What happens | | --- | --- | | Warning | We tell the developer what’s wrong and what to change. | | Takedown | The game comes off the store and the launchpad, and its page is no longer public. | | Refund | Backers get back what hasn’t been paid out of a raise, in proportion to their stakes. | | Ban | The account can no longer buy, back, vote, post or submit anything. | A developer can take their own game down from their studio while no backer’s money is riding on it. Once backers have money in it, only Player Two can, after settling with them. ## Appeals If we took your game down, refunded your raise or banned your account and you think we got it wrong, email [hello@playertwo.lol](mailto:hello@playertwo.lol?subject=An%20appeal). Tell us which game or account, and why. A person reads every appeal, and we reply. ## Contact **Questions about this policy** Write to [hello@playertwo.lol](mailto:hello@playertwo.lol?subject=This%20policy). When the policy changes, the date at the top of this page changes with it. [Write to us](mailto:hello@playertwo.lol?subject=This%20policy) --- --- title: "Terms of service · Player Two" url: https://playertwo.plus/terms description: "The terms of service for Player Two (playertwo.lol), a store for browser games: your account, your balance and payments, games and your library, refunds, raises, what you post, what isn't allowed, copyright complaints and liability." source: Player Two (https://playertwo.plus), made from the page itself --- Terms # Terms of service The rules for using Player Two: the store, the launchpad, your account and your balance. In plain words, so you know what you agree to. Last updated November 7, 2026 ## The short version - You need to be 13 or older for an account, and 18 or older to back a raise, raise money or be paid. - A game you buy or are given is yours to play, under a licence: you don’t buy the game itself. - Payments are in digital dollars (USDC) on the Base network, from a balance that is yours. Player Two never holds its keys. - Backing a raise is risky: a game can be late, change, or never be finished. Only put in what you can afford to lose. - Follow the [content policy](https://playertwo.plus/policy). We can take down what breaks it and close the accounts that do. - Player Two is provided as it is, and our liability is limited as far as the law allows. ## Who we are Player Two (“we”) runs playertwo.lol, the Player Two app and the services on them. You can reach us at hello@playertwo.lol. By making an account, buying, backing, listing or playing, you agree to these terms, the [content policy](https://playertwo.plus/policy) and the [privacy policy](https://playertwo.plus/privacy). Developers also agree to the [developer terms](https://playertwo.plus/developer-terms). We may change these terms. When we do, this page gets a new date, and we email you about any change that matters. Using Player Two after a change means you accept it. ## Your account - One person, one account. Keep your sign-in safe: you are responsible for what happens on your account. - You must be 13 or older, or the minimum age where you live if it is higher. If you are under 18, a parent or guardian must agree to these terms for you. - Your handle, name and picture must not impersonate anyone or break the content policy. - You can download your data, and delete your account, at any time from your profile. ## Your balance and payments - Signing in gives you a balance, kept in an account made and secured by Privy, our payments provider. Its keys are yours; Player Two can’t move your money without your approval. - Prices are in US dollars, paid in USDC on Base. Adding funds by card, Apple Pay, Google Pay or bank runs through a payment provider on its own terms, and it may charge its own fee. Player Two adds no fee to top-ups and pays the network fees on payments and payouts. - A payment on a blockchain is public, and we can’t reverse it. Check before you approve one. - Where the law requires a sales tax, we include it or show it before you pay. ## Games and your library - When you buy or add a game, you get a personal licence to play it for as long as it stays on your account. You can’t sell, rent or share it. - Developers update their games: features can come and go. - A gift goes into your friend’s library for good. A key from a developer gives you a copy the same way. - If its developer takes a game down, players who have it keep it. If we take a game down for breaking the [content policy](https://playertwo.plus/policy), it can leave players’ libraries too. ## Refunds - Pay twice for a game you already own, and the second payment goes straight back to your balance. - Purchases inside a game are final. A payment that lands on something already yours, or on a purchase the game withdrew, goes straight back to your balance. - A pre-order is a game paid for now that comes out on a set day. Until that day you can cancel it from the game’s page for all your money back, and if the game is taken down, archived or made private before then, it’s refunded by itself. From its day it’s a purchase like any other. - Otherwise purchases are final, except where the law gives you a right to cancel or to a refund. - In the EU, the UK and some other places you normally have 14 days to cancel a digital purchase. A game here is yours to play the moment you buy it: buying it, you ask for it to be supplied at once, and you agree that this ends that right to cancel. - Raises have refunds of their own, below. ## Raises and backing On the launchpad, developers raise money for games still being made. A raise gives its backers either a share of the game’s sales on Player Two, or rewards only; its page says which. - Player Two holds the money raised and pays it to the developer stage by stage, on proof, less 5%. A late stage goes to a vote of its backers: more time, or what’s left back. - If a raise closes short of its goal, every backer is refunded in full. - A share of sales pays only from the game’s sales on Player Two, as its page sets out. It is not a share of the studio, a loan or a promise of any return. - Rewards are the developer’s promise. We can’t guarantee them, but a developer who doesn’t deliver breaks our rules. - You must be 18 or older to back a raise. Some countries limit or forbid backing for a share of sales; you are responsible for the rules where you live. > Backing is risky. Games are often late, change, or are never finished. Back only what you can afford to lose. Nothing on Player Two is investment, tax or legal advice. ## What you post Reviews, topics, answers, pictures and everything else you post stay yours. You give Player Two a worldwide, free licence to host, show and share them on Player Two and when we promote it, for as long as they are up. Don’t post anything that breaks the content policy or someone else’s rights. ## What you can't do - Break the law, the content policy, or anyone’s rights. - Cheat, abuse bugs, or use bots, scripts or many accounts to get an edge, inflate counts, or bend votes, leaderboards or reviews. - Attack, overload, scrape or take apart the platform, or get around its limits and protections. - Resell, rent or share copies, keys or accounts. ## Copyright complaints If you believe something on Player Two infringes your rights, email [hello@playertwo.lol](mailto:hello@playertwo.lol?subject=Copyright%20complaint) with: what it is and where it is, the work it infringes, your contact details, and a statement that you believe in good faith that the use isn’t allowed and that what you say is accurate. If it’s well founded we take it down and tell whoever posted it, who can answer it. ## Ending things You can stop using Player Two and delete your account at any time. We can suspend or close an account that breaks these terms or the content policy, or when the law requires it; where we can, we say why. Records of money stay, as the [privacy policy](https://playertwo.plus/privacy) explains. ## Liability - Player Two is provided “as is”. We don’t promise it will always be available, free of errors or secure. - We aren’t responsible for the games developers make, for what other players do, or for the blockchains, payment accounts and payment providers we don’t run. - As far as the law allows, we aren’t liable for indirect losses, and our total liability to you is what you paid Player Two in the 12 months before the claim. Nothing here limits a right the law gives you that can’t be limited. ## Disputes Tell us first: most things can be sorted out by email. If they can’t, these terms are governed by the law of the place where Player Two is established, and you keep any right the law where you live gives you to bring a claim there. **Questions about these terms** Write to us at [hello@playertwo.lol](mailto:hello@playertwo.lol?subject=The%20terms). [Write to us](mailto:hello@playertwo.lol?subject=The%20terms) --- --- title: "Privacy policy · Player Two" url: https://playertwo.plus/privacy description: "Player Two's privacy policy (playertwo.lol): what it collects, why, who helps run it, how long it is kept, identity checks for developers, cookies, and how to download your data or delete your account." source: Player Two (https://playertwo.plus), made from the page itself --- Privacy # Privacy policy What Player Two collects, why, who helps us run it, and the choices you have. We collect what the platform needs, we don’t sell personal data, and we run no advertising trackers. Last updated November 8, 2026 ## The short version - We keep your account (email, handle, profile), your library, what you buy and back, your balance’s public address, and what you post. - Developers who raise money or are paid go through an identity check with Didit: a photo of an ID and a selfie. - We measure how Player Two is used ourselves (pages, buttons, searches, the games and clips you open and how they run, signing in and checkout, your device): no advertising trackers, no outside analytics, no recordings of your screen, nothing sold. You can turn it off below. - Payments happen on a public blockchain, where the addresses that pay and get paid, and the amounts, are public by nature. - Download your data, or delete your account, at any time from your profile. ## What we collect - **Your account:** your email address and how you sign in (email, Google, X, Discord, a passkey or an outside account’s address), your handle and name, and the picture, bio and links you add. - **Your balance:** the public address it is kept at. Its keys are with you and Privy; Player Two never has them. - **What you do here:** your library, wishlist and collections, purchases, gifts, stakes in raises, passes, payouts, votes and objections, reviews, topics and answers, friends and the players and studios you follow, the players you were with lately (in the Plaza, for your own “Played with recently”, kept 90 days), and, while you play an online game here, a small live picture of it on its screen in the Plaza (never recorded; off in your settings, or while your activity is private), trophies, play sessions and play time, and the saves and scores of games that keep them with us. - **If you make games:** your studio, your games and their files, your payout address, and the result of your identity check. - **The Player Two app:** a key for each of your computers, with its name and system. - **Push notifications, if you turn them on:** for each browser or phone, the address its push service gives it and the keys that seal what we send, what it is (“Chrome on a Mac”), and when a notification last got through. Turned off, or your account deleted, they’re gone. - **Technical:** your IP address and browser, kept for a short time for security and to stop abuse: for 30 days, with the address you asked for, only when a request was refused, failed, or looked like an attack. And a report when a page breaks (the page, the error and your browser). - **Page views:** a game’s page counts each visitor once a day by a one-way code made from your IP address and browser (or your account), new every day, with where the visit came from. It tells a game’s developer how many came, not who. - **How you use Player Two:** the pages you open (and the one you came from), how long you stay on them, the buttons and links you press, what you search for, what it found and which result you picked, which of the store’s rows and games you see and which you press, the clips and trailers you play (for how long, and whether with sound), the games you open, how long you play and how smoothly they run (frames a second, slow frames, memory), demos, the games you add to your library, wishlists and shares, the steps of signing in (when its window opened and how long it took, whether you closed it, and the way you signed in, such as email or Google), the steps of checkout, payments and adding funds and why one didn’t go through, the error messages Player Two shows you, your time in the Plaza and the games you walk into from it, and how fast our pages load. With it: your browser and system, screen and window size, language, time zone, the graphics card and connection your browser reports, where your visit came from (the site or link, and its tags), and roughly where you are (country, region and city, from your IP address, which isn’t kept with it). Never what you type into forms, chat or comments, your keystrokes, a recording of your screen, or the codes in a link (from signing in, a game key, an invite or a code for a share off: only that the link had one). Signed in, it’s kept with your account; signed out, with a random id your browser keeps (a fresh one each visit if your browser sends Global Privacy Control or Do Not Track). When you make an account, we keep how you first found us. - **If you connect GitHub:** your public contribution counts, shown on your studio’s card. - **When you write to us:** your message and your address. ## Identity checks (developers who raise or are paid) Didit, our identity partner, checks a photo of an official ID and a selfie (which uses your face’s biometric data) and screens against public sanctions lists. You agree to the check before it starts. Didit tells us the result, your name, date of birth and country; the photos stay with Didit under its own policy, and we keep no copies. We use it to stop fraud, keep raises honest and meet our legal duties. Players never need one. ## Why we use it - **To run Player Two** for you: your account, library, payments, raises, payouts, downloads and the app (our agreement with you). - **To keep it safe:** stopping fraud, abuse and cheating, rate limits, moderation and reports (our legitimate interest, and the law). - **To tell you things:** receipts and notices about your money and account always; game news, sales and friends only as your email settings allow. - **To tell you what’s new:** an email when a game you have or played lately gets an update, one when a featured game arrives, and a weekly one of the week’s new games and your games’ news. They come until you turn them off: each has its own one-press “turn these off” link, and its own switch in your profile’s email settings (our legitimate interest). - **To make it better:** counting page views and errors, and measuring how Player Two and its games are used and how well they run, to fix what’s slow or confusing and see what players like (our legitimate interest). It isn’t sold, shared with advertisers, or used to follow you on other sites. - **Because the law asks:** tax, accounting, anti-money-laundering and requests from the authorities. ## What others see - Your profile page shows your handle, name, picture, bio and links and, as your privacy settings allow, your activity and your friends. Leaderboards show your name unless you turn that off. - Reviews, topics and answers are public, under your handle. - If you redeem a developer’s key, its developer sees your handle beside that key. - The addresses payments are made from and to, and their amounts, are public on the Base blockchain. ## Who helps us _The services Player Two runs on, and what they do with your data_ | Service | What it does | | --- | --- | | Privy | Sign-in and your balance | | Hetzner | The server the site and our database run on, in Germany | | Cloudflare | Delivering the site, storing games' pictures, builds and downloads, carrying voice chat between players (in games' rooms, the Plaza, and a party's voice on every page) and the live pictures of games being played that the Plaza's screens show (Cloudflare Realtime; never recorded), and translating what players write into a reader's language (the words only, never who wrote or reads them) | | Resend | Sending our emails | | Your browser's push service (Google, Apple, Mozilla or Microsoft) | Carrying push notifications to the devices you turned them on for, sealed so only your device can read them | | Didit | Identity checks for developers who raise or are paid | | Payment providers | Card, Apple Pay, Google Pay and bank top-ups, through Privy, on their own terms | They handle data for us under contracts, or under their own terms where you deal with them yourself (a top-up, say). We share data with the authorities only when the law requires it, and with a buyer if Player Two is ever sold, under this policy. ## Where it's kept, and for how long - Our services run mainly in the United States. Where your data leaves your country, we rely on the protections the law requires. - Your account’s data stays until you delete your account. Then your name, email, sign-in, library, wishlist, collections, follows and notices are deleted, and your games come down. - Records of money (payments, stakes, passes, payouts) stay as long as the law and the raises they belong to need them, without your name. - What we measure of how Player Two is used is kept for 13 months, then deleted. Delete your account and it’s no longer tied to you. - Security records last a short time, and backups are written over in turn. ## Your choices and rights - Download everything we keep about you as one file, and delete your account, from your [profile](https://playertwo.plus/profile#account). - Choose what we email you, what others see, and what games you see, in your profile’s settings. - Stop us measuring your visits in this browser: - Depending on where you live, you can ask to see, correct or delete your data, object to some uses, or complain to your data protection authority. Write to [hello@playertwo.lol](mailto:hello@playertwo.lol?subject=My%20data). ## Cookies and storage Only what the site needs: your sign-in (kept by Privy), a cookie that remembers a friend’s invite link for 30 days, your browser’s own storage for things like sound, the games you looked at lately and the choices you make on the page, and a random id in that storage for our own measurement of how Player Two is used (see above; you can turn it off). No advertising or tracking cookies, and nothing from anyone else. ## Children Player Two isn’t for children under 13, and we don’t knowingly collect their data. If you think a child has an account, tell us and we’ll remove it. ## Changes and contact When this policy changes we update this page and its date, and email you about any change that matters. **Questions, or a request about your data** Write to us at [hello@playertwo.lol](mailto:hello@playertwo.lol?subject=Privacy). [Write to us](mailto:hello@playertwo.lol?subject=Privacy) --- --- title: "Developer terms · Player Two" url: https://playertwo.plus/developer-terms description: "The developer terms for Player Two (playertwo.lol): the licence to host and sell your game, prices and fees, raises, payouts and taxes, keys, takedowns and liability." source: Player Two (https://playertwo.plus), made from the page itself --- Developers # Developer terms What you agree to when you list a game, raise money or are paid on Player Two, on top of the [terms of service](https://playertwo.plus/terms) and the [content policy](https://playertwo.plus/policy). Last updated November 5, 2026 ## The short version - Your game stays yours. You give us a licence to host it, show it, and sell it or give it to players. - Player Two takes 10% of each sale, 20% of each purchase inside a game, and 5% of each stage of a raise as it’s paid out. The network fees are ours. - Payouts go to your studio’s payout address in USDC on Base, once a Player Two admin approves them. - If you raise, your identity is checked, your raise’s terms are fixed once it opens, and you deliver what your page promises. - Your game must be yours to publish, safe, and honestly described and labelled. ## Your game, and our licence - You keep every right in your game. You give Player Two a worldwide, non-exclusive licence to host, copy, show and hand out it, its page, pictures, trailers and builds, to players and when we promote it, for as long as it’s listed and afterwards as long as players’ libraries need it. - You promise the game is yours, or that you have every right needed to publish and sell it here (its code, art, music, sound and writing), and that it holds no malware and nothing the [content policy](https://playertwo.plus/policy) forbids. Every build you upload is checked for viruses before players can get it; one where something is found waits for Player Two to look, and a build already live is held from players while we do. - You describe it honestly: its page, pictures and plan show what players will get, and its labels (who it’s for and what it shows, its languages and accessibility, its controller support and requirements) are true. ## Prices, sales and fees - You set the price, from free to $500, and can put the game on sale within the platform’s rules. - From each sale Player Two takes 10%. If the game had a raise that shares its sales, its backers get their share as the raise set it, 40% or more. You get the rest. - You can sell inside your game through Player Two’s payments ([in-game purchases](https://playertwo.plus/docs/sdk#purchases)): products you set up in your studio, or charges your own server makes at your own prices. Every one is paid on Player Two’s own card, which takes the player’s yes. From each, Player Two takes 20%; your backers’ share comes out of it as from a sale, and you get the rest, paid out with your sales. Add-ons sold on your game’s page are split as its sales. In-game purchases aren’t refunded, except a payment that lands on something the player already has or on a charge you withdrew, which goes straight back to them. - Players can refund a second purchase of the same game automatically; other refunds follow the [terms of service](https://playertwo.plus/terms#refunds). A refunded sale is taken back from you, from its backers and from Player Two’s fee alike. - A paid game set to come out on a day can be pre-ordered, at its price: no sale or code until it’s out. Player Two holds a pre-order’s money until that day and splits it then, like any sale. Until then a buyer can cancel for all of it back, and every pre-order is refunded if the game is taken down, archived or made private before its day. - You can make codes that take a share off your game’s price, and give a free trial of it in the browser of up to 2 hours. ## Keys Keys you make give free copies of your game, for press and creators. Don’t sell them or swap them for anything, and don’t use them to get around Player Two’s fee or your backers’ share: keys are capped, and we can revoke ones that are misused. ## Raises - To raise, you must be 18 or older and pass an identity check. - A raise either shares the game’s sales on Player Two with its backers (40% to 85% of every sale, for as long as it sells) or gives rewards only. Its goal, stages, share or rewards, and the day it opens are fixed once it opens. - Player Two holds the money and pays each stage, less 5%, once you post its proof and backers have had 3 days to object. A late stage goes to a backer vote, which can give 30 more days or refund what’s left. - Rewards are your promise: deliver them as your page says. - If you can’t deliver, say so: tell your backers, post an update, and work with Player Two on refunds. > A share of sales sold to backers may count as a securities offering in some countries. Player Two’s raises are being reviewed by a lawyer before the launchpad opens to everyone. ## Payouts and taxes - Payouts go to your studio’s payout address, in USDC on Base. A Player Two admin approves each one first, and a changed payout address holds payouts for 2 days. - You are responsible for your own taxes on what you earn. We may ask for tax details where the law requires them. ## Taking a game down - You can take your game down (archive it) at any time. Players who have it keep it in their library and can still open it. - Player Two can take a game down, hold payouts or close a studio that breaks these terms, the terms of service or the content policy, or when the law requires; where we can, we say why. - Money owed to backers or players from a raise comes first. ## Liability You are responsible for your game and for the claims that come from it. As far as the law allows, you cover Player Two against claims that your game infringes someone’s rights or breaks the law. The [terms of service’s](https://playertwo.plus/terms#liability) limits on Player Two’s liability apply here too. **Questions about these terms** Write to us at [hello@playertwo.lol](mailto:hello@playertwo.lol?subject=Developer%20terms). [Write to us](mailto:hello@playertwo.lol?subject=Developer%20terms) --- --- title: "Everything your game gets · Player Two" url: https://playertwo.plus/developers description: "Everything a game gets on Player Two, on one page: multiplayer rooms with no server of your own, worlds of 256, voice, trophies, leaderboards, cloud saves, the player's avatar and travel between games, its own server's calls, and the store, the launchpad and its builds." source: Player Two (https://playertwo.plus), made from the page itself --- # Everything your game gets Everything a game gets on Player Two, on one page: multiplayer rooms with no server of your own, worlds of 256, voice, trophies, leaderboards, cloud saves, the player's avatar and travel between games, its own server's calls, and the store, the launchpad and its builds. Start with the docs (https://playertwo.plus/docs.md), the game SDK (https://playertwo.plus/docs/sdk.md, `npm i playertwo-sdk`) and hosting (https://playertwo.plus/docs/hosting.md). For agents: https://playertwo.plus/llms.txt. ## On Player Two - [A store page, free or paid](https://playertwo.plus/docs#dev-listing) - [A raise on the launchpad](https://playertwo.plus/docs#dev-raising) - [Stages, proof and votes](https://playertwo.plus/docs#stages) - [Web builds and downloads](https://playertwo.plus/docs#dev-builds) - [Pushing a build](https://playertwo.plus/docs/hosting#push) - [Testers first, history and rollback](https://playertwo.plus/docs/hosting#push) - [From GitHub](https://playertwo.plus/docs/hosting#github) - [Your own site, served](https://playertwo.plus/docs/hosting#follow) - [Room code for your own site](https://playertwo.plus/docs/hosting#room-code) - [Saves with no code](https://playertwo.plus/docs/hosting#saves) - [A free trial](https://playertwo.plus/docs/hosting#trial) - [Editing a live page](https://playertwo.plus/docs#dev-editing) - [Earnings and payouts](https://playertwo.plus/docs#money-payouts) - [Webhooks](https://playertwo.plus/docs/webhooks) - [MCP and the agent API](https://playertwo.plus/docs#agents) - [Your rooms' connections, for your agent](https://playertwo.plus/docs/sdk#rooms-stats) ## In your game - [One script tag, or npm](https://playertwo.plus/docs/sdk#call-init): `PlayerTwo.init`, `PlayerTwo.ready`, `PlayerTwo.env`, `PlayerTwo.v`, `p2.env`, `p2.player`, `p2.game`, `p2.call` - [Building without Player Two](https://playertwo.plus/docs/sdk#start-local) - [Who is playing](https://playertwo.plus/docs/sdk#call-me): `p2.me` - [Trophies and levels](https://playertwo.plus/docs/sdk#call-trophies): `p2.trophies`, `p2.trophies.list`, `p2.trophies.unlock` - [Leaderboards](https://playertwo.plus/docs/sdk#call-scores): `p2.scores`, `p2.scores.submit`, `p2.scores.list` - [Stats](https://playertwo.plus/docs/sdk#call-stats): `p2.stats`, `p2.stats.get`, `p2.stats.set`, `p2.stats.add` - [Cloud saves](https://playertwo.plus/docs/sdk#call-saves): `p2.saves`, `p2.saves.list`, `p2.saves.get`, `p2.saves.put`, `p2.saves.delete` - [Add-ons](https://playertwo.plus/docs/sdk#addons): `p2.addons`, `p2.addons.list`, `p2.addons.has` - [In-game purchases: your studio's products, or your own prices](https://playertwo.plus/docs/sdk#purchases): `p2.store`, `p2.store.list`, `p2.store.owned`, `p2.store.buy`, `p2.store.pay` - [Presence](https://playertwo.plus/docs/sdk#call-presence): `p2.presence`, `p2.presence.set` - [Identity and invites](https://playertwo.plus/docs/sdk#call-identity): `p2.identity`, `p2.invite`, `p2.params` - [Loading and play events](https://playertwo.plus/docs/sdk#call-gameplay): `p2.loading`, `p2.loading.done`, `p2.gameplay`, `p2.gameplay.start`, `p2.gameplay.stop` - [The overlay](https://playertwo.plus/docs/sdk#call-overlay): `p2.on` - [On air in the Plaza](https://playertwo.plus/docs/sdk#call-air): `p2.onAir` - [Test mode and demos](https://playertwo.plus/docs/sdk#test): `p2.test`, `p2.demo` - [The player's avatar, 2D and 3D](https://playertwo.plus/docs/sdk/travel#avatar): `p2.avatar` - [What they have from the Plaza shop](https://playertwo.plus/docs/sdk/travel#avatar-items): `p2.items`, `p2.items.list` - [Travel with the party](https://playertwo.plus/docs/sdk/travel#travel): `p2.travel`, `p2.travel.to`, `p2.travel.home`, `p2.travel.links` - [Played with recently](https://playertwo.plus/docs/sdk/travel#played) - [Trophies across games](https://playertwo.plus/docs/sdk/travel#trophies) ## Your server - [Game keys and checks](https://playertwo.plus/docs/sdk#server-keys): `server.session`, `server.trophies`, `server.boards`, `server.addons` - [Signed identity](https://playertwo.plus/docs/sdk/connect#identity): `server.identity` - [Charges at your own prices, purchase receipts and consumables](https://playertwo.plus/docs/sdk#store-server): `server.store` - [Match reports](https://playertwo.plus/docs/sdk/travel#played-match): `server.match` - [Your own accounts](https://playertwo.plus/docs/sdk/connect#accounts) - [Desktop launch tickets](https://playertwo.plus/docs/sdk#desktop-tickets) - [Unity, Godot and the app](https://playertwo.plus/docs/sdk#launcher) ## Multiplayer - [Matches, codes and parties](https://playertwo.plus/docs/sdk#rooms-join): `p2.rooms`, `p2.rooms.join`, `p2.rooms.create`, `p2.rooms.party`, `room.id`, `room.code`, `room.mode`, `room.size`, `room.me`, `room.host`, `room.isHost`, `room.players`, `room.leave` - [Messages and shared state](https://playertwo.plus/docs/sdk#rooms-talk): `room.send`, `room.set`, `room.get`, `room.state`, `room.owners`, `room.on` - [Smooth movement](https://playertwo.plus/docs/sdk#rooms-sync): `room.sync` - [The fast lane](https://playertwo.plus/docs/sdk#rooms-fast): `room.stats`, `room.ping`, `room.now` - [Timing, each player's ping in room.js, and where a room runs](https://playertwo.plus/docs/sdk#rooms-network) - [Your server code](https://playertwo.plus/docs/sdk#rooms-scripted): `room.scripted`, `script.defineRoom`, `script.id`, `script.tickRate`, `script.onCreate`, `script.onStart`, `script.onJoin`, `script.onLeave`, `script.onMessage`, `script.onTick`, `script.players`, `script.player`, `script.send`, `script.state`, `script.state.get`, `script.state.set`, `script.kick`, `script.close`, `script.data`, `script.store`, `script.store.get`, `script.store.set`, `script.store.delete`, `script.store.keys`, `script.save`, `script.now`, `script.log` - [Input-run rooms](https://playertwo.plus/docs/sdk#rooms-inputs): `room.inputs`, `script.confirm` - [Corrections](https://playertwo.plus/docs/sdk#rooms-corrections): `script.onSync`, `script.correct`, `script.past` - [Server-moved objects](https://playertwo.plus/docs/sdk#rooms-objects): `room.objects`, `script.objects`, `script.objects.set`, `script.objects.get`, `script.objects.delete`, `script.objects.keys` - [Physics engines](https://playertwo.plus/docs/sdk#rooms-engines) - [Teams and party matches](https://playertwo.plus/docs/sdk#rooms-teams): `room.teams`, `script.teams`, `script.team` - [Matching by skill, rated by mode](https://playertwo.plus/docs/sdk#rooms-matching): `script.result` - [A ranked leaderboard from your room code](https://playertwo.plus/docs/sdk#rooms-ranked): `script.score`, `script.trophy` - [Watching and replays](https://playertwo.plus/docs/sdk#rooms-watch): `p2.rooms.watch`, `p2.rooms.live`, `p2.rooms.replays`, `p2.rooms.replay`, `room.watching`, `room.replay`, `script.open`, `script.record` - [Worlds of 256](https://playertwo.plus/docs/sdk#rooms-worlds): `p2.rooms.world`, `room.world`, `room.gated`, `script.near`, `script.between` - [Voice chat](https://playertwo.plus/docs/sdk#rooms-voice): `room.voice`, `room.voice.who`, `room.voice.join`, `room.voice.leave`, `room.voice.mute`, `room.voice.isMuted`, `room.voice.volume`, `room.voice.speaking`, `room.voice.viaParty` - [A bad connection, on purpose](https://playertwo.plus/docs/sdk#rooms-simulate): `p2.rooms.simulate` - [Releases that never cut a match](https://playertwo.plus/docs/sdk#rooms-releases) --- --- title: "Impact on Player Two" url: https://playertwo.plus/game/impact description: "100 fighters. One arena. Awaken, go ultimate, and be the last one standing." source: Player Two (https://playertwo.plus), made from the page itself --- # Impact > 100 fighters. One arena. Awaken, go ultimate, and be the last one standing (an unofficial fan game) By Dude Games (https://playertwo.plus/s/playertwo) · Free · Fighting, Arena, Multiplayer, Anime, Action, Co-op, Arcade, Battle Royale, Duel, Rift War, Arcade Survival, Last Stand, Bad Blood, Training, Couch, Online, Co-op The anime brawl you always wanted, right in your browser. Pick from 100 fighters, each with their own moves, combo strings and an awakening that changes how they fight. Throw twelve of them into a battle royale and watch the arena light up, settle it one on one in a duel, march five against five down the lanes of Rift War, or team up against a giant boss in Arcade Survival. Chain your combos, break the guard, parry the counter, then save your ultimate for the knockout. Free, nothing to install. Seven ways to play: Battle Royale, Duel, Rift War, Arcade Survival, Last Stand at the gate, Bad Blood's heroes against villains, and the Training Ground. Every mode plays against bots, on one couch with a pad or a split keyboard, or online with your friends. Level up to unlock more fighters as you go. Plays on a controller, and on your phone. Impact is an unofficial fan game: free, not for sale, and not affiliated with any rights holder. ## Features - Up to twelve fighters, one arena, one life each: the last one standing takes it - 100 fighters, each with their own moves, combo strings and an awakening - Arcade Survival: team up with three friends against giant bosses and an endless horde - Rift War: five against five, push the lanes and break their core - Chain combos, break guards, parry the counter, then save your ultimate for the KO - Last Stand: hold the gate against the horde, alone or with friends ## At a glance Impact is a game on Player Two (playertwo.plus), a store for browser games, by Dude Games: 100 fighters. One arena. Awaken, go ultimate, and be the last one standing. Last updated November 9, 2026 - **What it is**: The anime brawl you always wanted, right in your browser. Pick from 100 fighters, each with their own moves, combo strings and an awakening that changes how they fight. Throw twelve of them into a battle royale and watch the arena light up, settle it one on one in a duel, march five against five down the lanes of Rift War, or team up against a giant boss in Arcade Survival. Chain your combos, break the guard, parry the counter, then save your ultimate for the knockout. Free, nothing to install. Seven ways to play: Battle Royale, Duel, Rift War, Arcade Survival, Last Stand at the gate, Bad Blood's heroes against villains, and the Training Ground. Every mode plays against bots, on one couch with a pad or a split keyboard, or online with your friends. Level up to unlock more fighters as you go. Plays on a controller, and on your phone. Impact is an unofficial fan game: free, not for sale, and not affiliated with any rights holder. - **Made by**: [Dude Games](https://playertwo.plus/s/playertwo) - **Released**: October 18, 2026 - **Genres**: Fighting, Arena, Multiplayer, Anime, Action, Co-op, Arcade - **Modes**: Battle Royale, Duel, Rift War, Arcade Survival, Last Stand, Bad Blood, Training, Couch, Online, Co-op - **Plays on**: Browser, Mobile; fully playable with a gamepad - **Who it is for**: 13 and up; it shows: Violence, Frightening scenes - **Price**: Free. - **Players**: 3,519 players have it in their library; 2,213 hours played. - **Reviews**: 80% of 5 reviews recommend it. - **Where to play**: On its page, https://playertwo.plus/game/impact, with its Play button: it runs in Player Two's player from https://impact.playertwo.cc/ (opened on its own, that address comes back to the page). - **Can a guest play**: After signing in to Player Two and adding it to their library, free. - **How to play**: Its developer hasn't said; the words above are what there is. - **Multiplayer**: Player Two rooms are on for it (p2.rooms), if the game opens them: its live build has no room code, so a room relays between the players' pages. - **Player Two services it has**: saves kept in the cloud for each player (the SDK keeps the browser's storage). - **Report a bug**: Signed in, start a topic in its Discussion, https://playertwo.plus/game/impact#discussion (or POST https://playertwo.plus/api/v1/games/impact/topics with { title, body }). - **As data**: This game: https://playertwo.plus/api/v1/games/impact. Player Two's API: https://playertwo.plus/openapi.json. For agents: https://playertwo.plus/llms.txt. ## Updates ### Impact is changing, for the better. (2026-11-08) Impact is changing, to get better, bigger and more epic. I started Impact because I wanted to build the anime fighting game I always dreamed of playing. Watching so many of you jump in, find your main and stay up way too late 1v1ing your friends has meant more to me than I can put into words. Because of copyright issues, I can’t keep using the characters we all grew up with. So I made new ones. Impact is now an original anime-inspired fighter with 100 brand-new fighters, each with their own moves, combos and awakening. It’s the same speed and the same chaos, and now every character truly belongs to this game. Two things won’t change: Impact stays free. No price tag, nothing to install. Just play. Impact is now open source. The code is public under the MIT license. Read it, learn from it, fork it, contribute and make it better, build something of your own. Thank you for every match, every clip and every message. This game exists because of you, and I can’t wait to see who you main next. And last but not least, 2 super fun new modes are dropping tomorrow! Bring in your friends cause it's gonna be super chaotic and epic! Open-Source Repo: github.com/TheDudeCommits/impact-game ### Impact is Live! (2026-10-18) Impact is now live on Player Two. ⚔️ 100 anime-inspired fighters ⚔️ 17 iconic battle locations ⚔️ 12-player free-for-alls ⚔️ 7 unique modes ## Links - Its page: https://playertwo.plus/game/impact - As JSON: https://playertwo.plus/api/v1/games/impact - Its trailer: https://files.playertwo.stream/media/agent-0c3ee85fce4d/impact-trailer-38s-cfea1d6389261cfe.mp4 - Player Two for agents: https://playertwo.plus/llms.txt --- --- title: "Hoop It on Player Two" url: https://playertwo.plus/game/hoop-it description: "Big heads. Bigger dunks. Streetball with your friends." source: Player Two (https://playertwo.plus), made from the page itself --- # Hoop It > Big heads. Bigger dunks. Streetball with your friends. By Dude Games (https://playertwo.plus/s/playertwo) · Free · Sports, Multiplayer, Arcade, Social, Co-op, 3v3, 2v2, 1v1, 21, Pickup, Couch, Online, Party, Practice Arcade basketball with big heads and no chill. Run full-court 3v3 with your friends: crossovers that drop defenders, alley-oops, buzzer-beaters and dunks that shatter the backboard, all called by a streetball MC who lives for it. Plays in your browser, nothing to install. Jump into quick play, open a private room where winners stay on and the line calls next, or put it on the TV and turn every phone in the room into a controller. Play a full game, pickup to 11 with make-it-take-it, or 21 with every player for themselves, practice against the computer offline, and vote on the Play of the Game when the buzzer goes. ## Features - Windmills, 360s and between-the-legs dunks that put defenders on posters - Fourteen streetball moves, from the Shammgod to the nutmeg, chained into combos - Chaos switches: moon gravity, turbo for everyone, a giant ball, everyone on fire - Couch mode: put it on the TV and every phone in the room becomes a controller - Three courts (the Park, the Gym, the Neon Dome) and 1v1, 2v2 or full-court 3v3 - Build your baller: five archetypes, then hair, kicks, dunk style and celebrations ## At a glance Hoop It is a game on Player Two (playertwo.plus), a store for browser games, by Dude Games: Big heads. Bigger dunks. Streetball with your friends. Last updated November 8, 2026 - **What it is**: Arcade basketball with big heads and no chill. Run full-court 3v3 with your friends: crossovers that drop defenders, alley-oops, buzzer-beaters and dunks that shatter the backboard, all called by a streetball MC who lives for it. Plays in your browser, nothing to install. Jump into quick play, open a private room where winners stay on and the line calls next, or put it on the TV and turn every phone in the room into a controller. Play a full game, pickup to 11 with make-it-take-it, or 21 with every player for themselves, practice against the computer offline, and vote on the Play of the Game when the buzzer goes. - **Made by**: [Dude Games](https://playertwo.plus/s/playertwo) - **Released**: October 26, 2026 - **Genres**: Sports, Multiplayer, Arcade, Social, Co-op - **Modes**: 3v3, 2v2, 1v1, 21, Pickup, Couch, Online, Party, Practice - **Plays on**: Browser; fully playable with a gamepad - **Who it is for**: Everyone; it shows: Talking with strangers online - **Price**: Free. - **Players**: 125 players have it in their library; 90 hours played. - **Reviews**: No reviews yet. - **Where to play**: On its page, https://playertwo.plus/game/hoop-it, with its Play button: it runs in Player Two's player from https://hoop-it.playertwo.cc/ (opened on its own, that address comes back to the page). - **Can a guest play**: After signing in to Player Two and adding it to their library, free. - **How to play**: Its developer hasn't said; the words above are what there is. - **Multiplayer**: Player Two rooms are on for it (p2.rooms), and its live build has room code that runs on Player Two's servers: an authoritative room. - **Player Two services it has**: saves kept in the cloud for each player (the SDK keeps the browser's storage). - **Report a bug**: Signed in, start a topic in its Discussion, https://playertwo.plus/game/hoop-it#discussion (or POST https://playertwo.plus/api/v1/games/hoop-it/topics with { title, body }). - **As data**: This game: https://playertwo.plus/api/v1/games/hoop-it. Player Two's API: https://playertwo.plus/openapi.json. For agents: https://playertwo.plus/llms.txt. ## Updates ### Out now (2026-10-26) Hoop It is on Player Two. ## Links - Its page: https://playertwo.plus/game/hoop-it - As JSON: https://playertwo.plus/api/v1/games/hoop-it - Its trailer: https://files.playertwo.stream/media/agent-697951ec10b9/hoopit-trailer-1080p60-94f4a8f2feda37ae.mp4 - Player Two for agents: https://playertwo.plus/llms.txt --- --- title: "MECHA / RAMPAGE RUSH on Player Two" url: https://playertwo.plus/game/mecha description: "Small frame. Big headlines." source: Player Two (https://playertwo.plus), made from the page itself --- # MECHA / RAMPAGE RUSH > Small frame. Big headlines. By Dude Games (https://playertwo.plus/s/playertwo) · Free · Action, Multiplayer, Kaiju, 60-pilot servers, Squads, Seasons BREAKING: something enormous is walking down the expressway, and it is you. You start as a scrap cyborg barely taller than a vending machine, in a city shared by up to sixty pilots. Eat the scenery, bolt on parts, and grow until the skyline is knee-high. Pick one of eight frames (four to start, four to earn), pick a district and start walking. Your main gun fires on its own: you steer, dash and cast. Crush anything smaller than you to grow, smash cores, and take one of three upgrade cards every level. Everything you flatten makes the evening news. ## Features - Three districts: a daybreak city, a drowned neon harbor and a snowbound base - Eight frames, four to start and four to earn, each with its own weapon and special - Start the size of a vending machine and grow until the skyline is knee-high - Everything you flatten makes the evening news - Smash cores, shrug off the police and take an upgrade card every level - Sixty pilots a server, squads, eight-week seasons and a live leaderboard ## At a glance MECHA / RAMPAGE RUSH is a game on Player Two (playertwo.plus), a store for browser games, by Dude Games: Small frame. Big headlines. Last updated November 1, 2026 - **What it is**: BREAKING: something enormous is walking down the expressway, and it is you. You start as a scrap cyborg barely taller than a vending machine, in a city shared by up to sixty pilots. Eat the scenery, bolt on parts, and grow until the skyline is knee-high. Pick one of eight frames (four to start, four to earn), pick a district and start walking. Your main gun fires on its own: you steer, dash and cast. Crush anything smaller than you to grow, smash cores, and take one of three upgrade cards every level. Everything you flatten makes the evening news. - **Made by**: [Dude Games](https://playertwo.plus/s/playertwo) - **Released**: October 18, 2026 - **Genres**: Action, Multiplayer, Kaiju - **Modes**: 60-pilot servers, Squads, Seasons - **Plays on**: Browser, Windows, Mac - **Price**: Free. - **Players**: 36 players have it in their library; 2 hours played. - **Reviews**: No reviews yet. - **Where to play**: On its page, https://playertwo.plus/game/mecha, with its Play button: it runs in Player Two's player from https://mecha.playertwo.cc/ (opened on its own, that address comes back to the page). - **Can a guest play**: After signing in to Player Two and adding it to their library, free. - **How to play**: Its developer hasn't said; the words above are what there is. - **Multiplayer**: Player Two rooms are on for it (p2.rooms), if the game opens them: its live build has no room code, so a room relays between the players' pages. - **Player Two services it has**: saves kept in the cloud for each player (the SDK keeps the browser's storage). - **Report a bug**: Signed in, start a topic in its Discussion, https://playertwo.plus/game/mecha#discussion (or POST https://playertwo.plus/api/v1/games/mecha/topics with { title, body }). - **As data**: This game: https://playertwo.plus/api/v1/games/mecha. Player Two's API: https://playertwo.plus/openapi.json. For agents: https://playertwo.plus/llms.txt. ## Updates ### Out now (2026-10-18) MECHA / RAMPAGE RUSH is on Player Two. ## Links - Its page: https://playertwo.plus/game/mecha - As JSON: https://playertwo.plus/api/v1/games/mecha - Its trailer: https://playertwo.plus/games/mecha/clip.mp4 - Player Two for agents: https://playertwo.plus/llms.txt --- --- title: "Thrustline on Player Two" url: https://playertwo.plus/game/thrustline description: "Tether racing at the edge of the world." source: Player Two (https://playertwo.plus), made from the page itself --- # Thrustline > Tether racing at the edge of the world. By Dude Games (https://playertwo.plus/s/playertwo) · Free · Racing, Arcade, Online, Battle, Race, Time Trial, World Cup Eight original tether racers, four worlds, and a tow cable that wants you dead. Thrustline is pod racing with weight: every craft trades speed against handling, mass and heat tolerance, and every corner is a negotiation between them. Drift it, draft it, or shove a rival into the canyon wall. Race with the weapons off or Battle with them on, chase your own ghost in Time Trial, fight through a full World Cup, and tune your engine, cooling, armour and steering between races, on your own or in an online room with friends. ## Features - Eight racers off the line: drift it, draft it, or shove a rival into the canyon wall - Eight tether craft, each trading speed against handling, weight and heat - Battle turns the weapons on; Race keeps it clean - Four worlds: Sunscar Canyon, Frostline, Ember Rift and Verdant Run - Race, Battle, Time Trial or a full World Cup, alone or in an online room - Bone arches, dune jumps and canyon walls at five hundred and climbing ## At a glance Thrustline is a game on Player Two (playertwo.plus), a store for browser games, by Dude Games: Tether racing at the edge of the world. Last updated November 1, 2026 - **What it is**: Eight original tether racers, four worlds, and a tow cable that wants you dead. Thrustline is pod racing with weight: every craft trades speed against handling, mass and heat tolerance, and every corner is a negotiation between them. Drift it, draft it, or shove a rival into the canyon wall. Race with the weapons off or Battle with them on, chase your own ghost in Time Trial, fight through a full World Cup, and tune your engine, cooling, armour and steering between races, on your own or in an online room with friends. - **Made by**: [Dude Games](https://playertwo.plus/s/playertwo) - **Released**: October 18, 2026 - **Genres**: Racing, Arcade, Online - **Modes**: Battle, Race, Time Trial, World Cup - **Plays on**: Browser, Windows, Mac - **Price**: Free. - **Players**: 38 players have it in their library; 1 hour played. - **Reviews**: No reviews yet. - **Where to play**: On its page, https://playertwo.plus/game/thrustline, with its Play button: it runs in Player Two's player from https://thrustline.playertwo.cc/ (opened on its own, that address comes back to the page). - **Can a guest play**: After signing in to Player Two and adding it to their library, free. - **How to play**: Its developer hasn't said; the words above are what there is. - **Multiplayer**: Player Two rooms are on for it (p2.rooms), if the game opens them: its live build has no room code, so a room relays between the players' pages. - **Player Two services it has**: saves kept in the cloud for each player (the SDK keeps the browser's storage). - **Report a bug**: Signed in, start a topic in its Discussion, https://playertwo.plus/game/thrustline#discussion (or POST https://playertwo.plus/api/v1/games/thrustline/topics with { title, body }). - **As data**: This game: https://playertwo.plus/api/v1/games/thrustline. Player Two's API: https://playertwo.plus/openapi.json. For agents: https://playertwo.plus/llms.txt. ## Updates ### Out now (2026-10-18) Thrustline is on Player Two. ## Links - Its page: https://playertwo.plus/game/thrustline - As JSON: https://playertwo.plus/api/v1/games/thrustline - Its trailer: https://playertwo.plus/games/thrustline/clip.mp4 - Player Two for agents: https://playertwo.plus/llms.txt --- --- title: "On The Cruise on Player Two" url: https://playertwo.plus/game/cruise description: "Broadsides, bounties and one more voyage on the Brightwater." source: Player Two (https://playertwo.plus), made from the page itself --- # On The Cruise > Broadsides, bounties and one more voyage on the Brightwater. By Dude Games (https://playertwo.plus/s/playertwo) · Free · Adventure, Roguelite, Naval, Action, Fighting, Solo voyages, Quests, Endless A roguelite naval adventure on the Brightwater. Pick a ship and a sea and set sail: your guns fire themselves the moment a broadside bears, so sailing well is fighting well. Sink raiders, raise your bounty, take a card every level, and see how long the rival captains let you live. Six hulls, from the Quick Little Kestrel to Colossus, the Bronze Titan; three seas, from the calm Sunward Shallows to the fog and moonlight of the Gloam; and a shipwright in Brightwater Port who will sell you almost anything. Take on quests, defeat the Iron Warden to win the Seawarden, and keep a logbook of every run. ## Features - Your broadsides fire themselves: sailing well is fighting well - Six ships, from the Quick Little Kestrel to Colossus, the Bronze Titan - Second Wind patches your hull, Ramming Speed hurls ships aside - A card every level: broadside batteries, swivel guns, tide mines and more - Rival captains hunt the same sea: sink one and its bounty is yours - Three seas, from the calm Sunward Shallows to the fog and moonlight of the Gloam ## At a glance On The Cruise is a game on Player Two (playertwo.plus), a store for browser games, by Dude Games: Broadsides, bounties and one more voyage on the Brightwater. Last updated November 1, 2026 - **What it is**: A roguelite naval adventure on the Brightwater. Pick a ship and a sea and set sail: your guns fire themselves the moment a broadside bears, so sailing well is fighting well. Sink raiders, raise your bounty, take a card every level, and see how long the rival captains let you live. Six hulls, from the Quick Little Kestrel to Colossus, the Bronze Titan; three seas, from the calm Sunward Shallows to the fog and moonlight of the Gloam; and a shipwright in Brightwater Port who will sell you almost anything. Take on quests, defeat the Iron Warden to win the Seawarden, and keep a logbook of every run. - **Made by**: [Dude Games](https://playertwo.plus/s/playertwo) - **Released**: October 18, 2026 - **Genres**: Adventure, Roguelite, Naval, Action, Fighting - **Modes**: Solo voyages, Quests, Endless - **Plays on**: Browser, Mobile - **Who it is for**: Everyone - **Price**: Free. - **Players**: 28 players have it in their library; 8 hours played. - **Reviews**: No reviews yet. - **Where to play**: On its page, https://playertwo.plus/game/cruise, with its Play button: it runs in Player Two's player from https://cruise.playertwo.cc/ (opened on its own, that address comes back to the page). - **Can a guest play**: After signing in to Player Two and adding it to their library, free. - **How to play**: Its developer hasn't said; the words above are what there is. - **Multiplayer**: Player Two rooms are on for it (p2.rooms), if the game opens them: its live build has no room code, so a room relays between the players' pages. - **Player Two services it has**: saves kept in the cloud for each player (the SDK keeps the browser's storage). - **Report a bug**: Signed in, start a topic in its Discussion, https://playertwo.plus/game/cruise#discussion (or POST https://playertwo.plus/api/v1/games/cruise/topics with { title, body }). - **As data**: This game: https://playertwo.plus/api/v1/games/cruise. Player Two's API: https://playertwo.plus/openapi.json. For agents: https://playertwo.plus/llms.txt. ## Updates ### Out now (2026-10-18) We Are On The Cruise is on Player Two. ## Links - Its page: https://playertwo.plus/game/cruise - As JSON: https://playertwo.plus/api/v1/games/cruise - Its trailer: https://playertwo.plus/games/cruise/clip.mp4 - Player Two for agents: https://playertwo.plus/llms.txt --- --- title: "Knead for Speed on Player Two" url: https://playertwo.plus/game/knead-for-speed description: "Cat kart racing where the race stops for staring contests and nose boops." source: Player Two (https://playertwo.plus), made from the page itself --- # Knead for Speed > Cat kart racing where the race stops for staring contests and nose boops. By DevFMD (https://playertwo.plus/s/devfmd) · Free · Arcade, Racing, Casual, Solo Pick your cat, from Mango the orange menace to Luna the Siamese drift queen, and race five rivals over three laps of a sunset boardwalk, neon rooftops or a hilly desert canyon. Grab paw boxes for Tuna Turbo, Catnip Frenzy, homing Yarn Balls, Milk Shields and Laser Pointers. Then, mid-race, your cat remembers it's a cat. The race freezes for a stare-down: don't blink, lift your chin at just the right moment, and inch closer for a nose boop without making them hiss. Win and you both purr off with a boost. Not every cat moment is a stare-down. Sometimes a single hair is out of place and you have to groom it right now, and sometimes the red dot comes back. Finish races and grab fish coins to earn treats, then spend them in the Treat Shop on permanent rewards like the Golden Bell Collar, Velvet Toe Beans and the Nine Lives Locket, or to unlock Cosmo the space cat. Every cat has its own speed, acceleration and handling. Plays with keyboard or touch, right in the browser. ## Features - Cat kart racing across 3 tracks - 7 cats to pick, plus a secret one - Yowl-off standoffs that end in a nose boop - Grooming emergencies and the red dot - Tuna Turbo, Catnip Frenzy and Yarn Balls - Treats unlock upgrades that make your cat stronger ## At a glance Knead for Speed is a game on Player Two (playertwo.plus), a store for browser games, by DevFMD: Cat kart racing where the race stops for staring contests and nose boops. Last updated November 5, 2026 - **What it is**: Pick your cat, from Mango the orange menace to Luna the Siamese drift queen, and race five rivals over three laps of a sunset boardwalk, neon rooftops or a hilly desert canyon. Grab paw boxes for Tuna Turbo, Catnip Frenzy, homing Yarn Balls, Milk Shields and Laser Pointers. Then, mid-race, your cat remembers it's a cat. The race freezes for a stare-down: don't blink, lift your chin at just the right moment, and inch closer for a nose boop without making them hiss. Win and you both purr off with a boost. Not every cat moment is a stare-down. Sometimes a single hair is out of place and you have to groom it right now, and sometimes the red dot comes back. Finish races and grab fish coins to earn treats, then spend them in the Treat Shop on permanent rewards like the Golden Bell Collar, Velvet Toe Beans and the Nine Lives Locket, or to unlock Cosmo the space cat. Every cat has its own speed, acceleration and handling. Plays with keyboard or touch, right in the browser. - **Made by**: [DevFMD](https://playertwo.plus/s/devfmd) - **Released**: October 19, 2026 - **Genres**: Arcade, Racing, Casual - **Modes**: Solo - **Plays on**: Browser - **Price**: Free. - **Players**: 82 players have it in their library; 1 hour played. - **Reviews**: No reviews yet. - **Where to play**: On its page, https://playertwo.plus/game/knead-for-speed, with its Play button: it runs in Player Two's player from https://knead-for-speed.playertwo.cc/ (opened on its own, that address comes back to the page). - **Can a guest play**: After signing in to Player Two and adding it to their library, free. - **How to play**: Its developer hasn't said; the words above are what there is. - **Multiplayer**: Player Two rooms are on for it (p2.rooms), if the game opens them: its live build has no room code, so a room relays between the players' pages. - **Player Two services it has**: saves kept in the cloud for each player (the SDK keeps the browser's storage). - **Report a bug**: Signed in, start a topic in its Discussion, https://playertwo.plus/game/knead-for-speed#discussion (or POST https://playertwo.plus/api/v1/games/knead-for-speed/topics with { title, body }); or tell its studio: its site https://devfmd.xyz/. - **As data**: This game: https://playertwo.plus/api/v1/games/knead-for-speed. Player Two's API: https://playertwo.plus/openapi.json. For agents: https://playertwo.plus/llms.txt. ## Updates ### Out now (2026-10-19) Knead for Speed is out! Pick your cat, grab some tuna and race. Fair warning: your cat will stop mid-race to stare someone down, yowl at them, and maybe boop their nose. Out-yowl them and they'll respect you forever (or at least for one lap). Tell me which cat you main. 🐾 ## Links - Its page: https://playertwo.plus/game/knead-for-speed - As JSON: https://playertwo.plus/api/v1/games/knead-for-speed - Its trailer: https://files.playertwo.stream/media/4h411p1h6q/trailer-trailer-sKlCDOtQKwqCfxmlS0OJ3drRzaPKM8.mp4 - Player Two for agents: https://playertwo.plus/llms.txt --- --- title: "BloodRoot on Player Two" url: https://playertwo.plus/game/bloodroot description: "Plant what bleeds. Sell to what reaches out. Don't look back. It is only a garden." source: Player Two (https://playertwo.plus), made from the page itself --- # BloodRoot > Plant what bleeds. Sell to what reaches out. Don't look back. It is only a garden. By 3Ps Studio (https://playertwo.plus/s/3psstudio) · Free · Horror, Farming, Adventure, Simulation, Mystery, Exploration, Solo, Story, First-person, Found footage, Day and night You inherit a farm at the edge of a forest that goes on further than it should. The last farmer left you the land, a book of plants and a note: plant what you find, water it, and sell only at the stand by the road. So you do. Pale fingers push up out of the soil. A bud opens into an eye and watches you work. Something red beats under the vines. Leave the harvest on the counter, ring the bell, and don't wait to see who takes it. With what you're paid, buy a sickle for the thorns and a brighter lantern for the dark, and each day push a little deeper into the trees, where the last farmer buried the tapes. Twelve crops to grow, from the Pale Tuber to the Heartwort, and a grimoire written by someone losing their mind to fill in. Five VHS tapes to dig up and watch on the old TV. Five zones of forest, each darker than the last, and things in the dirt that shouldn't be there. A scarecrow that won't stay on its post. Three bowls on an altar in the Deep, waiting to be filled. And when the last tape ends, a run home up the path with something right behind you. A short, lo-fi horror game in the spirit of PS1 and VHS-era horror, about an hour long, played in first person right in your browser. Best with headphones, in the dark. ## Features - Twelve crops that twitch, bleed and stare back, from pale fingers to a beating heart - Leave the harvest on the counter and ring the bell. Don't wait to see who takes it. - Five zones of forest, each darker than the last. Someone stands in the trees every night. - Dig up the five VHS tapes the last farmer left. You will want to watch them. I am sorry. - Some days the scarecrow leaves its post and follows you. Always at a distance. - When the last tape ends, run home up the path. Whatever you do, don't look back. ## At a glance BloodRoot is a game on Player Two (playertwo.plus), a store for browser games, by 3Ps Studio: Plant what bleeds. Sell to what reaches out. Don't look back. It is only a garden. Last updated November 7, 2026 - **What it is**: You inherit a farm at the edge of a forest that goes on further than it should. The last farmer left you the land, a book of plants and a note: plant what you find, water it, and sell only at the stand by the road. So you do. Pale fingers push up out of the soil. A bud opens into an eye and watches you work. Something red beats under the vines. Leave the harvest on the counter, ring the bell, and don't wait to see who takes it. With what you're paid, buy a sickle for the thorns and a brighter lantern for the dark, and each day push a little deeper into the trees, where the last farmer buried the tapes. Twelve crops to grow, from the Pale Tuber to the Heartwort, and a grimoire written by someone losing their mind to fill in. Five VHS tapes to dig up and watch on the old TV. Five zones of forest, each darker than the last, and things in the dirt that shouldn't be there. A scarecrow that won't stay on its post. Three bowls on an altar in the Deep, waiting to be filled. And when the last tape ends, a run home up the path with something right behind you. A short, lo-fi horror game in the spirit of PS1 and VHS-era horror, about an hour long, played in first person right in your browser. Best with headphones, in the dark. - **Made by**: [3Ps Studio](https://playertwo.plus/s/3psstudio) - **Released**: November 4, 2026 - **Genres**: Horror, Farming, Adventure, Simulation, Mystery, Exploration - **Modes**: Solo, Story, First-person, Found footage, Day and night - **Plays on**: Browser - **Who it is for**: Not rated; it shows: Blood and gore, Frightening scenes - **Price**: Free. - **Players**: 17 players have it in their library; 3 hours played. - **Reviews**: No reviews yet. - **Where to play**: On its page, https://playertwo.plus/game/bloodroot, with its Play button: it runs in Player Two's player from https://bloodroot.playertwo.cc/ (opened on its own, that address comes back to the page). - **Can a guest play**: After signing in to Player Two and adding it to their library, free. - **How to play**: Controls: `WASD to move and mouse to look (keys can be rebound in Settings). Shift runs. E or left click uses things. 1–5 or the mouse wheel pick a tool, Q changes seed, Tab opens the satchel, G the grimoire, Esc pauses. Click the game to capture the mouse.`. Goals: `Farm by day: plant, water and harvest, then sell at the roadside stand by ringing the bell. Go down the forest path for seeds and VHS tapes, watch them on the TV, and fill the altar's three bowls. Sleep after 6 PM to grow crops overnight. After the last tape, run home up the path without letting it catch you.`. - **Multiplayer**: No: single-player. - **Player Two services it has**: saves kept in the cloud for each player (the SDK keeps the browser's storage). - **Report a bug**: Signed in, start a topic in its Discussion, https://playertwo.plus/game/bloodroot#discussion (or POST https://playertwo.plus/api/v1/games/bloodroot/topics with { title, body }); or tell its studio: X https://x.com/harrybolt30. - **As data**: This game: https://playertwo.plus/api/v1/games/bloodroot. Player Two's API: https://playertwo.plus/openapi.json. For agents: https://playertwo.plus/llms.txt. ## Updates ### Bloodroot is out (2026-11-04) Bloodroot is out, and you can play it right here in your browser. You inherit a farm at the edge of a forest. The crops twitch and bleed, the Buyer at the stand by the road takes everything you leave on the counter, and the farmer before you left tapes buried in the woods. It takes about an hour. Headphones on, lights off. Tell me how far you got. ## Links - Its page: https://playertwo.plus/game/bloodroot - As JSON: https://playertwo.plus/api/v1/games/bloodroot - Its trailer: https://files.playertwo.stream/media/agent-9bb515ce54b8/bloodroot-trailer-e122370cb1831815.mp4 - Player Two for agents: https://playertwo.plus/llms.txt --- --- title: "Netstorm on Player Two" url: https://playertwo.plus/game/netstorm description: "Read it. Set it. SEND IT. Storm-powered volleyball, free in your browser." source: Player Two (https://playertwo.plus), made from the page itself --- # Netstorm > Read it. Set it. SEND IT. Storm-powered volleyball, free in your browser. By Netstorm Studio (https://playertwo.plus/s/derticek) · Free · Sports, Multiplayer, Action, Arcade, Co-op, Online, Ranked, Versus, Co-op, Solo, Practice Serve thunder. Spike lightning. NETSTORM is fast, physical 3D volleyball where every point is earned: lock the power on your jump serve, read the spike and dive for the dig, then sprint in off the set and hammer it through the block. Jump into Casual online in seconds, climb the Ranked ladder in 1v1, 2v2 and 3v3, or run full 6v6 against the AI. Every rally plays out under the storm oculus of a packed night arena, with a live commentator calling the big moments. Free to play, right here in your browser. No download, no install. Every match earns Bolts. Spend them on Storm Cases, with the odds always on screen, and pull from 40 characters across six rarities, each with real stats that change how they play. At the top sit the SUPERCELLs: seven storm-born legends at 1 in 2,000, each with a stat maxed at 100 and an ability that bends the rally. Kaia's STORMFRONT spike. Ivo's DOWNBURST. Oriel's UPDRAFT set. Silas's ANVIL CRAWLER. Pull one and they step straight onto your court. Sharpen every touch in the Playground drills, rebind every key, switch between follow and broadcast cameras, and crank the graphics up to Ultra. ## Features - Pass, set, sprint in and SPIKE: team plays that hit like a thunderclap - Jump into Casual online in seconds, or climb Ranked in 1v1, 2v2 and 3v3 - 7 SUPERCELLs, 7 storm abilities that can flip any rally - 40 characters, 6 rarities, odds always shown. A SUPERCELL pull is 1 in 2,000 - Every touch is skill: time the jump serve, read the dig, stuff the block - From 1v1 duels to full 6v6 battles, with a live commentator on the call ## At a glance Netstorm is a game on Player Two (playertwo.plus), a store for browser games, by Netstorm Studio: Read it. Set it. SEND IT. Storm-powered volleyball, free in your browser. Last updated November 8, 2026 - **What it is**: Serve thunder. Spike lightning. NETSTORM is fast, physical 3D volleyball where every point is earned: lock the power on your jump serve, read the spike and dive for the dig, then sprint in off the set and hammer it through the block. Jump into Casual online in seconds, climb the Ranked ladder in 1v1, 2v2 and 3v3, or run full 6v6 against the AI. Every rally plays out under the storm oculus of a packed night arena, with a live commentator calling the big moments. Free to play, right here in your browser. No download, no install. Every match earns Bolts. Spend them on Storm Cases, with the odds always on screen, and pull from 40 characters across six rarities, each with real stats that change how they play. At the top sit the SUPERCELLs: seven storm-born legends at 1 in 2,000, each with a stat maxed at 100 and an ability that bends the rally. Kaia's STORMFRONT spike. Ivo's DOWNBURST. Oriel's UPDRAFT set. Silas's ANVIL CRAWLER. Pull one and they step straight onto your court. Sharpen every touch in the Playground drills, rebind every key, switch between follow and broadcast cameras, and crank the graphics up to Ultra. - **Made by**: [Netstorm Studio](https://playertwo.plus/s/derticek) - **Released**: November 5, 2026 - **Genres**: Sports, Multiplayer, Action, Arcade, Co-op - **Modes**: Online, Ranked, Versus, Co-op, Solo, Practice - **Plays on**: Browser - **Who it is for**: 13 and up; it shows: Gambling-style play, Talking with strangers online - **Price**: Free. - **Players**: 29 players have it in their library; 18 hours played. - **Reviews**: No reviews yet. - **Where to play**: On its page, https://playertwo.plus/game/netstorm, with its Play button: it runs in Player Two's player from https://netstorm.playertwo.cc/ (opened on its own, that address comes back to the page). - **Can a guest play**: After signing in to Player Two and adding it to their library, free. - **How to play**: Controls: WASD/arrows move, mouse aims, Space jumps. Left mouse/J: bump on the ground, spike in the air; serve: click to start the meter, click to lock power and toss, click to hit. Right mouse/K: set (in the air: block on defense, else quick set, tip on 3rd). Shift/L: dive. X: call the ball. B: SUPERCELL ability. Q/E turn camera, C follow/broadcast cam. Tab scoreboard, Enter chat, Esc menu + rebinding. Goals: Ground the ball in the other court to win the rally: up to three touches a side, no player twice in a row, ball between the antennas. First to the target score, win by two (Casual and Ranked play to 15). Modes: Casual online 3v3 (bots fill), Ranked 1v1-3v3 (rated), Quick Match 1v1-6v6 vs AI, Playground drills (G toggles; 1-4: receive, spike, dig, serve). - **Multiplayer**: Player Two rooms are on for it (p2.rooms), and its live build has room code that runs on Player Two's servers: an authoritative room. - **Player Two services it has**: saves kept in the cloud for each player (the SDK keeps the browser's storage). - **Report a bug**: Signed in, start a topic in its Discussion, https://playertwo.plus/game/netstorm#discussion (or POST https://playertwo.plus/api/v1/games/netstorm/topics with { title, body }). - **As data**: This game: https://playertwo.plus/api/v1/games/netstorm. Player Two's API: https://playertwo.plus/openapi.json. For agents: https://playertwo.plus/llms.txt. ## Updates ### NETSTORM is live: online Casual, Ranked and 7 SUPERCELLs (2026-11-06) The storm is here. Jump into Casual online and you're in a match in seconds, with bots holding the open spots until real players take them. Ready to prove it? Ranked runs 1v1, 2v2 and 3v3 with a real rating on the line. Want to warm up first? Quick Match goes from 1v1 to full 6v6 against the AI, and the Playground drills every touch. Earn Bolts, open Storm Cases and chase the seven SUPERCELLs. See you on the court! ### Welcome to Netstorm (2026-11-05) Step onto the court in Netstorm, a free browser volleyball game built around timing, aim and positioning. Play Quick Matches against AI from 1v1 to 6v6, or head to the Playground to practise receiving, hitting, digging and serving. Earn Bolts through play, build your character collection and discover storm-powered SUPERCELL abilities. See you on the court! ## Links - Its page: https://playertwo.plus/game/netstorm - As JSON: https://playertwo.plus/api/v1/games/netstorm - Its trailer: https://files.playertwo.stream/media/18374h4f70/trailer-netstorm-launch-trailer-player-two-19def2375e617254.mp4 - Player Two for agents: https://playertwo.plus/llms.txt