Rules

Building a rule

A rule definition has four parts, applied in this order: measure → condition → payout → limits.

{
  "measure": { "type": "sum", "metricKey": "settlement_volume" },
  "condition": { "type": "growth_vs_previous_period", "minGrowthPercent": "10" },
  "payout": { "type": "percent_of_measure", "of": "growth", "rate": "0.001" },
  "limits": [{ "type": "cap_per_period", "amount": "500" }]
}

This rule pays 0.1% of a participant’s settlement-volume growth when it beats the previous period by at least 10%, up to 500 per period.

Measure: what the rule reads, for the days the participant earned in the period.

Type Reads Value
daily_balance A snapshot metric with subject address Balance-days: each wallet’s daily balance, summed over the days. Calculated per wallet.
sum An event or total metric The sum of values
count An event metric The number of events
average An event or snapshot metric The average event value, or the average daily balance
status KYB approval ("status": "kyb_approved") 1 once the participant is enrolled and KYB-approved, otherwise 0

Condition: when the rule pays.

Type Fields Pays when
always — Always
first_time — The participant qualifies for the first time
threshold min and/or max The measured value is within the bounds (inclusive)
growth_vs_previous_period minGrowthPercent The value grew by at least this percentage over the participant’s previous period

Payout: how much it pays.

Type Fields Pays
fixed amount A fixed amount
per_unit amount amount × the measured value
percent_of_measure rate, of (value or growth) rate × the value, or × the growth over the previous period
annual_rate rate Balance-days × rate ÷ 365
tiered tiers The tier the value falls in pays its fixed amount or rate × the value

Rates are fractions: "0.035" is 3.5%.

Limits (optional, each at most once):

Type Fields Effect
cap_per_period amount The rule pays at most this much per participant per period
once_per_participant — The rule pays a participant only once

Not every combination makes sense, so each measure allows certain blocks:

Measure Conditions Payouts Limits
daily_balance always annual_rate cap_per_period
sum always, first_time, threshold, growth_vs_previous_period fixed, per_unit, percent_of_measure, tiered both
count always, first_time, threshold, growth_vs_previous_period fixed, per_unit, tiered both
average always, threshold, growth_vs_previous_period fixed, per_unit, percent_of_measure, tiered both
status always, first_time fixed both

GET /rule-blocks returns this catalog with a JSON Schema for each block, and POST /rules/validate checks a definition before you save it. The full field rules are in Rule definitions.

Rule library

Rule definitions

A definition is an object with exactly four fields: measure, condition, payout and limits (an array, which may be empty). The concepts are explained in Building a rule.

Measure

type Fields Metric it can read
daily_balance metricKey snapshot, subject address
sum metricKey event or total
count metricKey event
average metricKey event or snapshot
status status: "kyb_approved" —

Condition

type Fields Notes
always —
first_time — Pays only in the period the participant first qualifies
threshold min, max (amounts) At least one is required; min ≤ max; bounds are inclusive
growth_vs_previous_period minGrowthPercent (amount; "10" = 10%) Needs a previous period with a value above zero

Payout

type Fields Notes
fixed amount (> 0)
per_unit amount (> 0) amount × the measured value
percent_of_measure rate (> 0), of: value or growth of: "growth" needs the growth_vs_previous_period condition
annual_rate rate (> 0) Balance-days × rate ÷ 365
tiered tiers: 1–20 of { from, to?, amount? , rate? } Each tier sets exactly one of amount or rate. Tiers must be in order with no gaps: each to equals the next from, and only the last tier omits to. A value below the first tier pays 0.

percent_of_measure, annual_rate and rate-based tiers multiply the measured value, so the metric’s unit must be the program’s reward asset.

Limits

type Fields
cap_per_period amount (0 or more)
once_per_participant —

Validation. Definitions are checked in two stages, and the first problem is returned as 422 with a path:

  1. Structure (any definition): invalid_definition, condition_not_allowed, payout_not_allowed, limit_not_allowed, duplicate_limit, growth_payout_needs_growth_condition, tiers_overlap, tiers_gap.
  2. Program fit (when the rule is attached, previewed, or validated against a program): metric_not_found, metric_deprecated, metric_kind_mismatch, metric_subject_mismatch, unit_mismatch.

The Rule object

Field Type Description
id UUID Rule ID
name string 1–200 characters
description string or null Up to 2,000 characters
version integer Latest version
status string active or archived
isDefault boolean true for the rules your library starts with
definition object Definition of the latest version
summary string The rule in one sentence
usedBy array Where the rule is in force or scheduled: { programId, programName, ruleKey, ruleVersion, effectivePeriodId }
createdAt, updatedAt timestamp

List rule blocks

GET /rule-blocks → 200 { measures, conditions, payouts, limits }

The catalog of rule building blocks. Each measure lists the conditions, payouts and limits it allows; each block includes a description and a JSON Schema (paramsSchema) for its fields.

{
  "measures": [
    {
      "type": "status",
      "description": "1 once the participant is KYB-approved and enrolled, else 0.",
      "paramsSchema": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "const": "status" },
          "status": { "type": "string", "const": "kyb_approved" }
        },
        "required": ["type", "status"],
        "additionalProperties": false
      },
      "conditions": ["always", "first_time"],
      "payouts": ["fixed"],
      "limits": ["cap_per_period", "once_per_participant"]
    }
  ],
  "conditions": ["…"],
  "payouts": ["…"],
  "limits": ["…"]
}

List rule templates

GET /rule-templates → 200 { templates: Template[] }

Templates are shorthand for common rules, accepted when you create or update a program.

Template Params Rule it creates
kyb_approval amount A one-time amount in the first period the participant is enrolled and KYB-approved
holdings metricKey, annualRate annualRate a year on a daily-balance metric whose unit is the reward asset

A template item can also set cap, a per-period cap.

List rules

GET /rules → 200 { rules: Rule[] }

Query Description
status active (default) or archived

Sorted by name. A read-only key sees only the rules used by its program.

Get a rule

GET /rules/{ruleId} → 200 Rule

{
  "id": "de3b5ba5-fc7f-479a-8df8-7009c50da521",
  "name": "KYB bonus",
  "description": "A bonus once a participant is KYB-approved",
  "version": 2,
  "status": "active",
  "isDefault": false,
  "definition": {
    "measure": { "type": "status", "status": "kyb_approved" },
    "condition": { "type": "always" },
    "payout": { "type": "fixed", "amount": "75" },
    "limits": [{ "type": "once_per_participant" }]
  },
  "summary": "75 on KYB approval, once per participant",
  "usedBy": [
    {
      "programId": "2e369621-ef27-499f-88c0-15032f767c54",
      "programName": "Partner Rewards",
      "ruleKey": "kyb_bonus",
      "ruleVersion": 1,
      "effectivePeriodId": "2026-09"
    },
    {
      "programId": "2e369621-ef27-499f-88c0-15032f767c54",
      "programName": "Partner Rewards",
      "ruleKey": "kyb_bonus",
      "ruleVersion": 2,
      "effectivePeriodId": "2026-11"
    }
  ],
  "createdAt": "2026-10-13T16:06:17.880Z",
  "updatedAt": "2026-10-13T16:06:35.907Z"
}

Here the program uses version 1 now and moves to version 2 from November. Returns 404 Rule not found if the rule doesn’t exist in your account.

List rule versions

GET /rules/{ruleId}/versions → 200 { versions: [{ version, definition, summary, createdAt }] }

Every version of the rule, oldest first.

Create a rule

POST /rules → 201 Rule

Body field Type Required Description
name string Yes 1–200 characters
description string No Up to 2,000 characters
definition object Yes A rule definition

The rule is created at version 1. It isn’t used anywhere until you attach it to a program.

POST /api/v1/rewards/rules
Idempotency-Key: create-kyb-bonus

{
  "name": "KYB bonus",
  "description": "A bonus once a participant is KYB-approved",
  "definition": {
    "measure": { "type": "status", "status": "kyb_approved" },
    "condition": { "type": "always" },
    "payout": { "type": "fixed", "amount": "50" },
    "limits": [{ "type": "once_per_participant" }]
  }
}

Errors: 422 structure problems (see Rule definitions), for example {"error":"tiers_gap","message":"tier 1 must start at 10","path":"definition.payout.tiers[1].from"}; 409 library_full.

Validate a rule

POST /rules/validate → 200 { valid: true, summary }

Checks a definition without saving it. Add programId to also check that it fits that program.

POST /api/v1/rewards/rules/validate
Idempotency-Key: validate-settlement-tiers

{
  "programId": "2e369621-ef27-499f-88c0-15032f767c54",
  "definition": {
    "measure": { "type": "sum", "metricKey": "settlement_volume" },
    "condition": { "type": "threshold", "min": "1000" },
    "payout": {
      "type": "tiered",
      "tiers": [
        { "from": "0", "to": "10000", "amount": "10" },
        { "from": "10000", "rate": "0.002" }
      ]
    },
    "limits": [{ "type": "cap_per_period", "amount": "500" }]
  }
}
{
  "valid": true,
  "summary": "by tier of settlement_volume (0 to 10000: 10; 10000 and up: 0.2% of it) when it is at least 1000, at most 500 per period"
}

An invalid definition returns the same 422 that saving it would, for example {"error":"condition_not_allowed","message":"condition threshold does not apply to measure daily_balance","path":"definition.condition.type"}.

Archive a rule

POST /rules/{ruleId}/archive → 200 Rule

Archived rules can’t be attached or upgraded to, and are hidden from GET /rules unless you ask for status=archived. Programs already using the rule keep it, including changes already scheduled. Archiving an archived rule changes nothing.

Update a rule

PATCH /rules/{ruleId} → 200 Rule

Body field Type Required Description
expectedVersion integer Yes The rule’s latest version
name string No Changes in place
description string or null No null clears it
definition object No A new definition creates version version + 1

A new definition applies to programs automatically. Every program using the rule moves to the new version from its next period (or its first period, if it hasn’t started). The current period keeps the version it is being calculated with. Programs that have ended, and rule keys scheduled to be detached, are skipped. If the new version doesn’t fit any of those programs, the whole update fails with 422 naming the program and rule key, and nothing changes.

A changed name or description doesn’t affect programs, and an unchanged definition doesn’t create a version.

PATCH /api/v1/rewards/rules/de3b5ba5-fc7f-479a-8df8-7009c50da521
Idempotency-Key: kyb-bonus-75

{
  "expectedVersion": 1,
  "definition": {
    "measure": { "type": "status", "status": "kyb_approved" },
    "condition": { "type": "always" },
    "payout": { "type": "fixed", "amount": "75" },
    "limits": [{ "type": "once_per_participant" }]
  }
}

The response is the rule at version 2; its usedBy shows each program’s move to version 2 (see Get a rule).

Errors:

  • 404 Rule not found
  • 409 {"error":"version_conflict","message":"Rule is at version 2, not 1"}
  • 422 structure problems, or a program the new version doesn’t fit, for example {"error":"metric_not_found","message":"Program Partner Rewards, rule growth: metric not_a_metric does not exist","path":"definition.measure.metricKey"}

Program rules

These endpoints attach library rules to a program and change them. Every change takes effect from the program’s next period, or its first period if it hasn’t started, and the response shows it as scheduled. Two changes to the same rule key for the same period: the later one replaces the earlier.

Change response

{
  "ruleKey": "kyb_bonus",
  "ruleId": "de3b5ba5-fc7f-479a-8df8-7009c50da521",
  "ruleVersion": 1,
  "action": "attach",
  "effectivePeriodId": "2026-11",
  "status": "scheduled"
}

action is attach, upgrade or detach; ruleVersion is null for a detach.

List program rules

GET /programs/{id}/rules → 200 { rules, scheduled }

  • rules: for each rule key, the rule in force in the current period.
  • scheduled: for each rule key, the next change, including detaches.

Each item has ruleKey, ruleId, ruleVersion, name, definition, summary, action and effectivePeriodId.

{
  "rules": [
    {
      "ruleKey": "kyb_bonus",
      "ruleId": "de3b5ba5-fc7f-479a-8df8-7009c50da521",
      "ruleVersion": 1,
      "name": "KYB bonus",
      "definition": {
        "measure": { "type": "status", "status": "kyb_approved" },
        "condition": { "type": "always" },
        "payout": { "type": "fixed", "amount": "50" },
        "limits": [{ "type": "once_per_participant" }]
      },
      "summary": "50 on KYB approval, once per participant",
      "action": "attach",
      "effectivePeriodId": "2026-09"
    }
  ],
  "scheduled": [
    {
      "ruleKey": "kyb_bonus",
      "ruleId": "de3b5ba5-fc7f-479a-8df8-7009c50da521",
      "ruleVersion": null,
      "name": "KYB bonus",
      "definition": null,
      "summary": null,
      "action": "detach",
      "effectivePeriodId": "2026-11"
    }
  ]
}

Attach a rule

POST /programs/{id}/rules → 201 Change

Body field Type Required Description
ruleKey string Yes The program’s name for the rule, lowercase snake_case
ruleId UUID Yes Library rule
ruleVersion integer Yes Version to use

A rule key stays linked to the same library rule for the life of the program: after a detach you can attach the key again, but only to the same rule.

POST /api/v1/rewards/programs/2e369621-ef27-499f-88c0-15032f767c54/rules
Idempotency-Key: attach-holdings-bonus

{ "ruleKey": "holdings_bonus", "ruleId": "733693c6-42f4-4c4f-8cdc-4df8eb53db2b", "ruleVersion": 1 }

Errors:

  • 404 Rule not found, 404 rule_version_not_found
  • 409 rule_archived, 409 rule_key_taken (the key is linked to a different rule), 409 rule_already_attached, 409 program_ended
  • 422 too_many_rules, or the rule doesn’t fit the program

Preview a rule

POST /programs/{id}/rules/preview → 200

Shows what one rule would pay in a period, using the program’s real activity, without saving anything. Preview a saved version or an unsaved definition:

Body field Type Description
ruleId + ruleVersion UUID + integer A library rule version
or definition object An unsaved rule definition
periodId string Optional. Defaults to the current period
participantId string Optional. One participant’s externalId; the result then also explains a zero amount

The preview runs the rule on its own, without the program’s other rules or adjustments. If the rule is already attached to the program, its history (for once_per_participant and first_time) is taken into account.

POST /api/v1/rewards/programs/2e369621-ef27-499f-88c0-15032f767c54/rules/preview
Idempotency-Key: preview-growth-v2-sep

{ "ruleId": "695c549d-9e03-4818-ba1f-701c64d9b3cc", "ruleVersion": 2, "periodId": "2026-09" }
{
  "periodId": "2026-09",
  "participants": 4,
  "earning": 1,
  "total": "1.000000",
  "sample": [
    {
      "participantId": "P-1001",
      "address": null,
      "amount": "1.000000",
      "explanation": {
        "measure": { "type": "sum", "value": "2000.000000", "previous": "1000.000000" },
        "condition": {
          "type": "growth_vs_previous_period",
          "minGrowthPercent": "10",
          "met": true,
          "growthPercent": "100.00"
        },
        "payout": {
          "type": "percent_of_measure",
          "rate": "0.001",
          "of": "growth",
          "base": "1000.000000",
          "beforeLimits": "1.000000"
        },
        "limits": { "capPerPeriod": { "cap": "900", "applied": false } }
      }
    }
  ]
}
Response field Description
participants Participants in the period
earning Participants who would earn more than zero
total Total the rule would pay
sample Up to 20 results, largest first, each with an explanation

Errors: 404 Period not found, 404 Participant not found, 404 Rule not found; 409 preview_too_large (more than 10,000 participants); 422 the rule doesn’t fit the program.

Upgrade or roll back a rule

PATCH /programs/{id}/rules/{ruleKey} → 200 Change

Body field Type Required Description
ruleVersion integer Yes Any version of the linked library rule, newer or older

Moves the rule key to that version from the next period. Sent while a detach is scheduled, it cancels the detach.

Errors: 404 rule_not_attached, 404 rule_version_not_found; 409 rule_archived, 409 program_ended; 422 the version doesn’t fit the program.

Detach a rule

DELETE /programs/{id}/rules/{ruleKey} → 200 Change

The rule stops applying from the next period; it keeps paying in the current one. Returns the change with action: "detach".

Errors: 404 rule_not_attached (including a rule already detached); 409 program_ended.