Skip to content

How a Balance combines an amount, token ID ranges, and ownership time ranges, and the rules for expanding, subtracting, and merging them.

A Balance says how many of which token IDs an address owns, and during which times. Ownership times let a token belong to someone for a window rather than forever, which is how vesting, subscriptions, and time-bound access work without extra logic.

Shape

json
{
  "amount": "1",
  "tokenIds": [
    { "start": "1", "end": "10" },
    { "start": "20", "end": "30" }
  ],
  "ownershipTimes": [
    { "start": "20", "end": "50" },
    { "start": "100", "end": "200" }
  ]
}
ts
export interface Balance<T extends NumberType> {
  amount: T;
  tokenIds: UintRange<T>[];
  ownershipTimes: UintRange<T>[];
}
FieldTypeRequiredDescription
amountUintyesQuantity owned. Must be greater than zero.
tokenIdsUintRange[]yesToken ID ranges the amount applies to
ownershipTimesUintRange[]yesTime ranges (UNIX milliseconds) during which the tokens are owned

A user's balance store holds an array of these. Approvals, trackers, and transfers all use the same structure.

Ask your agent:

text
Show bob's balance of token ID 1 in collection 1 right now.

The MCP builder tools (query_balance) produce the objects on this page.

How It Works

Expansion

One Balance with several ID ranges and several time ranges means every combination. Read it as nested loops:

js
for (const balance of balances) {
  for (const tokenIdRange of balance.tokenIds) {
    for (const ownershipTimeRange of balance.ownershipTimes) {
      // owns balance.amount of tokenIdRange during ownershipTimeRange
    }
  }
}

The example above expands to:

  1. 1x of IDs 1-10 during times 20-50
  2. 1x of IDs 1-10 during times 100-200
  3. 1x of IDs 20-30 during times 20-50
  4. 1x of IDs 20-30 during times 100-200

Subtraction

Subtracting a slice can split one Balance into several. Removing "1x of IDs 1-10 during times 20-50" from the example leaves:

json
[
  {
    "amount": "1",
    "tokenIds": [
      { "start": "1", "end": "10" },
      { "start": "20", "end": "30" }
    ],
    "ownershipTimes": [{ "start": "100", "end": "200" }]
  },
  {
    "amount": "1",
    "tokenIds": [{ "start": "20", "end": "30" }],
    "ownershipTimes": [{ "start": "20", "end": "50" }]
  }
]

Duplicates Add

Overlapping entries in a Balance[] are added together. Within a single Balance, the tokenIds ranges must not overlap each other, and neither may the ownershipTimes ranges. For example, these two valid entries add:

json
[
  {
    "amount": "1",
    "tokenIds": [{ "start": "1", "end": "10" }],
    "ownershipTimes": [{ "start": "100", "end": "200" }]
  },
  {
    "amount": "1",
    "tokenIds": [{ "start": "1", "end": "10" }],
    "ownershipTimes": [{ "start": "100", "end": "200" }]
  }
]

Their combined balance is:

json
{
  "amount": "2",
  "tokenIds": [{ "start": "1", "end": "10" }],
  "ownershipTimes": [{ "start": "100", "end": "200" }]
}

Ownership Times in Practice

Most collections do not need time-bound ownership. Use the full range [{ "start": "1", "end": "18446744073709551615" }] and set the noCustomOwnershipTimes invariant to lock that in. Transfers, approvals, and trackers all carry ownership times, so a transfer can move "IDs 1-10 for 2026 only" and leave the rest with the sender.

Guidance

  • Store balances in the most compact form: merge adjacent ranges and avoid duplicate ranges.
  • When you subtract, split the remainder correctly across both dimensions.
  • Treat time as a real dimension. Two balances with the same IDs but different times are different holdings.

Edit this page on GitHub