| name | portals-game-economy |
| description | Sell in-game products for Coins inside a hosted Portals web game with Portals.economy — microtransactions, IAP, durable and consumable SKUs, purchase confirmation, player inventory, and spending consumables. Use when working with Portals.economy.getCatalog, getInventory, purchase, consume, operationId idempotency, the product catalog behind get_game_economy_catalog and update_game_economy_catalog, the purchase sandbox, or why a live purchase is blocked. |
In-game purchases
Portals.economy is how a hosted game sells to its own players. The currency is Coins, which players already hold on Portals; the game never sees a card, a dollar price, or a wallet balance, and it never renders a payment form. Portals owns the confirmation UI and the money.
Read references/portals-game-economy.md for the full API, the catalog fields, every error code, and worked examples.
The catalog is not in the game's code
A game can only sell a SKU that exists in its catalog, authored per game and pinned to a release. purchase("anything_else") cannot invent a product. So the order of work is catalog first, code second:
list_web_games — resolve the game ID when it is not already known.
get_game_economy_catalog — read what the game already sells, and its draft_revision.
update_game_economy_catalog — draft a SKU (upsert), or withdraw one (retire).
- Write game code against those exact SKU strings.
test_game_economy_purchase — run it against the owner's simulated wallet.
Portals review approval releases the exact approved draft: from that moment real-Coin purchases work in draft launches — the editor preview and the staging share link — before any publish. Publishing then captures the approved catalog into the immutable live release. Editing the draft closes draft selling until the next approval, and never changes what a live game charges today.
When the user asks to add microtransactions, call these tools yourself; do not send them to My Games for ordinary draft setup. If they did not specify every field, form a small coherent proposal from the requested game design and state the SKU/price/kind/grant/limit assumptions. Confirm a new SKU and its kind before its first upsert because both are permanent catalog identity choices; then perform the catalog writes yourself. Read before every write, preserve all intended fields because upsert replaces the whole product, and carry the returned revision into the next write. A permanent retirement still requires explicit intent.
A product is one of two kinds, and the choice is permanent for that SKU:
durable — bought once, owned forever. grantQuantity is always 1. Remove-ads, a character, a level pack.
consumable — grants a stackable grantQuantity the game spends with consume(). Lives, hints, currency packs.
Wiring
Portals.economy ships in the managed SDK; there is nothing extra to include.
<script src="./_portals/sdk.js"></script>
await Portals.ready();
const products = await Portals.economy.getCatalog();
const owned = await Portals.economy.getInventory();
Buying must start from a real player gesture:
buyButton.addEventListener("click", async () => {
const result = await Portals.economy.purchase("extra_lives_5");
if (result.status === "cancelled") return;
const inventory = await Portals.economy.getInventory();
applyEntitlements(inventory);
});
Spending a consumable is idempotent on an operationId the game chooses. Reuse the same id when retrying the same gameplay event, and a fresh one for a new event:
const result = await Portals.economy.consume("extra_lives_5", 1, `revive-${runId}-${deathCount}`);
lives = result.quantity;
Rules that are easy to get wrong
- A cancelled purchase is a normal outcome, not a rejection.
purchase() resolves with status: "cancelled" when the player dismisses the Portals confirmation. Code that only handles the promise rejecting will silently grant nothing and say nothing.
purchase() must be called directly from a click or tap handler. Calling it after an await, from a timer, or on load rejects with PLAYER_ACTION_REQUIRED — the player gesture has expired by then. Do the awaiting after the purchase, not before it.
- Never grant an entitlement the server did not. Award only what
purchase() and consume() return. Client-reported scores and peer messages must never move Coins, inventory, or access — see the portals-sdk and portals-multiplayer-and-voice skills.
consume() needs a stable operationId, 8–128 URL-safe characters. A random id per retry double-spends the player's items on a flaky network; that is the whole reason the argument exists. Derive it from the gameplay event (run-42-revive-1), never from Math.random() at the call site.
- Re-read inventory rather than tracking it locally. The player may own things from an earlier session, another device, or a purchase made mid-session.
getInventory() is the truth; a local counter is a guess.
- Outside a Portals host there is no economy. On a local dev server
getCatalog() and getInventory() resolve to [], and purchase()/consume() reject. Keep the game playable — gate the shop UI on a non-empty catalog instead of assuming it is there.
- Retiring a SKU is permanent. The id is burned for the game's lifetime and cannot be re-created, because players who bought it keep an entitlement that must keep resolving. Code that reads a retired SKU has to keep working.
- A price is 10–5,400 Coins, and a game may hold 50 active and 100 lifetime SKUs. Players also have daily spend caps across a game and across Portals, so a purchase can fail with
DAILY_GAME_LIMIT even though the player has the Coins.
- Icons are game files.
iconPath points inside the game's own pushed source (assets/lives.png); a CDN URL fails the published game's CSP exactly like every other external asset.
When purchases are blocked
ECONOMY_UNAVAILABLE or NOT_READY from a hosted game is usually not a code bug. Real purchases additionally require the owner's monetization readiness — verified email, Stripe identity verification, account in good standing, and an operator-approved catalog: the reviewed catalog on the current release for live plays, or the approved current draft for editor-preview and staging-link plays — and access is still rolling out to creators in stages. None of that is visible to the MCP by design; the owner reads it at portals.to/my-games → Economy. Say so plainly rather than rewriting working purchase code.
Related
- Identity, saves, and leaderboards: the
portals-sdk skill. Sign-in is a precondition for any purchase.
- Pushing, testing, and publishing the game itself: the
portals-web-games skill.
- Keeping a multiplayer session from awarding entitlements: the
portals-multiplayer-and-voice and portals-server-scripts skills.