BricqsBricqs
Concept · Progression12 min read · For engineers + technical PMs

Loyalty tiers in headless gamification

A tier is a named status level a user occupies based on cumulative behaviour. Bronze, Silver, Gold. Most loyalty programs reach for tiers because they communicate progress at a glance and let benefits scale with engagement. This page covers what a tier system actually is, how Bricqs evaluates and updates tiers, and the patterns you will reach for when wiring tiers into your product.

useTier
One hook returns current tier, next tier, and points remaining.
Key takeaways
  • A tier is a named status level a participant occupies based on cumulative behaviour, and every participant is at exactly one tier at a time.
  • Compare tiers by the level integer, never by the code string; codes are display labels that can be renamed in the dashboard.
  • Anchor tier criteria on lifetime totals so redemption never demotes a participant; tier movement should be monotonic upward.
  • Tier evaluation runs in the same code path as points award, so there is no separate cron and a tier_changed fact is emitted on every transition.
  • Tier drives UX surfaces like chips and benefits pages; never use a tier gate as the source of truth for paid access or authorization.
1. The concept

What a tier is, in any gamification system

Before the API. Every tier system, regardless of vendor, decomposes into the same primitives. If you understand these, the Bricqs implementation is just one mapping you could make.

Definition

A tier is a named level a participant occupies based on satisfying a set of criteria. Tiers are mutually exclusive: a participant is at exactly one tier at a time. Tier membership is dynamic; as criteria change (more points, segment switch, time-based reset) the participant can move up or down.

Who owns it

Engineers wiring tier rendering into product surfaces (header chips, profile pages, benefits screens); growth and lifecycle teams choosing thresholds and benefits. Tiers are owned by product, computed by the platform, surfaced by engineering.

How it differs from adjacent mechanics
vs badges

Badges accumulate and never expire; a user collects them. Tiers are exclusive and dynamic: one current tier at a time, with the possibility of moving up or down.

vs points

Points are the currency. Tiers are the destination. Most programs use both: points are what the user earns and spends; tiers track lifetime earning.

vs segments

A segment is a property of the user (premium plan, EU region). A tier is earned through behaviour over time. Segments are inputs; tiers are an output of the engagement loop.

vs milestones

A milestone is a one-time threshold crossing. A tier is an ongoing status with benefits. Crossing a milestone often promotes a tier, but the tier is the steady state.

The decomposition every tier system uses: a definition (the tier as a noun: code, name, level, color, criteria) and a placement (the tier as a verb: this participant is at this tier as of this time, with the next-tier delta shown alongside). Definitions are tenant-scoped configuration. Placements are participant-scoped state that the engine updates whenever a relevant signal changes.

The three operations every API supports: read a participant’s current tier (with next-tier progress), let the engine update tier automatically when criteria change, and override tier manually for edge cases (VIP, support escalation). Most calls are reads; writes are rare.

The hardest design decision is not the schema, it is the evaluation cadence. Two patterns dominate. Continuous evaluation re-evaluates after every relevant event (points earned, segment changed); lowest latency, simplest mental model. Scheduled evaluation runs on a fixed cadence (nightly, weekly); lower cost, harder to debug (“why is my tier still Silver?”). Bricqs uses continuous evaluation on points changes; scheduled re-evaluation is on the roadmap for segment-driven tiers.

2. Design decisions

When tiers are the right tool, and when they are not

Tiers are heavy. They imply persistent benefits, communications about status changes, and a graceful path for downgrades. Pick the moments where they earn their weight.

Reach for a tier when
01Benefits scale

You want benefits to scale with engagement

Tiers map naturally to differentiated benefits: better support response time, larger discounts, exclusive features.

  • The tier is the contract; the benefits are the consequence.
  • Pair with the rewards engine to gate redeemables by tier level.
02Visible status

You want a visible status marker on the profile

Tier names (Bronze, Silver, Gold, Platinum) communicate progress at a glance. Users self-identify with their tier.

  • Loss aversion at the threshold drives re-engagement.
  • Status chips earn real estate on profile and header surfaces.
03Auto segmentation

You want segmentation derived from behaviour

Tiers are computed from criteria, so they update without manual intervention.

  • Email campaigns can filter on tier without maintaining a list.
  • Dashboards stay accurate even as the participant base churns.
04Long-term

You want long-term progression beyond points

Points are short-lived (earn, spend, expire). Tiers are the long-term recognition that survives spend events.

  • Even when balances are zeroed, the user keeps their tier.
  • Lifetime totals drive evaluation, so tier is monotonic.
Do not use a tier when
01Permanent and additive

The recognition should be permanent and additive

That is a badge, not a tier. Tiers are mutually exclusive: getting Gold replaces Silver in the active slot.

02One level

You only have one level

A single-tier system is just a flag. If everyone is ‘Member’, a boolean attribute or a single segment will do.

03Flapping

Criteria fire constantly and tier flaps up and down

Flapping (Bronze, Silver, Bronze in the same day) is worse than no tier. Move to a slower-changing metric like quarterly point totals.

04Same benefits

Benefits would not differ across levels

Tiers without differentiated benefits are decorative, and decorative tiers train users to ignore the system.

05Short campaign

It is a short campaign that ends in two weeks

Tiers are persistent. Use the contest system for time-bound rankings; reserve tiers for the long-running loyalty layer.

Before you read on
  • Looking for the marketing-team take? The strategy guide on progression systems covers when tiers fit a program, threshold design, KPIs, and example loyalty stacks.
  • Configure in the dashboard: Progression → Tiers → New Tier.

Bricqs models tiers with the standard two-part decomposition above: a definition (the level) and a placement (the participant’s current level). Definitions are created through the dashboard or the admin API. Placements update automatically whenever the participant’s points change, and can also be overridden through a direct API call.

Tier evaluation runs in the same code path as points award, so you never have to schedule a re-evaluation cron. Tier changes emit a fact your downstream systems can subscribe to. The React SDK auto-refreshes when a tier change is broadcast on the client event bus.

3. Data model

What a tier looks like

Two shapes you will see in API responses and SDK hooks. The first is the tier definition (the template). The second is the participant view (current placement plus next-tier delta).

jsonTier definition
{
  "code": "gold",                  // your code, what you use in API calls
  "name": "Gold",                  // shown in the UI
  "level": 3,                      // ordered level, used for upgrade/downgrade comparison
  "color": "#FFD700",              // optional, used for accents
  "icon_url": "https://cdn.example.com/tiers/gold.png",
  "description": "Premium status with extra benefits.",
  "criteria": {                    // what a participant needs to satisfy this tier
    "min_points": 5000
  },
  "next_criteria": {               // optional, used to compute next-tier progress
    "min_points": 10000
  },
  "is_active": true                // false = paused, will not be assigned
}

The level field is what matters for ordering. Codes are opaque strings; level is what the evaluator uses to decide which tier is higher. Always assign distinct ascending integers (1, 2, 3, 4).

criteria is open-ended. Start with min_points; add segment filters or custom rules later. The evaluator returns the highest-level tier whose criteria the participant satisfies.

jsonWhat a participant sees (current placement + next tier)
{
  "participant_id": "user_42",
  "current_tier": {
    "code": "gold",
    "name": "Gold",
    "level": 3,
    "color": "#FFD700",
    "achieved_at": "2026-02-01T10:00:00Z"
  },
  "next_tier": {                         // null if at top tier
    "code": "platinum",
    "name": "Platinum",
    "points_required": 10000,
    "points_remaining": 8500
  },
  "tier_history": [                      // best-effort, last few transitions
    { "code": "bronze",  "name": "Bronze", "achieved_at": "2026-01-01T..." },
    { "code": "silver",  "name": "Silver", "achieved_at": "2026-01-15T..." },
    { "code": "gold",    "name": "Gold",   "achieved_at": "2026-02-01T..." }
  ]
}

next_tier is computed live. points_remaining is derived at read time from the participant’s lifetime points and the next tier’s threshold. Changes to thresholds reflect immediately without a batch job.

tier_history is best-effort. Last few transitions, useful for surfacing recent upgrades. For an audit trail, subscribe to the tier-change fact via webhook once outbound delivery ships.

4. Lifecycle

How a tier change happens

One sequence covers both automatic and manual tier changes. The difference is what kicks it off.

  1. 01

    A trigger arrives

    Either the participant just earned points (the engine re-evaluates automatically), or your backend called POST /api/v1/gamify/tiers/assign to override.
  2. 02

    The evaluator picks the highest-eligible tier

    For each active tier definition, the evaluator checks whether the participant satisfies the criteria block. The highest-level tier that matches wins. If nothing matches, the participant has no tier.
  3. 03

    The engine compares to current tier

    If the new tier equals the current tier, the call short-circuits as a no-op (no fact emitted). Re-assigning the same tier twice is safe.
  4. 04

    Otherwise, the placement is updated

    The participant's current tier is set to the new tier and the transition is appended to tier history.
  5. 05

    A tier-change fact is emitted

    The fact system.tier_changed.v1 includes the new tier, new level, previous tier and level, and the source of the change (automatic vs assign).
  6. 06

    The SDK refreshes

    Components using useTier listen to a tier:changed event on the client bus and re-fetch. Once outbound webhook delivery for progression events ships, the tier-change fact will also be delivered to your downstream consumers.
On debouncingThere is no built-in debounce window. If your points criteria is too sensitive (and a participant’s points oscillate around a threshold) you can get tier flapping. The fix is upstream: set thresholds to amounts the participant cannot easily cross multiple times in a session, and prefer lifetime totals over spendable balances as the tier signal.
5. REST API

Read and override tiers from any backend

Two endpoints cover almost every case. All endpoints are under /api/v1/gamify/ and authenticate with an API key (bq_live_ for production, bq_test_ for sandbox).

Read a participant’s tier

Returns current tier, next-tier delta, and recent transition history. Read this from your backend when you need to gate a benefit, or call it through the SDK to render a tier chip.

bashGET /api/v1/gamify/participants/{participant_id}/tier
curl https://api.bricqs.com/api/v1/gamify/participants/user_42/tier \
  -H "Authorization: Bearer bq_live_xxxxx"

# Response (200 OK)
{
  "participant_id": "user_42",
  "current_tier": {
    "code": "gold",
    "name": "Gold",
    "level": 3,
    "color": "#FFD700",
    "achieved_at": "2026-02-01T10:00:00Z"
  },
  "next_tier": {
    "code": "platinum",
    "name": "Platinum",
    "points_required": 10000,
    "points_remaining": 8500
  },
  "tier_history": [
    { "code": "bronze", "name": "Bronze", "achieved_at": "2026-01-01T..." },
    { "code": "silver", "name": "Silver", "achieved_at": "2026-01-15T..." },
    { "code": "gold",   "name": "Gold",   "achieved_at": "2026-02-01T..." }
  ]
}

Override a tier manually

Bypass automatic evaluation and pin a participant to a specific tier. Used for VIP promotions, support escalations, or manual corrections. Safe to retry with an Idempotency-Key header.

bashPOST /api/v1/gamify/tiers/assign
curl -X POST https://api.bricqs.com/api/v1/gamify/tiers/assign \
  -H "Authorization: Bearer bq_live_xxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: vip-promo-2026-q2-user_42" \
  -d '{
    "participant_id": "user_42",
    "tier_code": "platinum",
    "reason": "VIP promotion (2026 Q2)"
  }'

# Response (200 OK)
{
  "participant_id": "user_42",
  "tier_code": "platinum",
  "tier_name": "Platinum",
  "level": 4,
  "previous_tier_code": "gold"
}

Manage tier definitions (admin only)

Most teams create and edit tiers in the dashboard. If you want to script it (e.g. seed tiers in CI), use the admin endpoints. They require an admin-scoped API key.

bashTier definition CRUD (admin key required)
GET    /api/v1/gamify/tiers
GET    /api/v1/gamify/tiers/{tier_code}
POST   /api/v1/gamify/tiers
       { "code": "gold", "name": "Gold", "level": 3, "criteria": { "min_points": 5000 }, ... }
PATCH  /api/v1/gamify/tiers/{tier_code}
       { "name": "Gold Elite" }
DELETE /api/v1/gamify/tiers/{tier_code}
6. React SDK

Render the tier chip with one hook

The useTier hook returns the current tier, the points to next tier, and the next tier name. It listens to a tier:changed event on the client bus and re-fetches automatically when a tier upgrade lands.

tsxcomponents/TierChip.tsx
"use client";
import { useTier } from "@bricqs/sdk-react";

export function TierChip({ engagementId }: { engagementId: string }) {
  const {
    currentTier,        // full tier object (or null if no tier yet)
    tierName,           // shortcut: name string
    tierLevel,          // shortcut: ordered level
    tierColor,          // shortcut: brand color
    pointsToNext,       // points remaining to the next tier
    nextTierName,       // name of the next tier (null at top)
    isLoading,
    error,
    refresh,
  } = useTier({ engagementId });

  if (isLoading) return null;
  if (error || !currentTier) return null;

  return (
    <div
      className="flex items-center gap-2 rounded-full px-3 py-1 border"
      style={{ borderColor: tierColor, color: tierColor }}
    >
      <span className="font-semibold">{tierName}</span>
      {nextTierName && pointsToNext != null && (
        <span className="text-xs text-slate-500">
          {pointsToNext.toLocaleString()} to {nextTierName}
        </span>
      )}
    </div>
  );
}

No polling by default. Tier changes are pushed via the client event bus, so the hook only re-fetches when a relevant event arrives. You can still call refresh() after a backend write that should bump the tier.

Render progress with pointsToNext. Combine with the next-tier threshold for a fill percentage. Show a celebration modal only when a tier:changed event fires, not on every render of the chip.

Field shape. Nested objects keep snake_case wire fields (achieved_at, tier_color); top-level shortcuts are camelCase. Prefer the shortcuts where they exist.

7. Common patterns

Recipes you will reach for

Four patterns that cover most tier implementations in production. Each is a small composition of the primitives above.

Pattern 1: VIP override from your backend

The user qualifies for a tier outside the normal points criteria (e.g. enterprise contract, support escalation, marketing campaign). Skip the evaluator and pin the tier directly. Use an Idempotency-Key derived from the business reason so retries do not produce noise.

tsserver.ts (after a contract is signed)
// In your contract-signing webhook
await fetch("https://api.bricqs.com/api/v1/gamify/tiers/assign", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.BRICQS_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `enterprise-contract-${contract.id}`,
  },
  body: JSON.stringify({
    participant_id: contract.account_owner_id,
    tier_code: "platinum",
    reason: `Enterprise contract ${contract.id}`,
  }),
});

The Idempotency-Key collapses duplicate retries; the second call returns the original response without emitting a second tier-change fact. If the participant is already at the requested tier, the call is a no-op short-circuit.

Pattern 2: Gate a feature on minimum tier level

Use the level field, not the code, when comparing tiers. Levels are ordered integers; codes are opaque strings. Comparing by level means renaming a tier in the dashboard does not break the gate.

tsxcomponents/GoldOrAboveOnly.tsx
"use client";
import { useTier } from "@bricqs/sdk-react";

const MIN_LEVEL_GOLD = 3;

export function GoldOrAboveOnly({
  engagementId,
  children,
}: {
  engagementId: string;
  children: React.ReactNode;
}) {
  const { tierLevel, isLoading } = useTier({ engagementId });
  if (isLoading) return null;
  if (tierLevel == null || tierLevel < MIN_LEVEL_GOLD) {
    return <UpgradeCta />;
  }
  return <>{children}</>;
}

Render an upgrade CTA for users below the threshold. Pair this with the points-to-next field to show them how close they are. Never use a tier gate as a security boundary; it is a UX gate, not an authorization gate. Real access control belongs in your backend.

Pattern 3: Trigger downstream side effects when tier changes

Subscribe to the tier-change fact and fan out to your CRM, email, and reward services. Each side effect is independent and safe to retry.

RoadmapOutbound webhook delivery for progression events (including tier.changed.v1) is the next major addition to the platform. Today, the fact is emitted internally but not delivered to your endpoint; poll GET /participants/{id}/tier from your sync job, or react inside a session via the SDK hook’s tier:changed client event. The code below is the shape your handler will take once outbound webhook delivery ships.
tsYour webhook handler
// POST /webhooks/bricqs, subscribed to tier.changed.v1
export async function POST(req: Request) {
  const event = await verifyAndParse(req); // your signature check

  const { new_tier, previous_tier } = event.payload;

  // Only run for upgrades, not downgrades
  if (rankOf(new_tier) <= rankOf(previous_tier)) {
    return new Response(null, { status: 200 });
  }

  await Promise.all([
    sendTierUpgradeEmail(event.participant_id, new_tier),
    syncTierToCrm(event.participant_id, new_tier),
    allocateWelcomeReward(event.participant_id, new_tier),
  ]);

  return new Response(null, { status: 200 });
}

Tier-change facts are emitted once per transition (the engine short-circuits no-op assigns), so your handler does not need to guard against duplicate fires for the same upgrade. Still, treat every handler as retry-safe: webhook delivery itself retries on non-2xx responses.

Pattern 4: Downgrade after a period of inactivity

Most loyalty programs decay tier status after a year of inactivity. Run a scheduled job that lists participants whose last activity is older than your decay window, and assign them the lowest-tier code (or no tier).

tsscheduled.ts (your nightly job)
// List inactive participants from your own analytics
const stale = await db.query(`
  SELECT participant_id FROM user_activity
  WHERE last_seen_at < NOW() - INTERVAL '12 months'
    AND current_tier_level > 1
`);

for (const { participant_id } of stale.rows) {
  await fetch("https://api.bricqs.com/api/v1/gamify/tiers/assign", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.BRICQS_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `inactivity-decay-${participant_id}-${monthKey()}`,
    },
    body: JSON.stringify({
      participant_id,
      tier_code: "bronze",
      reason: "Inactivity decay",
    }),
  });
}

The monthly Idempotency-Key prevents the same participant from being demoted twice if your job retries. Run it nightly; the first run of each month does the real work and subsequent runs are no-ops. Communicate the decay policy in your tier benefits page so users are not surprised.

8. Design pitfalls

Design choices teams regret later

Each pitfall is a structural decision you make at design time. Catch them in the dashboard and the spec, not after the program is live.

Mistake

Making the tier code your comparison surface. App code reads if (tier === 'gold'). The first time a PM renames Gold to Gold Elite, every gated benefit silently breaks for Gold users.

Fix

Design choice: expose level (an ordered integer) as the stable contract; codes are display labels that can change. Build your gating logic against level, document it as the public surface, and treat code renames as cosmetic.

Mistake

Anchoring tier criteria on spendable points. Spend fluctuates: a user redeems a reward, drops below the threshold, gets demoted mid-session. The program looks like it punishes redemption.

Fix

Design choice: tier criteria should anchor on lifetime totals (only-increasing). Redemption never demotes; tier movement is always upward. The Bricqs evaluator defaults to this when min_points is the criterion.

Mistake

Conflating tier with entitlement ("Gold users get the premium plan"). Tier evaluation runs on engagement-engine timing; paid-access systems need transactional guarantees about who has what.

Fix

Design choice: tier drives UX (chip, benefits page, hint cards). Your billing or entitlement system drives access. Subscribe to tier-change events to keep them aligned, but never let tier be the source of truth for paid access.

Mistake

Numbering tiers with no headroom (level 1, 2, 3 with no gaps). The first time product wants to insert a mid-tier between Silver and Gold, every cached comparison and every dashboard filter that referenced level >= 3 is wrong.

Fix

Design choice: number your tiers with gaps (10, 20, 30, 40, ...). Inserting a new level later becomes a config change instead of a renumbering migration. The platform never reads the gaps; your downstream tooling will thank you.

Mistake

Designing tiers without qualitatively different benefits. Three levels where the only difference is "larger discount" are decorative; users notice the lack of differentiation and the program loses signal.

Fix

Design choice: each tier needs at least one benefit that is qualitatively different from the one below (a new feature, a status surface, an exclusive event). If you cannot name one for a tier, the tier should not exist; collapse it into the neighbour.

FAQ

Common questions when integrating

Wire it up

Build with tiers

Pair this with the umbrella strategy guide on progression systems, or jump to the React SDK setup page.