Phoenix Gamification
Spin the wheel

Spin the Wheel Overview

Phoenix provides a flexible spin-the-wheel gamification feature that allows users to spin for rewards with configurable probability weights and frequency limits.

Spin-the-wheel lets you create wheel games where users can spin to win rewards. Each wheel has:

  • Segments: Multiple segments with different reward items (or no-win segments)
  • Probabilities: Weighted chances for each segment
  • Frequency Limits: Configurable limits (daily, total, cooldown, or unlimited)
  • Date Ranges: Optional start/end dates for time-limited wheels

Client Endpoints

All client endpoints require JWT or HMAC authentication.

Get Available Wheels

Retrieve all active wheels available for a user.

GET /v1/wheels/{tenant_id}

Response:

{
  "wheels": [
    {
      "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
      "name": "Daily Spin",
      "description": "Spin once per day for rewards",
      "config": {
        "segments": [
          {
            "reward_item_id": "item-gems-100",
            "probability": 3,
            "label": "100 Gems"
          },
          {
            "reward_item_id": null,
            "probability": 5,
            "label": "Try Again"
          },
          {
            "reward_item_id": "item-coins-50",
            "probability": 2,
            "label": "50 Coins"
          }
        ],
        "frequency": {
          "type": "daily_limit",
          "value": 1
        },
        "starts_at": "2025-01-01T00:00:00Z",
        "ends_at": "2025-12-31T23:59:59Z"
      }
    }
  ]
}

Spin the Wheel

Spin a wheel for a user. The result is determined by weighted random selection.

POST /v1/wheels/{tenant_id}/{wheel_id}/spin

Request Body:

{
  "user_id": "user_123"
}

Response:

{
  "segment_index": 0,
  "segment": {
    "reward_item_id": "item-gems-100",
    "probability": 3,
    "label": "100 Gems"
  },
  "reward_item": {
    "item_id": "item-gems-100",
    "name": "100 Gems",
    "description": "100 premium gems",
    "reward_type": "currency",
    "payload": {
      "currency": "gems",
      "amount": 100
    }
  }
}

Error Responses:

  • 400 - Wheel not active, expired, not started, or frequency limit exceeded
  • 404 - Wheel not found

Get User Spin History

Retrieve a user's spin history.

GET /v1/wheels/{tenant_id}/user/{user_id}/history

Query Parameters:

ParameterTypeDescription
wheel_idUUIDFilter by specific wheel (optional)
limitintegerPage size (default: 100, max: 1000)
offsetintegerPage offset (default: 0)

Response:

{
  "spins": [
    {
      "id": "spin-123",
      "wheel_id": "wheel-abc",
      "result_index": 0,
      "reward_item_id": "item-gems-100",
      "spun_at": "2025-01-04T10:30:00Z"
    },
    {
      "id": "spin-124",
      "wheel_id": "wheel-abc",
      "result_index": 1,
      "reward_item_id": null,
      "spun_at": "2025-01-03T15:45:00Z"
    }
  ],
  "total": 25
}

JavaScript Example

// Get available wheels
async function getAvailableWheels(tenantId) {
  const response = await fetch(
    `${QUERY_API_URL}/v1/wheels/${tenantId}`,
    {
      headers: {
        'Authorization': `Bearer ${jwt}`,
        'Content-Type': 'application/json'
      }
    }
  );
  
  return response.json();
}

// Spin a wheel
async function spinWheel(tenantId, wheelId, userId) {
  const response = await fetch(
    `${QUERY_API_URL}/v1/wheels/${tenantId}/${wheelId}/spin`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${jwt}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ user_id: userId })
    }
  );
  
  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error || 'Failed to spin wheel');
  }
  
  return response.json();
}

// Example: Display wheel and handle spin
const { wheels } = await getAvailableWheels('tenant_abc');
const dailyWheel = wheels[0];

const result = await spinWheel('tenant_abc', dailyWheel.id, 'user_123');

if (result.reward_item) {
  console.log(`You won: ${result.reward_item.name}!`);
} else {
  console.log('Better luck next time!');
}

Wheel Configuration

Segments

Each segment represents one slice of the wheel:

{
  "reward_item_id": "uuid-or-null",  // null = no-win segment
  "probability": 3,                  // Weight (higher = more likely)
  "label": "100 Gems"                // Optional display label
}

Probability Calculation:

  • Probabilities are relative weights, not percentages
  • Example: [3, 5, 2] means segment 2 is 5/10 = 50% likely, segment 1 is 3/10 = 30%, segment 3 is 2/10 = 20%

Frequency Limits

Control how often users can spin:

TypeDescriptionValue Meaning
unlimitedNo limitsvalue ignored
daily_limitMax spins per dayvalue = max spins (resets at midnight UTC)
total_limitMax spins evervalue = max spins per user
cooldownTime between spinsvalue = hours to wait

Example Configurations:

// Daily limit: 3 spins per day
{
  "type": "daily_limit",
  "value": 3
}

// Total limit: 10 spins ever
{
  "type": "total_limit",
  "value": 10
}

// Cooldown: 4 hours between spins
{
  "type": "cooldown",
  "value": 4
}

// Unlimited
{
  "type": "unlimited"
}

Date Ranges

Optional time-based availability:

{
  "starts_at": "2025-01-01T00:00:00Z",  // Wheel not available before this
  "ends_at": "2025-12-31T23:59:59Z"     // Wheel not available after this
}

Admin Endpoints

Admins can manage wheel definitions via the admin API.

Create Wheel

POST /v1/tenants/{tenant_id}/wheels

Request:

{
  "name": "Daily Spin",
  "description": "Spin once per day",
  "config": {
    "segments": [
      { "reward_item_id": "item-gems-100", "probability": 3, "label": "100 Gems" },
      { "reward_item_id": null, "probability": 5, "label": "Try Again" }
    ],
    "frequency": {
      "type": "daily_limit",
      "value": 1
    }
  }
}

List Wheels

GET /v1/tenants/{tenant_id}/wheels

Update Wheel

PUT /v1/tenants/{tenant_id}/wheels/{wheel_id}

Delete Wheel

DELETE /v1/tenants/{tenant_id}/wheels/{wheel_id}

Reward Tracking

Winning spins are automatically tracked in ClickHouse via the Reward Grants API. You can query all wheel rewards using:

GET /v1/rewards/{tenant_id}/user/{user_id}?source_type=wheel

Best Practices

  1. Probability Design: Use simple ratios (e.g., 1, 2, 3) rather than percentages for easier adjustment
  2. No-Win Segments: Include segments with reward_item_id: null to create "try again" experiences
  3. Frequency Limits: Start with daily limits to encourage daily engagement
  4. Date Ranges: Use for seasonal or promotional wheels
  5. Error Handling: Always handle frequency limit errors gracefully in your UI