Check Minting Limits
Overview
This guide reads the on-chain minting rate limits, replays the tumbling-window state off-chain, and pre-flights a proposed mint against three independent delay mechanisms:
- Hourly window —
directMintingHourlyLimitUBAvia theMintingRateLimiterlibrary - Daily window —
directMintingDailyLimitUBAvia the same library - Large-mint rule — amounts strictly above
directMintingLargeMintingThresholdUBAincur a fixeddirectMintingLargeMintingDelaySecondshold
All three throttle rather than reject: over-limit mints emit DirectMintingDelayed with an executionAllowedAt timestamp.
The binding delay is whichever rule pushes executionAllowedAt furthest into the future.
The complete runnable example is available in the flare-viem-starter repository.
How the windows work
MintingRateLimiter uses clock-aligned tumbling windows, not rolling windows.
On initialization, the window start is snapped to a multiple of windowSizeSeconds:
windowStartTimestamp = block.timestamp - block.timestamp % windowSizeSeconds
With windowSizeSeconds = 3600, the hourly window aligns to UTC hour boundaries (00:00-01:00, 01:00-02:00, …).
The daily window (86400 seconds) aligns to 00:00 UTC.
On every write, the limiter advances the window:
windowsElapsed = (now - windowStartTimestamp) / windowSizeSeconds
mintedInCurrentWindow = subOrZero(mintedInCurrentWindow, windowsElapsed * maxPerWindow)
windowStartTimestamp += windowsElapsed * windowSizeSeconds
Two consequences worth noting:
- Unused capacity does not roll over.
subOrZeroclamps at zero, so an idle hour does not give twice the cap the next hour — every fresh window starts at exactlymaxPerWindow. - Over-cap mints are delayed, not rejected.
executionAllowedAtis set towindowStartTimestamp + windowSize * mintedInCurrentWindow / maxPerWindow, so overflow drains hour-by-hour through thesubOrZerostep.
Reading the limiter state on its own returns stale (windowStart, minted) values until the next write touches the contract, so this script replays the slide off-chain to show the values as they would be right now.
Large minting delay
Large-mint delay is not part of MintingRateLimiter.
It applies when the proposed mint amount is strictly greater than directMintingLargeMintingThresholdUBA:
if amount > directMintingLargeMintingThresholdUBA:
executionAllowedAt_large = now + directMintingLargeMintingDelaySeconds
This rule runs even when the hourly and daily windows have headroom.
unblockDirectMintingsUntil bypasses the hourly and daily limiter only — it does not skip large-mint delay.
On Flare mainnet, the threshold and delay are currently 4M XRP and 2 hours respectively (see Operational Parameters).
Prerequisites
- The flare-viem-starter cloned locally with dependencies installed.
- A configured
publicClientpointing at the Flare network you want to query (Coston2 by default in the starter).
Minting Limits Script
import { dropsToXrp, xrpToDrops } from "xrpl";
import { getContractAddressByName } from "./utils/flare-contract-registry";
import {
getAssetMintingGranularityUBA,
getDirectMintingDailyLimitUBA,
getDirectMintingDailyLimiterState,
getDirectMintingHourlyLimitUBA,
getDirectMintingHourlyLimiterState,
getDirectMintingLargeMintingDelaySeconds,
getDirectMintingLargeMintingThresholdUBA,
getDirectMintingsUnblockUntilTimestamp,
} from "./utils/fassets";
// 1. Window sizes are clock-aligned tumbling, not rolling. Hourly snaps to
// UTC hour boundaries; daily snaps to 00:00 UTC.
const HOURLY_WINDOW_SECONDS = 3600n;
const DAILY_WINDOW_SECONDS = 86400n;
// Example mint to pre-flight (net FXRP, before fees). Override with MINT_PREFLIGHT_XRP.
const DEFAULT_PREFLIGHT_MINT_XRP = 10;
// 2. Format helpers.
function formatUba(uba: bigint): string {
return `${uba.toString()} UBA (${dropsToXrp(uba.toString())} XRP)`;
}
function formatTimestamp(secondsSinceEpoch: bigint, now: bigint): string {
const iso = new Date(Number(secondsSinceEpoch) * 1000).toISOString();
const delta = Number(secondsSinceEpoch - now);
const relative = delta >= 0 ? `in ${delta}s` : `${-delta}s ago`;
return `${iso} (${relative})`;
}
function bigintMin(a: bigint, b: bigint): bigint {
return a < b ? a : b;
}
function bigintMax(a: bigint, b: bigint): bigint {
return a > b ? a : b;
}
// 3. Replay the limiter slide off-chain. Reading state alone returns stale
// `(windowStart, minted)` values until the next write touches the limiter,
// so we re-anchor the window and drain `mintedInCurrentWindow` using the
// same window-advancement logic as MintingRateLimiter.sol.
function computeWindowState({
now,
windowStartTimestamp,
mintedInCurrentWindowUBA,
limitUBA,
windowSizeSeconds,
}: {
now: bigint;
windowStartTimestamp: bigint;
mintedInCurrentWindowUBA: bigint;
limitUBA: bigint;
windowSizeSeconds: bigint;
}) {
let effectiveStart = windowStartTimestamp;
let usedUBA = mintedInCurrentWindowUBA;
if (
windowStartTimestamp > 0n &&
now >= windowStartTimestamp + windowSizeSeconds
) {
const windowsElapsed = (now - windowStartTimestamp) / windowSizeSeconds;
effectiveStart = windowStartTimestamp + windowsElapsed * windowSizeSeconds;
const drained = windowsElapsed * limitUBA;
usedUBA = drained >= usedUBA ? 0n : usedUBA - drained;
}
const remainingUBA = limitUBA > usedUBA ? limitUBA - usedUBA : 0n;
const nextResetAt = effectiveStart + windowSizeSeconds;
return { effectiveStart, usedUBA, remainingUBA, nextResetAt };
}
// 4. Window delay mirrors MintingRateLimiter: overflow is scheduled into the
// current tumbling window proportionally to accumulated minted volume.
function computeWindowExecutionAllowedAt({
now,
effectiveStart,
usedUBA,
proposedAmountUBA,
limitUBA,
windowSizeSeconds,
limiterDisabled,
}: {
now: bigint;
effectiveStart: bigint;
usedUBA: bigint;
proposedAmountUBA: bigint;
limitUBA: bigint;
windowSizeSeconds: bigint;
limiterDisabled: boolean;
}): bigint {
if (limiterDisabled || limitUBA === 0n || proposedAmountUBA === 0n) {
return now;
}
const mintedAfter = usedUBA + proposedAmountUBA;
if (mintedAfter <= limitUBA) {
return now;
}
return effectiveStart + (windowSizeSeconds * mintedAfter) / limitUBA;
}
// 5. Large-mint delay is independent of the hourly/daily windows. Amounts
// strictly above the threshold are held for a fixed duration from `now`.
function computeLargeMintExecutionAllowedAt({
now,
proposedAmountUBA,
largeThresholdUBA,
largeDelaySeconds,
}: {
now: bigint;
proposedAmountUBA: bigint;
largeThresholdUBA: bigint;
largeDelaySeconds: bigint;
}): bigint {
if (proposedAmountUBA > largeThresholdUBA) {
return now + largeDelaySeconds;
}
return now;
}
function computeDirectMintingExecutionAllowedAt({
now,
proposedAmountUBA,
hourly,
daily,
largeThresholdUBA,
largeDelaySeconds,
limiterDisabled,
}: {
now: bigint;
proposedAmountUBA: bigint;
hourly: {
effectiveStart: bigint;
usedUBA: bigint;
limitUBA: bigint;
windowSizeSeconds: bigint;
};
daily: {
effectiveStart: bigint;
usedUBA: bigint;
limitUBA: bigint;
windowSizeSeconds: bigint;
};
largeThresholdUBA: bigint;
largeDelaySeconds: bigint;
limiterDisabled: boolean;
}) {
const hourlyAt = computeWindowExecutionAllowedAt({
now,
effectiveStart: hourly.effectiveStart,
usedUBA: hourly.usedUBA,
proposedAmountUBA,
limitUBA: hourly.limitUBA,
windowSizeSeconds: hourly.windowSizeSeconds,
limiterDisabled,
});
const dailyAt = computeWindowExecutionAllowedAt({
now,
effectiveStart: daily.effectiveStart,
usedUBA: daily.usedUBA,
proposedAmountUBA,
limitUBA: daily.limitUBA,
windowSizeSeconds: daily.windowSizeSeconds,
limiterDisabled,
});
const largeAt = computeLargeMintExecutionAllowedAt({
now,
proposedAmountUBA,
largeThresholdUBA,
largeDelaySeconds,
});
const executionAllowedAt = bigintMax(hourlyAt, bigintMax(dailyAt, largeAt));
const delayed = executionAllowedAt > now;
const delayReasons: string[] = [];
if (hourlyAt === executionAllowedAt && hourlyAt > now) {
delayReasons.push("hourly window");
}
if (dailyAt === executionAllowedAt && dailyAt > now) {
delayReasons.push("daily window");
}
if (largeAt === executionAllowedAt && largeAt > now) {
delayReasons.push("large-mint threshold");
}
return {
executionAllowedAt,
delayed,
delayReasons,
hourlyAt,
dailyAt,
largeAt,
};
}
// 6. Print one window.
function printWindow(
label: string,
opts: {
limitUBA: bigint;
usedUBA: bigint;
remainingUBA: bigint;
effectiveStart: bigint;
nextResetAt: bigint;
now: bigint;
},
) {
const { limitUBA, usedUBA, remainingUBA, effectiveStart, nextResetAt, now } =
opts;
const usedPct =
limitUBA === 0n ? 0 : Number((usedUBA * 10000n) / limitUBA) / 100;
const row = (key: string, value: string) =>
console.log(`${key.padEnd(17)} ${value}`);
console.log(`=== ${label} ===`);
row("Limit:", formatUba(limitUBA));
row("Used:", `${formatUba(usedUBA)} (${usedPct.toFixed(2)}%)`);
row("Remaining:", formatUba(remainingUBA));
row("Window started:", formatTimestamp(effectiveStart, now));
row("Window resets at:", formatTimestamp(nextResetAt, now));
console.log();
}
async function main() {
// 7. Resolve `AssetManagerFXRP` through the Flare Contract Registry.
const assetManagerAddress =
await getContractAddressByName("AssetManagerFXRP");
console.log("AssetManagerFXRP address:", assetManagerAddress, "\n");
// 8. Read AMG granularity, hourly and daily caps, raw limiter state, the
// unblock flag, and the large-minting threshold and delay in parallel.
const [
amgGranularityUBA,
hourlyLimitUBA,
dailyLimitUBA,
hourlyState,
dailyState,
unblockUntilTimestamp,
largeThresholdUBA,
largeDelaySeconds,
] = await Promise.all([
getAssetMintingGranularityUBA(assetManagerAddress),
getDirectMintingHourlyLimitUBA(assetManagerAddress),
getDirectMintingDailyLimitUBA(assetManagerAddress),
getDirectMintingHourlyLimiterState(assetManagerAddress),
getDirectMintingDailyLimiterState(assetManagerAddress),
getDirectMintingsUnblockUntilTimestamp(assetManagerAddress),
getDirectMintingLargeMintingThresholdUBA(assetManagerAddress),
getDirectMintingLargeMintingDelaySeconds(assetManagerAddress),
]);
const now = BigInt(Math.floor(Date.now() / 1000));
const limiterDisabled = unblockUntilTimestamp > now;
// 9. Limiter state is returned as raw AMG (uint64); convert to UBA via
// `assetMintingGranularityUBA` before passing into the window math.
const computeAndPrint = (
label: string,
limitUBA: bigint,
state: readonly [bigint, bigint],
sizeSeconds: bigint,
) => {
const [windowStart, mintedAmg] = state;
const result = computeWindowState({
now,
windowStartTimestamp: windowStart,
mintedInCurrentWindowUBA: mintedAmg * amgGranularityUBA,
limitUBA,
windowSizeSeconds: sizeSeconds,
});
printWindow(label, { ...result, limitUBA, now });
return result;
};
const hourly = computeAndPrint(
"Hourly window",
hourlyLimitUBA,
hourlyState,
HOURLY_WINDOW_SECONDS,
);
const daily = computeAndPrint(
"Daily window",
dailyLimitUBA,
dailyState,
DAILY_WINDOW_SECONDS,
);
// 10. Honor the unblock flag. While `directMintingsUnblockUntilTimestamp`
// is in the future, governance has temporarily disabled the hourly/daily
// limiter. Large-mint delay still applies.
console.log("=== Other flags ===");
if (limiterDisabled) {
console.log(
"Hourly/daily limiter DISABLED until:",
formatTimestamp(unblockUntilTimestamp, now),
"— window caps are not enforced right now.",
);
console.log(
"Large-mint delay is still enforced above",
formatUba(largeThresholdUBA) + ".",
);
} else {
console.log(
"Hourly/daily limiter active (unblockUntilTimestamp =",
unblockUntilTimestamp.toString() + ")",
);
}
console.log("Large minting threshold:", formatUba(largeThresholdUBA));
console.log("Large minting delay: ", `${largeDelaySeconds.toString()}s`);
console.log();
// 11. Pre-flight gate: largest single mint with no delay from any mechanism.
const hourlyHeadroomUBA = limiterDisabled
? hourlyLimitUBA
: hourly.remainingUBA;
const dailyHeadroomUBA = limiterDisabled ? dailyLimitUBA : daily.remainingUBA;
const safeRemainingUBA = bigintMin(
bigintMin(hourlyHeadroomUBA, dailyHeadroomUBA),
largeThresholdUBA,
);
console.log(
"Maximum single mint with no delay (hourly, daily, and large-mint):",
formatUba(safeRemainingUBA),
);
console.log();
// 12. Pre-flight a concrete mint amount, including large-mint delay.
const preflightMintXrp = Number(
process.env.MINT_PREFLIGHT_XRP ?? DEFAULT_PREFLIGHT_MINT_XRP,
);
const proposedAmountUBA = BigInt(xrpToDrops(preflightMintXrp.toString()));
const preflight = computeDirectMintingExecutionAllowedAt({
now,
proposedAmountUBA,
hourly: {
effectiveStart: hourly.effectiveStart,
usedUBA: hourly.usedUBA,
limitUBA: hourlyLimitUBA,
windowSizeSeconds: HOURLY_WINDOW_SECONDS,
},
daily: {
effectiveStart: daily.effectiveStart,
usedUBA: daily.usedUBA,
limitUBA: dailyLimitUBA,
windowSizeSeconds: DAILY_WINDOW_SECONDS,
},
largeThresholdUBA,
largeDelaySeconds,
limiterDisabled,
});
console.log(`=== Pre-flight: ${preflightMintXrp} XRP mint ===`);
if (!preflight.delayed) {
console.log("Result: executes immediately (no rate-limit delay).");
} else {
console.log(
"Result: would emit DirectMintingDelayed with executionAllowedAt",
formatTimestamp(preflight.executionAllowedAt, now),
);
console.log(
"Binding delay:",
preflight.delayReasons.length > 0
? preflight.delayReasons.join(", ")
: "unknown",
);
console.log(
"Hourly window would allow at:",
formatTimestamp(preflight.hourlyAt, now),
);
console.log(
"Daily window would allow at:",
formatTimestamp(preflight.dailyAt, now),
);
console.log(
"Large-mint rule would allow at:",
formatTimestamp(preflight.largeAt, now),
);
console.log(
"The executor must wait, then call executeDirectMinting again with the same FDC proof.",
);
}
console.log();
}
void main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Code Breakdown
- Window sizes.
HOURLY_WINDOW_SECONDSandDAILY_WINDOW_SECONDSare thewindowSizeSecondsvalues theMintingRateLimiter.sollibrary uses —3600aligns to UTC hour boundaries,86400aligns to 00:00 UTC. - Format helpers.
formatUbaprints raw UBA (units of basis assets) alongside the XRP equivalent viadropsToXrp, andformatTimestampshows ISO time with a relative offset so it is clear whether a timestamp is in the future or the past. - Replay the limiter slide.
computeWindowStatemirrors the window-advancement logic inMintingRateLimiter.sol: it advanceswindowStartpast every tumble that has elapsed and drainsmintedInCurrentWindowbywindowsElapsed * limit. Without this off-chain replay, you would see stale numbers between writes. - Window delay math.
computeWindowExecutionAllowedAtapplies the proportional overflow formula fromMintingRateLimiter: whenused + proposedexceeds the cap,executionAllowedAt = effectiveStart + windowSize * (used + proposed) / limit. - Large-mint delay math.
computeLargeMintExecutionAllowedAtreturnsnow + directMintingLargeMintingDelaySecondswhenproposed > threshold, otherwisenow. - Combine all three.
computeDirectMintingExecutionAllowedAttakes the maximum of the hourly, daily, and large-mint timestamps and reports which rule is binding. - Print one window's live state.
printWindowshows the live cap, used amount, and percentage, remaining headroom, the effective window start, and the next UTC tumble. - Resolve
AssetManagerFXRP.getContractAddressByName("AssetManagerFXRP")looks up the asset manager through the Flare Contract Registry. - Read everything in parallel.
Promise.allfetches the AMG granularity, the hourly and daily caps, the raw limiter state for hourly and daily windows, theunblockUntilTimestamp, and the large-minting threshold and delay in one round trip. - Convert AMG to UBA.
The limiter stores
mintedInCurrentWindowin AMG (uint64) for cheap on-chain storage. Multiplying byassetMintingGranularityUBArebases it into UBA (units of basis assets) so it can be compared against the UBA-denominated cap. - Honor the unblock flag.
When
getDirectMintingsUnblockUntilTimestampreturns a future timestamp, governance has temporarily disabled the hourly/daily limiter. Large-mint delay still applies. - Safe maximum without delay.
bigintMin(hourlyHeadroom, dailyHeadroom, largeThresholdUBA)is the largest amount that avoids delay from any mechanism. Minting exactly at the threshold is allowed; strictly above it triggers the large-mint hold. - Pre-flight a concrete amount.
Set
MINT_PREFLIGHT_XRP(defaults to10) to see whether that mint would execute immediately or emitDirectMintingDelayed. A delayed mint does not revert — the executor retries with the same FDC proof afterexecutionAllowedAt.
Important Notes
- Large mintings have an independent fixed delay.
Mintings above
directMintingLargeMintingThresholdUBAare delayed bydirectMintingLargeMintingDelaySecondseven if the hourly and daily windows have headroom. - Governance unblock is window-only.
unblockDirectMintingsUntildrains hourly/daily backlogs; it does not bypass the large-mint rule. - Reads are stale between writes. Always advance the window off-chain when displaying live limiter state.
- Plan UI flows around
DirectMintingDelayedso users are not surprised by a deferred execution. - Delay and cap failures — see Delays in the Minting Troubleshooting guide for retry steps when rate limits bind.
To continue your FAssets development journey, you can:
- Mint with the 32-byte memo flow in Mint FXRP.
- Mint with a reserved tag in Mint FXRP with Tag.
- Read the protocol details in Minting.