in-game-purchases is a skill from the Summer skill library: a guide your AI agent reads when the task matches, so it builds the good version on the first try. Sell items for Sparks via the Summer purchase sheet: checkout, not enough Sparks, unlocks, grants.
When your agent uses it
- Adding a shop, buy button or paid item to a Summer game and wiring request_checkout
- The player does not have enough Sparks (checkout rejected with insufficient_funds) and the game should tell them how to buy more Sparks
- Deciding whether an item should be sold for Sparks, or handling pending, declined, expired, rejected or manual_review purchase results
Install
summer setup installs every skill for your agent. To add only this one:
claude-code for your agent, such as codex or cursor. See Skills for every option.
The skill
Sparks are Summer’s currency. A game never takes money, never holds a balance and never approves a payment. It asks Summer for a checkout, Summer shows its own purchase sheet over the game, and the game reacts to the authoritative result.
The engine has no call that sells Sparks from inside the game. When a
checkout fails with
insufficient_funds, tell the player they need more
Sparks and that they can get them in the Summer app, then let them press Buy
again.
When to sell with Sparks
- Sell anything that persists across sessions or devices (cosmetics, unlocks, passes) as a Summer item with a Sparks price. Why: the platform owns the item, so ownership survives reinstalls and cannot be forged by editing the game.
- Do not invent your own currency that is bought with money. Why: Summer can only refund, protect and pay you out for Sparks purchases.
- Open the shop only from an explicit shop or buy button. Why: the Summer sheet interrupts play; surprise payment prompts read as pressure and get games reported.
Rules and the reason for each
- Never grant an item from a button press or a receipt. Unlock only when
Summer.client.itemsshows the player owns it. Why: the receipt proves the debit; the inventory projection is what the platform guarantees the player owns, including after a crash or on another device. - Never send prices or amounts. The game passes an offer ID and a
quantity (
SummerCheckoutLine.create(offer_id, quantity)). Why: the platform prices everything; a client-supplied price is exactly what a cheater edits. - One call is one purchase. Every
request_checkoutcreates a new identity. Never call again to “retry” an unknown outcome (SummerCheckoutOperation.is_outcome_unknown()). Why: the first request may already be charged; the SDK recovers it and delivers the result on the signal. - Handle results in
checkout_status_changed, and deduplicate byrequest_idplusversion. Connect normally (not deferred) before requesting. To draw a checkout panel, readget_checkout_statuses()andhas_pending_checkouts()instead of keeping your own list. Why: results can arrive after a restart or twice after a crash; a synchronous handler is how the SDK knows the result was delivered. - Treat
manual_reviewandpendingas not done. Why: the platform has not decided yet; showing success or failure would be a guess. - Check readiness and hide what is unavailable.
check_checkout_readiness()(buying) andcheck_browse_readiness()(listing withlist_items()) return aSummerResult. Why: editor play, guest sessions and older Summer apps do not offer purchases; a dead button is worse than no button.
Result states
checkout_status_changed(status: SummerCheckoutStatus) delivers a status
whose state is one of:
status.is_terminal() tells you whether the state can still change. Unknown
future error_code values never mean success.
The operation returned by request_checkout also completes: ok on
purchased, a failure otherwise. The status signal is the one to build UI on.
Minimal complete example
Multiplayer: granting an effect on a dedicated authority
Anything the Sparks buy (“Grow now”, “Summon rain”) is a checkout for an item, and on a server the authority must never grant it because a client says it paid. Why: a client message is the one thing a cheater controls. Model the effect as a catalog item with instance ownership (each purchase is a separate owned item, not a stack). Why: a stackable item shares one owned item ID across purchases, so the ID could not tell two purchases apart. The client checks out; the receipt’sitems[i].owned_item_ids are the owned
items. The client sends the authority only an owned item ID. The authority:
- reads that player’s inventory with
Summer.authority.items.inventory_for_session(session)and checks the item is there, owned, and of the right definition; - checks its own record of redeemed item IDs, kept in the player’s secret
player data (
Summer.authority.player_data), so the item grants once per player in every world; - writes the new record, and applies the effect only after that write succeeds.
- The save ID comes from the owned item ID. Why: resending the same save ID
with the same content returns the earlier commit (
commit.replayed), so a retry after a timeout records the redemption once. - Apply the effect when the commit succeeds even if
commit.replayedis true and the effect is not in your world yet. Why: a replay means the redemption was recorded earlier, but your world may have crashed before it applied the effect. - The engine has no call that uses up an owned item. The item stays in the player’s inventory after it is redeemed; the redeemed list is what stops a second grant. Show redeemed items as used in your UI.
Idempotency
The SDK generates the checkout request ID and replays it across transport failures and app restarts; the platform debits a checkout once per request. Your only job is therequest_id:version dedupe above, so a replayed signal
does not show a message or play an effect twice.
Per-surface behaviour
- Summer Games app on iOS: a native Summer sheet over the game.
- Summer desktop app: an overlay sheet; the result returns to the game on the signal.
- Web: the same sheet in the page.
- Editor play and local runs: readiness is
unavailable. Build the unavailable path first.
Testing
- Test against Summer staging, which runs in sandbox mode. Why: no real money moves and results are identical to production.
- Walk every state: decline the sheet, let it expire, buy with too few Sparks, get Sparks in the Summer app, then buy the item. Kill the game while a checkout is pending and relaunch; the result must still arrive once.
- Verify the unlock comes from
items.refreshed, not from the checkout signal.
Store policy notes
- Apple requires In-App Purchase for digital goods and currency sold inside iOS apps. Summer’s iOS sheet and app handle this; never add your own payment links, web prices or “cheaper on the web” text inside the game. Why: it can get the Summer app removed from the App Store for every game.
- Randomized paid rewards must disclose their odds before purchase (Apple and most regions require it).
- Say what a purchase gives in plain words next to the price.
Availability
request_checkout, list_items, Summer.client.items,
Summer.authority.items.inventory_for_session and Summer.authority.player_data
ship in the engine today. Buying Sparks inside the game does not: players get
Sparks in the Summer app.
See also
- multiplayer-state
- ui-basics
From a shell
No Summer MCP tools in this session? Run each one as a command; whensummer --version is missing or below 4.0, use npx -y summer-engine@latest in place of summer. Fields are flags in kebab-case (--scene-path res://main.tscn) or one --input '<json>', and summer ops describe <op> prints the exact input. The summer-cli skill has the rest.
Related skills
multiplayer-state: Authority-owned state in Summer multiplayer: shared and private State groups, Commands, Events.ui-basics: Build UI: HUDs, menus, health bars, dialogue boxes, Control trees, anchors, containers and themes.
Source:
library/skills/in-game-purchases/SKILL.md, rendered from summer-engine@4.3.0 on npm.
