Skip to main content
XP is 4D’s loyalty currency. It’s run like money, not like a score: every XP movement is a balanced double-entry ledger transaction in whole integer units, append-only, with a full audit trail, and member balances settle to an on-chain token.

How members earn XP

XP accrual is rules, not code. Your organization defines XP rules (data, managed from the console) and the accrual engine evaluates them against every incoming event:
  • a rule matches an event type (e.g. an engagement.* or commerce.* event) plus optional conditions on its fields;
  • its award is a fixed amount or a formula over event fields, optionally with a per-event cap;
  • when several rules match one event, the awards are summed and the most restrictive cap applies;
  • rules are versioned with effective time windows: a rule change never rewrites history, it supersedes from a point in time.
Accrual is exactly-once: duplicate event deliveries are detected and become no-ops, so a member can’t be double-awarded for one action. To see what your organization’s rules currently pay for each event type, read the pricing your backend can discover in Earning events.

What can stop an award

A matching rule is necessary for an award, not sufficient. Before XP is issued, the accrual engine runs your organization’s earning controls, and any one of them can decline to pay. A declined award is a normal, recorded outcome, not an error: the event was accepted and stored, nothing was written to the ledger, and sending the same event again gets the same answer. Don’t retry it. The same-content guard is always on, including for organizations that have configured nothing. One content item pays a member once for a given action, for life. Reading an article and answering the quiz on that same article stay separately earnable, because the guard keys on the action as well as the item. It reverses nothing: XP already issued stays issued, and only future repeat awards stop. Where a canonical event carries no stable content identity (a purchase, for example) there is nothing to key on, and the guard doesn’t apply. Earning events lists which event types the guard covers and which stay repeatable. Four further controls are optional and configured per organization from the console. Each one, and the guard, records the reason it declined: Daily windows run midnight to midnight in the organization’s own earning timezone, which isn’t necessarily UTC. A sponsor campaign award runs the three controls that measure what the member has already been paid: content_already_earned, action_lifetime_limit and action_daily_limit. It skips the two XP-counted ones (member_daily_cap, org_daily_pool), which measure what the organization gives away out of its own budget rather than out of a sponsor’s. An admin grant runs none of the five. None of this shows up in the /v1/track or /v1/ingest response, which reports only whether the event was accepted; accrual happens after. To show a member what they can still earn, read GET /v1/users/{user_id}/xp-earning. It returns their remaining XP for the day, the per-action headroom, the instant the day resets, and recent refusals already written as member-facing sentences. An organization with no earning controls answers limited: false, and there’s nothing to show.

XP boosts

A boost is a period in which every award your XP rules make is multiplied: “double XP this week”, set from the console. The multiplier is more than 1 and at most 10, with up to two decimals. The boosted award is rounded down to a whole unit. A boost can reach every member or be narrowed by requirements: a tier, a cohort, or holding a ticket for a named event (a ticket sale to the member that was not refunded). Requirements of the same kind are alternatives, so two tiers mean either tier. Requirements of different kinds must all hold, so a tier plus a ticket means a member in that tier who also holds the ticket. When several boosts reach a member at once, the largest multiplier pays. Boosts never stack. Whether a boost applies is decided at the event’s occurred_at, not when it arrives. The multiplier applies before the earning controls, so the controls see the boosted amount and a daily cap still holds during a boost. To tell a member a boost is on, read GET /v1/users/{userId}/xp-boost (reference).

XP expiry

XP does not expire unless your organization turns expiry on in the console. The window is 180, 365 or 730 days. With expiry on, XP lapses one window after the later of two moments: when the member earned it, and when the current policy took effect. Switching expiry on therefore lapses nothing until one full window has passed, however old the member’s XP is. Shortening the window resets the policy’s start, so no XP lapses sooner than one new window after the change. Lengthening it leaves the start alone. Spending uses the oldest XP first, so a redemption consumes the XP closest to lapsing. When XP lapses, it is debited from the member’s spendable balance and burned on-chain the same way a redemption is. Lapsing runs periodically, so XP whose date has just passed can still show on the balance until the next run takes it. Read a member’s schedule with GET /v1/users/{userId}/xp-expiry (reference). Dates are computed on every read from the ledger and the policy.

Where XP comes from

Every award is a transfer from somewhere; that’s the double-entry part. Organizations fund awards from their engagement reserve, or from a sponsor campaign: a budgeted pool a sponsor funds, drawn down as its rules award XP, reported against its budget. Admins can also make direct grants to a member, with a recorded reason.

Tiers

Tiers rank members by qualifying XP: what they’ve earned, separate from what they’ve spent. Your organization defines ordered thresholds (Bronze/Silver/Gold-style) with attached benefits, computed on lifetime qualifying XP. A member’s spendable balance going down (redemption) doesn’t demote them. A member’s tier is worked out from their qualifying XP against the current ladder on every read, so a ladder change shows on the next call. Your backend can read the ladder with GET /v1/tiers and a member’s standing with GET /v1/users/{userId}/balance (reference). The ladder itself is edited in the console.

Reading a member’s XP

Five reads cover what a club site shows a member about their XP. All take your organization API key.

Redemption

Members spend XP on rewards from your organization’s catalog. A redemption burns the spent XP, ledger-first, then settled on-chain, so total supply always reconciles. Eligibility is checked when the claim is made, and a claim already made is never taken back. If a member’s tier later falls, the rule applies only to new claims. A reward with no minimum tier behaves as it always has: every member can claim it when the existing cap, stock, and XP balance checks allow it. A claim can be refused for one of these reasons. Treat these as expected outcomes, not generic server errors:

Reward codes

A reward whose fulfillment type is code issues a single-use code when it is claimed, in the same transaction as the burn. The code comes back as code on the claim (POST /v1/users/{id}/redeem) and on the member’s claims list (GET /v1/users/{id}/redemptions). Set fulfillment.params.code_prefix (1 to 12 uppercase letters or digits, for example VEGA10) to brand it: VEGA10-7K3QX9MPLA. The platform tracks whether a code was used; what a code is worth is up to the partner honouring it. When your checkout applies one, mark it used with POST /v1/users/{id}/redemptions/{redemptionId}/use from your backend (an org key, never a member session). It sets code_used_at once; a second use is refused with REWARD_CODE_USED (409), so call it before you charge. Send an Idempotency-Key tied to the exact checkout attempt and its contents: retrying that same use replays the original response, so a failed checkout can resume without spending another code. A different checkout must use a different key. See Mark a reward code used. A partner backend that signs fans in itself reads a member’s catalog, with the same claimability verdicts the member sees, at GET /v1/users/{id}/rewards (reference). Keep a catalog useful while you gate it: putting most rewards behind the top tier makes the program read as closed. This is merchandising guidance, not a product restriction. A tier that rewards are gated at cannot be deleted while they still require it. Deleting one, or saving a ladder that leaves one out, is refused with TIER_HAS_GATED_REWARDS (409) and nothing is written. The blocking rewards come back in error.details.rewards, each with its id, name, and the minTierId it requires, so when a single save removes several tiers, you can tell which removal each blocker is about. Clear or move minTierId on those rewards, then delete the tier.

On-chain settlement

Member balances are backed by an XP token on LightLink (an Ethereum L2; nonprod uses the Pegasus testnet). Awards mint to the member’s platform-custodied wallet, redemptions burn, and the platform continuously reconciles ledger totals against on-chain supply. As an integrator you don’t have to touch any of this (the REST API and ledger are the interface), but the trail is publicly verifiable.

API reference

Every route below authenticates with your organization API key as a bearer header (see Authentication). XP amounts are integer strings in XP minor units, never JSON numbers. Errors use the standard envelope: { "error": { "code", "status", "message", "details" } }.

Tier ladder

GET /v1/tiers lists your organization’s ladder, lowest threshold first.
200 OK:
Tier fields are camelCase. threshold is the qualifying XP a member needs. basis is always lifetime. benefits is the object your organization attached to the tier. An organization with no ladder returns { "tiers": [] }. GET /v1/tiers/{tierId} returns one tier in the same shape. A tier id outside your organization returns 404 NOT_FOUND. Creating, changing and deleting tiers needs a partner admin session. An org key gets 403 FORBIDDEN on those calls.

Balance and tier

200 OK:
The fields are camelCase. spendableBalance is what the member can redeem. lifetimeQualifyingXp is what tiers rank on. tier is picked from the current ladder on every read and is null when no tier’s threshold is at or below the member’s qualifying XP, which in practice means your organization has no ladder. asOf is the time of the read, not the time the tier last changed, so don’t poll it as a tier-change signal. This read does not check membership: a user id with no XP in your organization reads as "0" for both figures.

XP history

GET /v1/users/{userId}/xp-history pages through the member’s ledger entries, newest first. It takes limit (1 to 50, default 25) and the opaque cursor from the previous page’s page_info.next_cursor. The full response schema is in the API reference.

XP expiry schedule

200 OK when your organization has expiry on:
schedule has one entry per UTC day on which the member loses XP, earliest first. The first entry can already be in the past when the lapse run has not caught up with it; that XP is still on the balance and still spendable. next is the earliest entry still ahead of now, or null when there is none. A member who has never earned gets "next": null and "schedule": []. When your organization has expiry off, the read answers without looking at the member:
With expiry on, a user id that isn’t a member of your organization returns 404 NOT_FOUND. So does a path segment that isn’t a valid id.

Active boost

200 OK while a boost reaches the member:
multiplier is a decimal string, for example "2" or "1.5". endsAt is camelCase. When no boost reaches the member right now, the response is { "boost": null }. A path segment that isn’t a valid id returns 404 NOT_FOUND.

Member reward catalog

GET /v1/users/{userId}/rewards returns your organization’s published rewards, newest first, each with the verdict for this member.
200 OK:
remaining_stock and image_url are null when the reward has unlimited stock or no image. min_tier_id and min_tier_name are present only on a tier-gated reward. claimability is { "state": "affordable" } or { "state": "blocked", "reason": … } with exactly one reason, checked in this order:

One claim

GET /v1/users/{userId}/redemptions/{redemptionId} returns a single claim.
200 OK:
status is one of pending, burned, fulfilled or failed. code is null unless the reward issues codes, and code_used_at stays null until the code is marked used. A claim that belongs to another member or another organization returns 404 NOT_FOUND.

Mark a reward code used

POST /v1/users/{userId}/redemptions/{redemptionId}/use takes no body. The Idempotency-Key header is optional and recommended.
200 OK returns the claim in the shape above, with code_used_at set. Retrying with the same Idempotency-Key replays this response.