Skip to main content
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:
Swap 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

  1. Never grant an item from a button press or a receipt. Unlock only when Summer.client.items shows 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.
  2. 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.
  3. One call is one purchase. Every request_checkout creates 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.
  4. Handle results in checkout_status_changed, and deduplicate by request_id plus version. Connect normally (not deferred) before requesting. To draw a checkout panel, read get_checkout_statuses() and has_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.
  5. Treat manual_review and pending as not done. Why: the platform has not decided yet; showing success or failure would be a guess.
  6. Check readiness and hide what is unavailable. check_checkout_readiness() (buying) and check_browse_readiness() (listing with list_items()) return a SummerResult. 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’s items[i].owned_item_ids are the owned items. The client sends the authority only an owned item ID. The authority:
  1. 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;
  2. 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;
  3. 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.replayed is 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 the request_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.
The game code is identical on every surface.

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; when summer --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.
  • 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.