Controller Presets
Guide teams through creating a preset for the Cartridge Controller. A preset is a config.json committed to cartridge-gg/presets that configures origin verification, session policies, theming, paymaster behavior, and optional iOS passkey support.
Invocation
The user wants help creating or debugging a Controller preset. Use AskUserQuestion to gather information interactively, one round at a time.
Process
Phase 1: Basics
Ask:
-
Game/project name — used as the directory name in configs/<name>/ . Must be lowercase kebab-case (e.g. dope-wars , loot-survivor ).
-
Which networks? — SN_MAIN , SN_SEPOLIA , or both.
Explain:
-
Sepolia is paymastered by default — no paymaster setup needed for testnet.
-
Mainnet requires a Slot paymaster (see slot-paymaster skill) to sponsor transactions.
Phase 2: Origin Configuration
Ask for the production domain(s) where the game will be hosted.
Generate the origin field. Apply these rules:
Rule Correct Wrong
No protocol prefix "game.example.com"
Wildcard for subdomains "*.example.com"
—
Wildcard does NOT match base domain *.example.com matches app.example.com but NOT example.com
Assuming *.example.com covers example.com
Multiple origins use an array ["example.com", "staging.example.com"]
—
localhost is always allowed Don't list it Adding "localhost" to origins
If they have a Capacitor mobile app, ask for the custom hostname and include it:
{ "origin": ["yourdomain.com", "my-custom-app"] }
This authorizes capacitor://my-custom-app (iOS) and https://my-custom-app (Android). The default capacitor://localhost is always allowed automatically.
IMPORTANT: If the user needs both example.com and *.example.com , they must list both explicitly.
Phase 3: Session Policies
Ask for the contract addresses and entrypoints the game calls. For each contract, collect:
-
Contract address (hex, checksummed)
-
Human-readable name and description
-
Methods with entrypoints
Build the chains section. Example:
{ "chains": { "SN_MAIN": { "policies": { "contracts": { "0x123...abc": { "name": "Game World", "description": "Main game contract", "methods": [ { "name": "Move Player", "description": "Move to a new position", "entrypoint": "move_player" } ] } } } } } }
Key rules:
-
Entrypoints must be snake_case and match the exact Cairo function name.
-
Chain IDs: use SN_MAIN (not SN_MAINNET ) and SN_SEPOLIA (not SN_TESTNET ).
-
Contract addresses differ between networks — confirm separate addresses for mainnet vs sepolia.
-
approve entrypoint triggers a CI warning — the validator flags it. If the user genuinely needs ERC20 approval, acknowledge the warning.
-
VRF: If the game uses Cartridge VRF, include the VRF provider contract (0x051Fea4450Da9D6aeE758BDEbA88B2f665bCbf549D2C61421AA724E9AC0Ced8F ) with request_random entrypoint. The keychain auto-labels VRF contracts with Cartridge branding.
Method options
Field Default Notes
isPaymastered
true
Set to false to require users to pay their own gas for this method
isEnabled
true
Whether the method is pre-checked in the session approval UI
isRequired
false
If true , user cannot uncheck this method
predicate
— Optional: conditional sponsorship based on contract state
Paymaster predicates
For conditional sponsorship:
{ "entrypoint": "move_player", "is_paymastered": true, "predicate": { "address": "0x456...def", "entrypoint": "check_move_eligibility" } }
The predicate contract is called first; the transaction is only sponsored if it returns true.
Message signing policies
If the game uses off-chain signed messages (EIP-712 style typed data), add a messages array alongside contracts :
{ "policies": { "contracts": { ... }, "messages": [ { "types": { "StarknetDomain": [...], "Message": [{ "name": "content", "type": "felt" }] }, "primaryType": "Message", "domain": { "name": "MyGame", "version": "1", "chainId": "SN_MAIN", "revision": "1" } } ] } }
Phase 4: Theme
Ask if they want a custom theme. Collect:
-
Name: display name for the game
-
Icon: SVG or PNG file (will be optimized to 16–256px)
-
Cover: PNG or JPG file (will be optimized to 768–1440px), optional
-
Primary color: hex color for accent/branding
Cover supports light/dark variants:
{ "theme": { "name": "MyGame", "icon": "icon.svg", "cover": { "light": "cover-light.png", "dark": "cover-dark.png" }, "colors": { "primary": "#F38332" } } }
Asset files go in the same directory as config.json . The build pipeline generates optimized WebP/PNG/JPG versions automatically — commit only the source files.
Phase 5: Apple App Site Association (AASA)
Ask if they have a native iOS app that uses passkeys.
If yes, collect:
-
Team ID: exactly 10 uppercase alphanumeric characters (from Apple Developer account)
-
Bundle ID: reverse DNS format (e.g. com.example.mygame )
The app ID is TEAMID.BUNDLEID . Validation rules:
-
Pattern: /^[A-Z0-9]{10}.[a-zA-Z0-9.-]+$/
-
Team ID must be exactly 10 characters
-
All AASA entries across all presets are aggregated into a single file served at https://x.cartridge.gg/.well-known/apple-app-site-association
-
The aggregated file must stay under 128 KB
{ "apple-app-site-association": { "webcredentials": { "apps": ["ABCDE12345.com.example.mygame"] } } }
If no iOS app, skip this section entirely (don't include the key).
Phase 6: Assemble and Validate
Assemble the complete config.json and present it to the user.
Run through validation checklist:
-
Origins have no protocol prefix
-
Chain IDs are SN_MAIN or SN_SEPOLIA (not SN_MAINNET /SN_TESTNET )
-
Contract addresses are different for each network
-
Entrypoints are snake_case matching Cairo function names
-
AASA app IDs match TEAMID.BUNDLEID format (if present)
-
Asset files (icon, cover) are referenced and will exist in the directory
-
No approve entrypoint unless intentional
Phase 7: Connector Integration
Show how to use the preset in their app:
import Controller from "@cartridge/controller";
const controller = new Controller({ preset: "<preset-name>", // matches the directory name in configs/ // Policies are loaded from the preset — do NOT also pass policies here // unless you set shouldOverridePresetPolicies: true });
Explain policy precedence:
- shouldOverridePresetPolicies: true
- policies → uses inline policies
-
Preset has policies for current chain → uses preset policies (ignores inline)
-
Preset has no policies for current chain → falls back to inline policies
-
No preset → uses inline policies
Phase 8: PR Submission
Guide the user to submit a PR to cartridge-gg/presets:
-
Create configs/<name>/config.json
-
Add asset files (icon, cover) to the same directory
-
CI runs validate-configs.ts — fix any errors before merge
-
After merge, configs are built and deployed to https://static.cartridge.gg/presets/<name>/config.json
Mainnet vs Sepolia Reference
Aspect Sepolia Mainnet
Paymaster Free, automatic Requires Slot paymaster with budget
Chain ID in config SN_SEPOLIA
SN_MAIN
Contract addresses Sepolia deploy Mainnet deploy
Recommended for Development, testing Production
Teams often include both chains in a single preset — use separate contract addresses for each.
Debugging Common Issues
"Policies show as unverified" → Origin mismatch. Check that config.origin matches the domain your app is served from (without protocol). If using wildcards, remember *.example.com does NOT match example.com .
"Preset policies not loading" → Check that the preset name in your Controller constructor matches the directory name in the presets repo exactly. The config is fetched from CDN at https://static.cartridge.gg/presets/<name>/config.json .
"Wrong policies for my chain" → Policies are selected by chain ID at runtime. Verify the chain ID in your config matches what your RPC returns. Use SN_MAIN /SN_SEPOLIA , not hex chain IDs.
"Paymaster not sponsoring on mainnet" → Sepolia is auto-sponsored. Mainnet requires creating a Slot paymaster, funding it with credits, and adding matching policies. See slot-paymaster skill.
"AASA validation failing" → Team ID must be exactly 10 uppercase alphanumeric chars. Bundle ID must be reverse DNS. Pattern: ABCDE12345.com.example.app .
"CI warns about approve entrypoint" → This is intentional — approve is flagged as a security concern. If your game genuinely needs ERC20 approval, the warning is acceptable but will require reviewer acknowledgment.