LOT-mode assets track every credit separately.
Use lots when points expire after a fixed window, when rewards should vest before they become spendable, when auth and settlement events need to match by reference_id (the auth/settlement pattern), or when reporting needs to preserve the issuer of redeemed value.
SIMPLE-mode assets do not create lots. See Asset configuration for when to choose each mode, and SIMPLE vs LOT mode for a feature comparison.
How lots work
When aLOT-mode asset is credited, a new lot is created:
Lot lifecycle
A lot moves through a series of statuses from creation to consumption or expiration.Expiration
Setexpires_at on a CREDIT action to give lots a deadline. The value can be a positive relative duration, a fixed timestamp, or a ${{ }} expression that resolves to either. To see when outstanding value will expire across a program or asset, for breakage estimation or forecasting, use the expiration schedule report.
A relative duration counts from the moment the lot is created, which is when the event is processed, not the event’s event_timestamp:
${{ }} expression sets a per-event deadline. The expression must resolve to a string in one of the two forms above. This is how you get occurrence-based or calendar-exact expiry: your backend computes the timestamp and sends it in event_data:
expires_at exactly. From that moment the lot is excluded from every operation that consumes or reserves value, so the credits cannot be spent even before the bookkeeping catches up. The bookkeeping follows within a few minutes: a background sweep marks the lot EXPIRED, posts the forfeited value to breakage as an EXPIRATION journal entry, and emits a balance.expired webhook.
Until the sweep runs, the balances API still includes the expired value in available, so a balance read in that brief window can show credits that can no longer be spent. The expiration schedule report accounts for the same window: value already past expires_at but not yet swept appears under expiring_before_window, then drops out of the report once forfeited.
Durations accept h, d (24 hours), and w (168 hours) units, plus the standard Go units down to seconds and below (m, s, ms). Units can be combined ("1w2d12h") and fractional values are allowed. Months and years are not accepted because their length varies by calendar date. For an approximate policy, use days ("180d" for approximately six months). For an exact calendar policy (“expires on the same day six months later”), compute the date in your backend, include it in event_data, and resolve it through a ${{ }} expression. Use a literal fixed timestamp only when every lot should share the same deadline.
Vesting
Setmatures_at on a CREDIT action to create a vesting period. Lots credited with a future matures_at land in DEFERRED status and automatically transition to AVAILABLE after the date passes. Deferred lots are excluded from debit operations.
The move happens shortly after the date passes. Scrip then sends a balance.matured webhook with the amount that became spendable. Use it to refresh the participant’s balance instead of polling.
DEFERRED cannot be targeted as a bucket for writes. It is read-only and available as a filter in query endpoints (e.g., status=DEFERRED on the lots list, or bucket=DEFERRED on journal entries).
A card settlement can use matures_at too, so settled rewards wait until a date such as the end of a billing cycle. See Delay when settled rewards can be spent.
Oldest-first spending
When debiting aLOT-mode asset, Scrip spends the oldest eligible lots first:
Viewing lots
Inspect a participant’s lots for a specific asset:Lot-aware operations
Hold, release, and forfeit operations onLOT-mode assets are lot-aware:
Lot bucket transitions (
AVAILABLE ↔ HELD) update the lot row in place, preserving the lot UUID across holds, releases, and settles. Partial transitions split the remainder into a new lot.
These operations return a lots_processed array showing which lots were affected and by how much:
SIMPLE vs LOT mode
The asset’sinventory_mode determines whether credits are tracked individually or as a running total.
Use
SIMPLE for balances with no per-credit lifecycle or provenance needs. Use LOT when expiration, vesting, auth/settlement matching, oldest-first spending, issuer attribution, or partner settlement matters.
A
CREDIT that sets expires_at or matures_at requires a LOT-mode asset. On a SIMPLE-mode asset, either field is an error, whatever the value or bucket. Saving a rule with such a CREDIT returns a 400 with reason invalid_inventory_mode, and validate with program_id reports the same error.The balance adjust endpoints for participants and groups reject a CREDIT that sets either field with 400 invalid_inventory_mode. During event processing, a credit to a SIMPLE-mode asset with either field fails, and the event fails with it.