Periods and rewards

How periods work

  • Periods start on the program’s periodAnchorDay (1–28, default 1) and end the day before the next month’s anchor day. With the default, periods are calendar months.
  • A period is identified by YYYY-MM, the month it starts in. The program’s first and last periods are shortened to its start and end dates.
  • Each period has a grace window of graceDays (0–31, default 5) after it ends, so late activity can still arrive before the period is finalized.
  • A period moves from OPEN to FINALIZING to FINALIZED. Finalize periods in order, once their grace window has passed. To correct a finalized period, add an adjustment, which lands in the current period.

How a reward is calculated

Rewards are calculated per participant and period, in the background, whenever something that affects them changes: new activity, a KYB change, an adjustment or a rule change. A response shows pendingRecalculation: true while a newer calculation is on its way.

  1. Earning window. A participant earns from the later of the period start and their enrollment date, through the earlier of the period end and yesterday. Today counts once it is over, because a day’s balance isn’t final until then.
  2. Each rule measures the participant’s activity in that window, checks its condition and calculates its payout. The payout is rounded down to 6 decimal places.
  3. Rule limits apply: a rule that already paid a participant under once_per_participant pays nothing, and cap_per_period scales the rule’s amount down to its cap.
  4. Adjustments you have added for the participant are added to the total. Adjustments aren’t capped.

Each result is a reward line: one per rule, or one per wallet for daily_balance rules. All arithmetic is exact (no floating point). Amounts are returned as strings with 6 decimal places.

Periods and rewards

List periods

GET /programs/{id}/periods → 200 { periods: Period[] }

Every period from the program’s start through the current one, oldest first.

Field Description
periodId YYYY-MM
startsOn, endsOn First and last day
status OPEN, FINALIZING or FINALIZED
graceEndsAt When the period can be finalized
finalizedAt When it was finalized
pendingRecalculations Calculations still in progress for the period
failedRecalculations Calculations that failed repeatedly. These must be resolved before the period can be finalized.
{
  "periods": [
    {
      "periodId": "2026-08",
      "startsOn": "2026-08-01",
      "endsOn": "2026-08-31",
      "status": "FINALIZED",
      "graceEndsAt": "2026-09-06T00:00:00.000Z",
      "finalizedAt": "2026-10-13T16:06:25.035Z",
      "pendingRecalculations": 0,
      "failedRecalculations": 0
    },
    {
      "periodId": "2026-09",
      "startsOn": "2026-09-01",
      "endsOn": "2026-09-30",
      "status": "OPEN",
      "graceEndsAt": "2026-10-06T00:00:00.000Z",
      "finalizedAt": null,
      "pendingRecalculations": 0,
      "failedRecalculations": 0
    }
  ]
}

Get period rewards

GET /programs/{id}/periods/{periodId}/rewards → 200

Each participant’s reward for the period, with every reward line and adjustment.

Query Description
limit, offset Paging. Default 50, max 500.
participantId One participant’s externalId
{
  "periodId": "2026-08",
  "status": "FINALIZED",
  "total": 2,
  "rewards": [
    {
      "participantId": "P-1001",
      "total": "42.500000",
      "status": "FINALIZED",
      "calculatedAt": "2026-10-13T16:06:25.035Z",
      "pendingRecalculation": false,
      "lines": [
        {
          "ruleKey": "onboarding_bonus",
          "ruleVersion": 1,
          "address": null,
          "amount": "25.000000",
          "explanation": {
            "measure": { "type": "status", "value": "1.000000" },
            "condition": { "type": "always", "met": true },
            "payout": { "type": "fixed", "amount": "25", "beforeLimits": "25.000000" },
            "limits": { "oncePerParticipant": { "alreadyPaid": false } }
          }
        },
        {
          "ruleKey": "usdpt_holdings",
          "ruleVersion": 1,
          "address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
          "amount": "17.500000",
          "explanation": {
            "measure": { "type": "daily_balance", "value": "182500.000000" },
            "condition": { "type": "always", "met": true },
            "payout": { "type": "annual_rate", "rate": "0.035", "base": "182500.000000", "beforeLimits": "17.500000" },
            "limits": {}
          }
        }
      ],
      "adjustments": []
    }
  ]
}

In this example, P-1001 held 18,250 USDPT for 10 days in August: 182,500 balance-days × 3.5% ÷ 365 = 17.50, plus the 25 onboarding bonus.

Field Description
total (top level) The number of participants matching the query, for paging
rewards[].total The participant’s reward: lines plus adjustments. Can be negative after an adjustment.
rewards[].status ESTIMATED until the period is finalized, then FINALIZED
rewards[].pendingRecalculation true while a newer calculation is queued
rewards[].lines Reward lines
rewards[].adjustments { id, amount, reason, createdAt }

Participants appear once they have a reward line or adjustment in the period.

Finalize a period

POST /programs/{id}/periods/{periodId}/finalize → 200

Locks the period’s rewards permanently. This can’t be undone. Finalize periods in order, once each period’s grace window has passed. The call waits for any calculations in progress to finish, so it can take up to about 60 seconds.

{
  "periodId": "2026-08",
  "startsOn": "2026-08-01",
  "endsOn": "2026-08-31",
  "graceEndsAt": "2026-09-06T00:00:00.000Z",
  "status": "FINALIZED",
  "finalizedAt": "2026-10-13T16:06:25.035Z",
  "finalizedBy": "33907490-5a41-440c-82e6-0f7880833d14",
  "participantCount": 2,
  "total": "44.897260"
}

finalizedBy is the ID of the API key that finalized the period, and total is the sum of every participant’s reward. After finalizing, activity and deletions for the period are rejected with period_finalized.

Errors (all 409):

error Meaning
period_already_finalized The period is already finalized
grace_window_open: ends at <time> Wait until the grace window ends
previous_period_not_finalized: <periodId> Finalize the earlier period first
recalculation_pending: <n> participants still queued Calculations didn’t finish in time; retry
recalculation_failed: <participants> Some calculations failed repeatedly; contact TBMC

Export rewards

GET /programs/{id}/periods/{periodId}/rewards/export → 200 text/csv

A CSV of a finalized period, one row per participant, for your payment or commission run. An open period returns 409 period_not_finalized.

participantId,onboarding_bonus,usdpt_holdings,adjustments,total,ruleVersions
P-1001,25.000000,17.500000,0.000000,42.500000,onboarding_bonus:1;usdpt_holdings:1
P-1002,0.000000,2.397260,0.000000,2.397260,usdpt_holdings:1
Column Description
participantId The participant’s externalId
One column per rule key The participant’s amount for that rule
adjustments Sum of adjustments
total Rule amounts plus adjustments; matches rewards[].total
ruleVersions The rule versions used, as ruleKey:version separated by ;

Text values that start with =, +, - or @ are prefixed with ' so spreadsheets don’t treat them as formulas.

Adjustments

An adjustment adds or subtracts an amount from a participant’s reward, for example to correct an earlier period. Adjustments always land in the program’s current period and can’t be edited or deleted: to reverse one, add an opposite adjustment.

Add an adjustment

POST /programs/{id}/adjustments → 201

Body field Type Required Description
participantId string Yes The participant’s externalId
amount string Yes Signed decimal, up to 6 decimal places, not zero. "-2.5" subtracts 2.5.
reason string Yes 1–500 characters
POST /api/v1/rewards/programs/2e369621-ef27-499f-88c0-15032f767c54/adjustments
Idempotency-Key: adj-P-1002-duplicate-deposit

{ "participantId": "P-1002", "amount": "-2.5", "reason": "Duplicate deposit reversed" }
{
  "id": "15e1a3be-0219-440e-bd29-ddb3b19ce3c1",
  "amount": "-2.500000",
  "createdAt": "2026-10-13T16:06:16.809Z",
  "participantId": "P-1002",
  "periodId": "2026-10",
  "reason": "Duplicate deposit reversed",
  "createdBy": "33907490-5a41-440c-82e6-0f7880833d14"
}

createdBy is the ID of the API key that added the adjustment.

Errors: 404 Participant not found; 409 no_open_period (the program has no open current period).

List adjustments

GET /programs/{id}/adjustments → 200 { adjustments, total }

Query Description
participantId One participant’s adjustments
periodId Adjustments in one period
limit, offset Paging. Default 50, max 500.

Newest first.

Statistics

GET /programs/{id}/stats → 200

Program totals and the reward total for each period.

{
  "currentPeriodId": "2026-10",
  "totals": {
    "participantsEnrolled": 2,
    "earningParticipants": 1,
    "estimatedTotal": "1.917808",
    "finalizedTotal": "44.897260",
    "earningBalance": "25000.000000"
  },
  "programBudget": { "amount": "150000.000000", "used": "50.650684", "remaining": "149949.349316", "over": false },
  "granularity": "monthly",
  "series": [
    { "periodId": "2026-08", "status": "FINALIZED", "total": "44.897260" },
    { "periodId": "2026-09", "status": "OPEN", "total": "3.835616" },
    { "periodId": "2026-10", "status": "OPEN", "total": "1.917808" }
  ]
}
Field Description
totals.participantsEnrolled Participants currently enrolled
totals.earningParticipants Participants earning in the current period
totals.estimatedTotal Rewards in the current period so far
totals.finalizedTotal Rewards in all finalized periods
totals.earningBalance Participants’ latest reported balances, combined
series Reward total per period, oldest first (up to the last 120 periods)
programBudget Budget usage for the program, or null with no budget. used is the reward total across every period, estimated and finalized.

Objects

Reward line object

Field Description
ruleKey The rule that produced the line
ruleVersion The rule version used
address The wallet, for daily_balance rules; otherwise null
amount The amount after limits
explanation How the amount was reached, below

Explanation object

Field Description
measure.type, measure.value What was measured, and the value (balance-days for daily_balance)
measure.previous The previous period’s value, for growth conditions
condition.type, condition.met The condition, and whether it was met
condition.reason Why it wasn’t met: not_first_time, below_min, above_max, no_previous_period, no_previous_value, growth_below_min
condition.growthPercent The growth, for growth conditions
payout.type, payout.beforeLimits The payout, and the amount before limits
payout.amount, payout.rate, payout.base, payout.units, payout.of, payout.tier The payout’s inputs, depending on its type
limits.oncePerParticipant.alreadyPaid true if the rule had already paid this participant
limits.capPerPeriod.cap, .applied The rule’s cap, and whether it reduced the amount

Comparing amount with payout.beforeLimits shows what the limits took off.

Budget usage object

Program and organization budgets are reported in this shape, or as null when no budget is set.

Field Description
amount The budget
used Rewards counted against the budget so far, estimated and finalized
remaining amount minus used. Negative once spending passes the budget.
over true when used is more than amount. Spending exactly the budget isn’t over.