Game Builder supports two complementary flows for persistent game items:
These flows are designed so that the game logic itself is unaware of whether player input came from a selected item or was typed manually. Item selection is a frontend concern; from the game's perspective, it receives an action string.
During game design, the design agent can define produced tokens — items that the game creates as output. The spec processor extracts this into a ProducedTokensArtifact that describes both the token's contents and where it can be used:
{
tokens: [
{
tokenType: "character", // Identifier for this token category
description: "A superhero character",
tokenSource: "player", // Extract fields from player state ("player" | "game")
fields: ["characterDescription", "characterName", "matchesWon"],
actionId: "describe-character", // Labels this token's action context (stored in metadata)
consumableIn: { // Where this token can be used as input (optional)
phase: "describe_character", // Player phase that accepts this token
actionField: "characterDescription" // Token field to use as the action string
}
}
]
}
This is a unified artifact — it tells the system both which fields to snapshot from game state when producing a token, and in which phase the token can substitute for player input. The spec processor extracts this as a single concept because the designer already knows both sides: "players create a character by describing it, and that character can be reused in future matches."
Game ends → Player wins
↓
Frontend calls POST /api/tokens/prepare
{ sessionId, tokenType: "character", playerId }
↓
Orchestrator extracts token data from game state,
generates image, returns token preview
↓
Frontend calls POST /api/tokens/mint
{ sessionId, tokenType: "character", playerId }
↓
Orchestrator uploads metadata to IPFS (via thirdweb),
mints ERC721 on Game Asset contract (CCGADiamond),
persists token in off-chain DB
↓
Frontend tracks progress via GET /api/tokens/progress/:sessionId/:playerId (SSE)
Token persistence is fully implemented across the stack using a hybrid approach: tokens are stored off-chain in the orchestrator's database and minted on-chain as ERC721 NFTs via the Game Asset contract (CCGADiamond) on Arbitrum Sepolia.
The orchestrator persists tokens in Supabase via SessionTokenRepository (session_tokens table):
session_tokens
├── id: string
├── session_id: string
├── player_address: string (wallet address)
├── token_type: string
├── token_id: number | null (on-chain ERC721 token ID)
├── metadata_uri: string | null (IPFS URI)
├── image_uri: string | null (IPFS URI)
├── image_https_url: string | null (HTTPS gateway URL)
├── metadata_https_url: string | null (HTTPS gateway URL)
├── transaction_hash: string | null (blockchain tx hash)
├── preview_token_data: JSONB | null (extracted game state fields)
├── preview_image_url: string | null
├── created_at: timestamp
└── updated_at: timestamp
Unique constraint: (session_id, player_address, token_type) — one token per player per session per type.
SessionTokenRepository provides getByPlayer(playerAddress) for cross-session queries, but this is not yet exposed as an API endpoint (see Orchestrator Changes below).