Skip to main content

Add a game — config only

A new game of an existing shape needs no backend code and no deploy. You create prizes, then a game that references a registered seed_generator + reward_handler + validator and supplies a handler_config.

1. Create prizes

A='-H Content-Type:application/json -H X-Tenant-Id:tenant_demo -H X-Merchant-Id:merchant_demo -H X-Roles:admin'

JACKPOT=$(curl -s $A -X POST localhost:8081/api/v1/admin/prizes \
-d '{"name":"Voucher 100K","type":"voucher","value":100000,"stock":{"total":50}}' | jq -r .data.prize_id)
MISS=$(curl -s $A -X POST localhost:8081/api/v1/admin/prizes \
-d '{"name":"Try again","type":"points","value":0,"stock":{"total":1000000}}' | jq -r .data.prize_id)
Admin calls need a role

The admin surface is role-guarded. Send a real admin JWT, or the X-Roles: admin dev header.

2. Create the game

A spin wheel = none seed + probability handler + basic validator:

curl -s $A -X POST localhost:8081/api/v1/admin/games -d "{
\"name\": \"Vòng Quay May Mắn\",
\"type\": \"spin_wheel\",
\"campaign_id\": \"camp_demo\",
\"seed_generator\": \"none\",
\"reward_handler\": \"probability\",
\"validator\": \"basic\",
\"status\": \"active\",
\"rules\": { \"max_plays_per_user\": 3, \"max_plays_per_day\": 1 },
\"handler_config\": { \"prizes\": [
{ \"prize_id\": \"$JACKPOT\", \"probability\": 0.05 },
{ \"prize_id\": \"$MISS\", \"probability\": 0.95 }
] },
\"ui\": { \"theme\": { \"primary\": \"#FF5733\", \"accent\": \"#f4c430\" },
\"wheel\": { \"segments\": [ { \"emoji\": \"💎\" }, { \"image\": \"https://cdn/seg.png\" } ] } }
}" | jq .data

That's it — the game is live. ui is an opaque blob: Core stores and returns it (and PUT /api/v1/games/{id} updates it any time); only the widget reads it, via the redacted GET /games/{id}/render. All ui fields are optional and fall back to built-in defaults. A new visual is a config change, never a Core change. See the widget README for the full ui schema (background, theme, wheel.segments, items, egg).

handler_config by shape

// probability (spin / scratch)
{ "prizes": [ { "prize_id": "p1", "probability": 0.05 }, { "prize_id": "p0", "probability": 0.95 } ] }

// score_to_tier (egg-catcher)
{ "tiers": [ { "min": 0, "max": 29, "prize_group": "t0" }, { "min": 70, "max": 1000, "prize_group": "t2" } ],
"prize_groups": { "t2": [ { "prize_id": "big", "probability": 1.0 } ] } }

// collect_items (gift-catcher) — also set seed_generator:"drop_sequence", validator:"drop_plan"
{ "drops": [ { "type": "voucher_50k", "prize_id": "p", "frequency": 4, "max_catchable": 2 } ],
"total_items": 15, "interval_ms": 500 }

// lucky_item (collection) — pair with a milestones block (see Wallet)
{ "items": [ { "item": "lucky_star", "weight": 1, "quantity": 1, "slot_index": 0 } ] }

Prize constraints & delivery

Each prize can carry caps and a delivery policy:

{ "name": "Voucher 100K", "type": "voucher", "value": 100000,
"stock": { "total": 50 },
"constraints": { "max_per_user": 1, "max_per_day": 1 },
"fulfillment": { "redemption_mode": "on_claim", "channel": "voucher_code" } }

See Rewards & fulfillment for channels and modes, and Wallet & milestones for lucky_item + milestones.

When config isn't enough

If your mechanic doesn't fit any registered handler, you add one — see Add a shape. That's the only time gameplay needs code.