Skip to content

predeterminedBalances: force each use of an approval to move exact balances in order, by manual list or increments, with precalculation and scaling.

Predetermined balances replace "up to N" with "exactly these". Each use of the approval must move precisely the balances the approval computes for that transfer number, which is how sequential minting, subscriptions, and pay-per-unit flows are enforced on-chain.

Shape

ts
export interface PredeterminedBalances<T extends NumberType> {
  manualBalances: ManualBalances<T>[];
  incrementedBalances: IncrementedBalances<T>;
  orderCalculationMethod: PredeterminedOrderCalculationMethod;
}

export interface PredeterminedOrderCalculationMethod {
  useOverallNumTransfers: boolean;
  usePerToAddressNumTransfers: boolean;
  usePerFromAddressNumTransfers: boolean;
  usePerInitiatedByAddressNumTransfers: boolean;
  useMerkleChallengeLeafIndex: boolean;
  challengeTrackerId: string;
}
FieldTypeDescription
manualBalances{ balances: Balance[] }[]One entry per transfer number. Exclusive with incrementedBalances.
incrementedBalancesIncrementedBalancesA start set plus rules for each subsequent transfer
orderCalculationMethodPredeterminedOrderCalculationMethodWhich counter gives the transfer number. Exactly one use* may be true.

Balances are never approximate. If the transfer's balances differ from the computed set, the approval does not match.

Manual Balances

A complete predeterminedBalances with manualBalances open:

json
{
  "predeterminedBalances": {
    "manualBalances": [
      {
        "balances": [
          {
            "amount": "1",
            "tokenIds": [
              { "start": "1", "end": "1" }
            ],
            "ownershipTimes": [
              { "start": "1691978400000", "end": "1723514400000" }
            ]
          }
        ]
      },
      {
        "balances": [
          {
            "amount": "5",
            "tokenIds": [
              { "start": "2", "end": "6" }
            ],
            "ownershipTimes": [
              { "start": "1691978400000", "end": "1723514400000" }
            ]
          }
        ]
      }
    ],
    "orderCalculationMethod": {
      "useOverallNumTransfers": true
    }
  }
}
{
  "predeterminedBalances": {
    "manualBalances": [
      {
        "balances": [
          {
            "amount": "1",
            "tokenIds": [
              { "start": "1", "end": "1" }
            ],
            "ownershipTimes": [
              { "start": "1691978400000", "end": "1723514400000" }
            ]
          }
        ]
      },
      {
        "balances": [
          {
            "amount": "5",
            "tokenIds": [
              { "start": "2", "end": "6" }
            ],
            "ownershipTimes": [
              { "start": "1691978400000", "end": "1723514400000" }
            ]
          }
        ]
      }
    ],
    "incrementedBalances": {
      "startBalances": [],
      "incrementTokenIdsBy": "0",
      "incrementOwnershipTimesBy": "0",
      "durationFromTimestamp": "0",
      "allowOverrideTimestamp": false,
      "recurringOwnershipTimes": {
        "startTime": "0",
        "intervalLength": "0",
        "chargePeriodLength": "0"
      },
      "allowOverrideWithAnyValidToken": false,
      "allowAmountScaling": false,
      "maxScalingMultiplier": "0"
    },
    "orderCalculationMethod": {
      "useOverallNumTransfers": true,
      "usePerToAddressNumTransfers": false,
      "usePerFromAddressNumTransfers": false,
      "usePerInitiatedByAddressNumTransfers": false,
      "useMerkleChallengeLeafIndex": false,
      "challengeTrackerId": ""
    }
  }
}

Transfer number 0 moves manualBalances[0], number 1 moves manualBalances[1], and so on. A number past the end matches nothing. incrementedBalances stays at its zero values when manualBalances is used.

Incremented Balances

A complete predeterminedBalances with incrementedBalances open:

json
{
  "predeterminedBalances": {
    "incrementedBalances": {
      "startBalances": [
        {
          "amount": "1",
          "tokenIds": [
            { "start": "1", "end": "1" }
          ],
          "ownershipTimes": [
            { "start": "1691978400000", "end": "1723514400000" }
          ]
        }
      ],
      "incrementTokenIdsBy": "1"
    },
    "orderCalculationMethod": {
      "useOverallNumTransfers": true
    }
  }
}
{
  "predeterminedBalances": {
    "manualBalances": [],
    "incrementedBalances": {
      "startBalances": [
        {
          "amount": "1",
          "tokenIds": [
            { "start": "1", "end": "1" }
          ],
          "ownershipTimes": [
            { "start": "1691978400000", "end": "1723514400000" }
          ]
        }
      ],
      "incrementTokenIdsBy": "1",
      "incrementOwnershipTimesBy": "0",
      "durationFromTimestamp": "0",
      "allowOverrideTimestamp": false,
      "recurringOwnershipTimes": {
        "startTime": "0",
        "intervalLength": "0",
        "chargePeriodLength": "0"
      },
      "allowOverrideWithAnyValidToken": false,
      "allowAmountScaling": false,
      "maxScalingMultiplier": "0"
    },
    "orderCalculationMethod": {
      "useOverallNumTransfers": true,
      "usePerToAddressNumTransfers": false,
      "usePerFromAddressNumTransfers": false,
      "usePerInitiatedByAddressNumTransfers": false,
      "useMerkleChallengeLeafIndex": false,
      "challengeTrackerId": ""
    }
  }
}
FieldDescriptionExample
startBalancesBalances for transfer number 0
incrementTokenIdsByAdd to every token ID per transfer number"1": transfer 1 gets ID 2, transfer 2 gets ID 3
incrementOwnershipTimesByAdd to every ownership time per transfer number"86400000": shift by one day each
durationFromTimestampReplace ownership times with [base, base + duration - 1]"2592000000": 30 days from now
allowOverrideTimestampLet the transfer supply base instead of block time
allowOverrideWithAnyValidTokenLet the transfer pick any single valid token ID
allowAmountScalingLet the transfer move any integer multiple of startBalances
maxScalingMultiplierCap on the multiplier. Required and greater than 0 when scaling is on."1000000000000" for micro-unit bases
recurringOwnershipTimesRepeating windows for subscriptionsmonthly with a 7-day charge window

Most options exclude each other. Transfer number 0 uses startBalances as-is; number N applies the increments N times.

Ask your agent:

text
Add a mint approval to collection 1 that hands out token IDs 1 to 100 in order, one token per transfer.

The MCP builder tools (add_approval, add_preset_approval) produce the objects on this page.

How It Works

Order Calculation

The transfer number comes from one counter:

MethodCounterTypical use
useOverallNumTransfersUses of the approval overallSequential mint: everyone gets the next ID
usePerToAddressNumTransfersUses per recipientEach recipient gets their own sequence
usePerFromAddressNumTransfersUses per sender
usePerInitiatedByAddressNumTransfersUses per initiator
useMerkleChallengeLeafIndexLeaf index from the Merkle challenge with challengeTrackerIdReserve ID N for leaf N (specific codes or addresses)

The use*NumTransfers methods read the same tracker as maxNumTransfers. The count is incremented even when maxNumTransfers sets 0 for that scope, and it is never reset by approval edits. Give an approval that should start at zero a fresh amountTrackerId.

json
{
  "orderCalculationMethod": {
    "useOverallNumTransfers": false,
    "usePerToAddressNumTransfers": false,
    "usePerFromAddressNumTransfers": false,
    "usePerInitiatedByAddressNumTransfers": false,
    "useMerkleChallengeLeafIndex": true,
    "challengeTrackerId": "uniqueId"
  }
}

Precalculation

Between signing and execution, other users' transfers can move the counter, so hand-computed balances go stale. Instead, ask the chain to compute them at execution time with precalculateBalancesFromApproval on the transfer:

ts
type TransferPrecalculation = {
  precalculateBalancesFromApproval: {
    approvalId: string;
    approvalLevel: 'collection' | 'incoming' | 'outgoing';
    approverAddress: string; // '' for collection
    version: string;         // must match the approval's version
    precalculationOptions: {
      overrideTimestamp: string;      // when allowOverrideTimestamp
      tokenIdsOverride: { start: string; end: string }[];  // when allowOverrideWithAnyValidToken
      scalingMultiplier: string;      // when allowAmountScaling
    }
  }
}
OptionApplies whenValidation
overrideTimestampdurationFromTimestamp set and allowOverrideTimestamp is true0 means block time. Ownership times become [t, t + duration - 1].
tokenIdsOverrideallowOverrideWithAnyValidToken is trueExactly one range with start == end, inside validTokenIds
scalingMultiplierallowAmountScaling is trueAt most maxScalingMultiplier. 0 means 1x.

Options whose flag is off are ignored without error.

json
{
  "precalculateBalancesFromApproval": {
    "approvalId": "approval-1",
    "approvalLevel": "collection",
    "approverAddress": "",
    "version": "1",
    "precalculationOptions": {
      "overrideTimestamp": "1704067200000",
      "tokenIdsOverride": [
        { "start": "5", "end": "5" }
      ],
      "scalingMultiplier": "0"
    }
  }
}

Duration from Timestamp

durationFromTimestamp overwrites every ownership time in startBalances with [base, base + duration - 1], where base is the block time or, when allowed, overrideTimestamp. Common durations in milliseconds:

Durationms
5 minutes300000
1 hour3600000
1 day86400000
1 week604800000
30 days2592000000
1 year31536000000
json
{
  "incrementedBalances": {
    "startBalances": [
      {
        "amount": "1",
        "tokenIds": [
          { "start": "1", "end": "1" }
        ],
        "ownershipTimes": [
          { "start": "1", "end": "18446744073709551615" }
        ]
      }
    ],
    "durationFromTimestamp": "2592000000",
    "allowOverrideTimestamp": true
  }
}
{
  "incrementedBalances": {
    "startBalances": [
      {
        "amount": "1",
        "tokenIds": [
          { "start": "1", "end": "1" }
        ],
        "ownershipTimes": [
          { "start": "1", "end": "18446744073709551615" }
        ]
      }
    ],
    "incrementTokenIdsBy": "0",
    "incrementOwnershipTimesBy": "0",
    "durationFromTimestamp": "2592000000",
    "allowOverrideTimestamp": true,
    "recurringOwnershipTimes": {
      "startTime": "0",
      "intervalLength": "0",
      "chargePeriodLength": "0"
    },
    "allowOverrideWithAnyValidToken": false,
    "allowAmountScaling": false,
    "maxScalingMultiplier": "0"
  }
}

Recurring Ownership Times

json
{
  "incrementedBalances": {
    "startBalances": [
      {
        "amount": "1",
        "tokenIds": [
          { "start": "1", "end": "1" }
        ],
        "ownershipTimes": [
          { "start": "1", "end": "18446744073709551615" }
        ]
      }
    ],
    "recurringOwnershipTimes": {
      "startTime": "1691978400000",
      "intervalLength": "2592000000",
      "chargePeriodLength": "604800000"
    }
  }
}
{
  "incrementedBalances": {
    "startBalances": [
      {
        "amount": "1",
        "tokenIds": [
          { "start": "1", "end": "1" }
        ],
        "ownershipTimes": [
          { "start": "1", "end": "18446744073709551615" }
        ]
      }
    ],
    "incrementTokenIdsBy": "0",
    "incrementOwnershipTimesBy": "0",
    "durationFromTimestamp": "0",
    "allowOverrideTimestamp": false,
    "recurringOwnershipTimes": {
      "startTime": "1691978400000",
      "intervalLength": "2592000000",
      "chargePeriodLength": "604800000"
    },
    "allowOverrideWithAnyValidToken": false,
    "allowAmountScaling": false,
    "maxScalingMultiplier": "0"
  }
}

Intervals of intervalLength begin at startTime. A transfer is accepted only inside the charge window, the chargePeriodLength milliseconds before the next interval starts, and it grants ownership for that whole next interval. Outside the window the approval fails with outside charge period. chargePeriodLength must be above 0 and at most intervalLength. The example is a monthly subscription from Aug 13, 2023 that can be paid up to 7 days in advance.

Amount Scaling

With allowAmountScaling, startBalances is the 1x unit and a transfer may move any integer multiple of it. coinTransfers on the same approval scale by the same multiplier.

json
{
  "predeterminedBalances": {
    "incrementedBalances": {
      "startBalances": [
        {
          "amount": "1",
          "tokenIds": [
            { "start": "1", "end": "1" }
          ],
          "ownershipTimes": [
            { "start": "1", "end": "18446744073709551615" }
          ]
        }
      ],
      "allowAmountScaling": true,
      "maxScalingMultiplier": "1000000000000"
    },
    "orderCalculationMethod": {
      "useOverallNumTransfers": true
    }
  }
}
{
  "predeterminedBalances": {
    "manualBalances": [],
    "incrementedBalances": {
      "startBalances": [
        {
          "amount": "1",
          "tokenIds": [
            { "start": "1", "end": "1" }
          ],
          "ownershipTimes": [
            { "start": "1", "end": "18446744073709551615" }
          ]
        }
      ],
      "incrementTokenIdsBy": "0",
      "incrementOwnershipTimesBy": "0",
      "durationFromTimestamp": "0",
      "allowOverrideTimestamp": false,
      "recurringOwnershipTimes": {
        "startTime": "0",
        "intervalLength": "0",
        "chargePeriodLength": "0"
      },
      "allowOverrideWithAnyValidToken": false,
      "allowAmountScaling": true,
      "maxScalingMultiplier": "1000000000000"
    },
    "orderCalculationMethod": {
      "useOverallNumTransfers": true,
      "usePerToAddressNumTransfers": false,
      "usePerFromAddressNumTransfers": false,
      "usePerInitiatedByAddressNumTransfers": false,
      "useMerkleChallengeLeafIndex": false,
      "challengeTrackerId": ""
    }
  }
}

Rules:

  • Every other incrementedBalances field must be zero, false, or empty. The base must be static.
  • maxScalingMultiplier must be greater than 0. The chain rejects unlimited scaling.
  • multiplier = transferAmount / baseAmount must be an integer of at least 1 and at most maxScalingMultiplier.
  • Precalculation accepts scalingMultiplier and returns the scaled balances. Or compute balances client-side.

Set startBalances to the smallest unit (amount: "1" of a micro-unit) with a large maxScalingMultiplier so users can buy any amount. Uses: pay-per-token (1 micro-token for 1 micro-unit of payment), prediction market deposits (1 micro-YES plus 1 micro-NO per micro-USDC, so 1 USDC is a 1000000x multiplier), credit purchases.

maxScalingMultiplier bounds one transfer, not the total. Pair scaling with maxNumTransfers or approvalAmounts to cap total exposure. When coinTransfers use overrideFromWithApproverAddress: true, the escrow pays multiplier * base per transfer, so bound it.

Amount scaling is incompatible with the Quest, Subscription, Invoice, Product, Bid/Listing, and Scheduled Payment standards, which require fixed amounts per transfer. The review_collection MCP tool flags allowAmountScaling combined with overrideFromWithApproverAddress.

Bounds

The approval's own tokenIds and ownershipTimes still apply. If the computed balances fall entirely outside them (transfer number 101 on an approval for IDs 1-100 with increment 1), the approval does not match. If they fall partly outside (IDs 95-105 against a bound of 1-100), this approval covers 95-100 and another approval must cover 101-105, or the transfer fails. The transfer must still move the exact computed set.

Approvals with predetermined balances are not auto-scannable. Transfers must prioritize them.

Edit this page on GitHub