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 every mechanism that can hold it back:
- Global minting cap —
mintingCapAMGis checked and reserved before the rate limits, and it reverts withMintingCapExceeded - Hourly window —
directMintingHourlyLimitUBAvia theMintingRateLimiterlibrary - Daily window —
directMintingDailyLimitUBAvia the same library - Large-mint rule — amounts at or above
directMintingLargeMintingThresholdUBAincur a fixeddirectMintingLargeMintingDelaySecondshold
Only the minting cap rejects; the three throttles delay.
Over-limit mints emit DirectMintingDelayed (windows) or LargeDirectMintingDelayed (large mints) with an executionAllowedAt timestamp.
The large-mint rule and the windows are mutually exclusive: a large minting takes its own branch, so it is never also throttled by the hourly or daily window — and it consumes no window capacity.
Within the window branch, the binding delay is whichever of the two windows pushes executionAllowedAt further into the future.
A delayed minting keeps its reserved capacity, and the retry re-enters executeDirectMinting with the same FDC proof: the cap and the limiters are not re-evaluated, so it mints as soon as executionAllowedAt has passed.
Calling before that reverts with DirectMintingStillDelayed.
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
Three 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. - The cap boundary is inclusive.
A minting executes immediately only while
mintedInCurrentWindow < maxMintingPerWindow, so a mint that lands exactly on the cap is already delayed. The largest amount that still executes immediately is one AMG below the remaining headroom.
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 greater than or equal to directMintingLargeMintingThresholdUBA:
if amountAmg >= directMintingLargeMintingThresholdAmg:
executionAllowedAt = now + directMintingLargeMintingDelaySeconds
else:
hourly and daily limiters run
Because this is an if/else, a large minting bypasses the hourly and daily limiter completely: it is never delayed by a window, and the amount is not recorded against either window.
A large minting is always delayed — even with a zero delay it is registered as a delayed minting and needs a second executeDirectMinting call with the same FDC proof.
On Flare mainnet, the threshold and delay are currently 4M XRP and 2 hours respectively (see Operational Parameters).
Global minting cap
Before any rate limit runs, executeDirectMinting reserves minting capacity against the FAsset-wide cap:
totalAMG = totalReservedCollateralAMG + toAmg(fAsset.totalSupply())
require(totalAMG + amountAmg <= mintingCapAMG) // else MintingCapExceeded
This one reverts — there is no delay and no event.
The cap is disabled when mintingCapAMG is 0.
The totalReservedCollateralAMG (collateral reservations plus already-delayed direct mintings, whose capacity stays reserved while they wait) has no getter, so an off-chain headroom of mintingCap - totalSupply is an upper bound.
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 {
getAssetManagerSettings,
getDirectMintingDailyLimitUBA,
getDirectMintingDailyLimiterState,
getDirectMintingHourlyLimitUBA,
getDirectMintingHourlyLimiterState,
getDirectMintingLargeMintingDelaySeconds,
getDirectMintingLargeMintingThresholdUBA,
getDirectMintingsUnblockUntilTimestamp,
getFAssetTotalSupply,
getLatestBlock,
} from "./utils/fassets";
// 1. Window sizes are clock-aligned tumbling, not rolling. Hourly snaps to
// UTC hour boundaries; daily snaps to 00:00 UTC. Both are fixed at asset
// manager initialization (`1 hours` / `1 days`) and have no getter.
const HOURLY_WINDOW_SECONDS = 3600n;
const DAILY_WINDOW_SECONDS = 86400n;
// Underlying XRP amount to pre-flight: what the payment delivers to the Core
// Vault, before minting and executor 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 formatDuration(seconds: number): string {
const total = Math.abs(seconds);
const parts = [
{ unit: "d", value: Math.floor(total / 86400) },
{ unit: "h", value: Math.floor((total % 86400) / 3600) },
{ unit: "m", value: Math.floor((total % 3600) / 60) },
{ unit: "s", value: total % 60 },
];
// Drop leading zero units, then keep everything through seconds so interior
// zeros survive (7200s is "2h 0m 0s", not "2h 0s") and "0s" stays readable.
const firstNonZero = parts.findIndex((part) => part.value > 0);
const kept =
firstNonZero === -1 ? parts.slice(-1) : parts.slice(firstNonZero);
return kept.map((part) => `${part.value}${part.unit}`).join(" ");
}
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 ${formatDuration(delta)}` : `${formatDuration(delta)} 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;
}
function subOrZero(a: bigint, b: bigint): bigint {
return a > b ? a - b : 0n;
}
// 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,
amgGranularityUBA,
}: {
now: bigint;
windowStartTimestamp: bigint;
mintedInCurrentWindowUBA: bigint;
limitUBA: bigint;
windowSizeSeconds: bigint;
amgGranularityUBA: bigint;
}) {
let effectiveStart = windowStartTimestamp;
let usedUBA = mintedInCurrentWindowUBA;
if (
windowStartTimestamp > 0n &&
now >= windowStartTimestamp + windowSizeSeconds
) {
const windowsElapsed = (now - windowStartTimestamp) / windowSizeSeconds;
effectiveStart = windowStartTimestamp + windowsElapsed * windowSizeSeconds;
usedUBA = subOrZero(usedUBA, windowsElapsed * limitUBA);
}
const remainingUBA = subOrZero(limitUBA, usedUBA);
// `recordMinting` delays as soon as `minted >= maxMintingPerWindow`, so the
// largest mint that still executes immediately is one AMG below the headroom.
const maxWithoutDelayUBA = subOrZero(remainingUBA, amgGranularityUBA);
const nextResetAt = effectiveStart + windowSizeSeconds;
return {
effectiveStart,
usedUBA,
remainingUBA,
maxWithoutDelayUBA,
nextResetAt,
};
}
// 4. Window delay mirrors MintingRateLimiter: overflow is scheduled into the
// current tumbling window proportionally to accumulated minted volume.
// Note the on-chain comparison is `minted < limit`, so landing exactly on
// the cap is already delayed.
function computeWindowExecutionAllowedAt({
now,
effectiveStart,
usedUBA,
proposedAmountUBA,
limitUBA,
windowSizeSeconds,
}: {
now: bigint;
effectiveStart: bigint;
usedUBA: bigint;
proposedAmountUBA: bigint;
limitUBA: bigint;
windowSizeSeconds: bigint;
}): bigint {
// A zero cap is not a valid deployed configuration (`recordMinting` would
// divide by zero); treat it as "no window throttle" for display purposes.
if (limitUBA === 0n) {
return now;
}
const mintedAfter = usedUBA + proposedAmountUBA;
if (mintedAfter < limitUBA) {
return now;
}
return effectiveStart + (windowSizeSeconds * mintedAfter) / limitUBA;
}
// 5. Large mintings take a separate branch in `_checkRateLimits`: an amount at
// or above the threshold is delayed by a fixed duration and never touches
// the hourly/daily limiter (so it also consumes no window capacity).
// The comparison is `>=`, so minting exactly at the threshold is delayed.
function isLargeMinting(
proposedAmountUBA: bigint,
largeThresholdUBA: bigint,
): boolean {
return proposedAmountUBA >= largeThresholdUBA;
}
// 6. Combine the two branches the way `_checkRateLimits` does: the large
// branch on its own, otherwise the later of the hourly and daily windows.
function computeDirectMintingExecutionAllowedAt({
now,
proposedAmountUBA,
hourly,
daily,
largeThresholdUBA,
largeDelaySeconds,
}: {
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;
}) {
// Large branch: the hourly and daily windows are not consulted at all.
if (isLargeMinting(proposedAmountUBA, largeThresholdUBA)) {
return {
executionAllowedAt: now + largeDelaySeconds,
// The contract records the delayed minting and returns before minting,
// so a large minting always needs a second `executeDirectMinting` call.
delayed: true,
delayReason: "large-mint threshold",
event: "LargeDirectMintingDelayed",
hourlyAt: undefined,
dailyAt: undefined,
};
}
const hourlyAt = computeWindowExecutionAllowedAt({
now,
effectiveStart: hourly.effectiveStart,
usedUBA: hourly.usedUBA,
proposedAmountUBA,
limitUBA: hourly.limitUBA,
windowSizeSeconds: hourly.windowSizeSeconds,
});
const dailyAt = computeWindowExecutionAllowedAt({
now,
effectiveStart: daily.effectiveStart,
usedUBA: daily.usedUBA,
proposedAmountUBA,
limitUBA: daily.limitUBA,
windowSizeSeconds: daily.windowSizeSeconds,
});
const executionAllowedAt = bigintMax(hourlyAt, dailyAt);
const delayReasons: string[] = [];
if (hourlyAt > now) delayReasons.push("hourly window");
if (dailyAt > now) delayReasons.push("daily window");
return {
executionAllowedAt,
delayed: executionAllowedAt > now,
delayReason: delayReasons.join(" + "),
event: "DirectMintingDelayed",
hourlyAt,
dailyAt,
};
}
// 7. Print one window.
function printWindow(
label: string,
opts: {
limitUBA: bigint;
usedUBA: bigint;
remainingUBA: bigint;
maxWithoutDelayUBA: bigint;
effectiveStart: bigint;
nextResetAt: bigint;
now: bigint;
},
) {
const {
limitUBA,
usedUBA,
remainingUBA,
maxWithoutDelayUBA,
effectiveStart,
nextResetAt,
now,
} = opts;
const usedPct =
limitUBA === 0n ? 0 : Number((usedUBA * 10000n) / limitUBA) / 100;
const row = (key: string, value: string) =>
console.log(`${key.padEnd(21)} ${value}`);
console.log(`=== ${label} ===`);
row("Limit:", formatUba(limitUBA));
row("Used:", `${formatUba(usedUBA)} (${usedPct.toFixed(2)}%)`);
row("Remaining:", formatUba(remainingUBA));
row("Max without delay:", formatUba(maxWithoutDelayUBA));
row("Window started:", formatTimestamp(effectiveStart, now));
row("Window resets at:", formatTimestamp(nextResetAt, now));
console.log();
}
async function main() {
// 8. Resolve `AssetManagerFXRP` through the Flare Contract Registry.
const assetManagerAddress =
await getContractAddressByName("AssetManagerFXRP");
console.log("AssetManagerFXRP address:", assetManagerAddress, "\n");
// 9. Pin every read to one block and take `now` from that block. Using the
// local clock would drift from `block.timestamp`, which is what the
// contract compares the window timestamps against.
const block = await getLatestBlock();
const blockNumber = block.number;
const now = block.timestamp;
// 10. Read the settings (AMG granularity, FAsset address, global minting cap),
// the hourly and daily caps, the raw limiter state, the unblock timestamp,
// and the large-minting threshold and delay in parallel.
const [
settings,
hourlyLimitUBA,
dailyLimitUBA,
hourlyState,
dailyState,
unblockUntilTimestamp,
largeThresholdUBA,
largeDelaySeconds,
] = await Promise.all([
getAssetManagerSettings(assetManagerAddress, blockNumber),
getDirectMintingHourlyLimitUBA(assetManagerAddress, blockNumber),
getDirectMintingDailyLimitUBA(assetManagerAddress, blockNumber),
getDirectMintingHourlyLimiterState(assetManagerAddress, blockNumber),
getDirectMintingDailyLimiterState(assetManagerAddress, blockNumber),
getDirectMintingsUnblockUntilTimestamp(assetManagerAddress, blockNumber),
getDirectMintingLargeMintingThresholdUBA(assetManagerAddress, blockNumber),
getDirectMintingLargeMintingDelaySeconds(assetManagerAddress, blockNumber),
]);
const amgGranularityUBA = BigInt(settings.assetMintingGranularityUBA);
// 11. 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,
amgGranularityUBA,
});
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,
);
// 12. The global minting cap is checked *before* the rate limits and it
// reverts with `MintingCapExceeded` instead of delaying. Reserved
// capacity (`totalReservedCollateralAMG`, which covers collateral
// reservations and already-delayed direct mintings) has no getter, so
// the headroom computed here is an upper bound.
const mintingCapAMG = BigInt(settings.mintingCapAMG);
const mintingCapEnabled = mintingCapAMG !== 0n;
const mintingCapUBA = mintingCapAMG * amgGranularityUBA;
const totalSupplyUBA = mintingCapEnabled
? await getFAssetTotalSupply(settings.fAsset, blockNumber)
: 0n;
// `checkMintingCap` compares AMG, and `convertUBAToAmg` floors, so floor the
// supply the same way instead of subtracting raw UBA.
const totalSupplyAMG = totalSupplyUBA / amgGranularityUBA;
const capHeadroomUBA =
subOrZero(mintingCapAMG, totalSupplyAMG) * amgGranularityUBA;
console.log("=== Global minting cap ===");
if (mintingCapEnabled) {
console.log("Minting cap: ", formatUba(mintingCapUBA));
console.log("Total supply: ", formatUba(totalSupplyUBA));
console.log("Headroom (max):", formatUba(capHeadroomUBA));
console.log(
"Above the headroom executeDirectMinting reverts with MintingCapExceeded (no delay).",
);
} else {
console.log("Minting cap disabled (mintingCapAMG = 0).");
}
console.log();
// 13. `unblockDirectMintingsUntil` only accepts timestamps in the past: it
// retroactively releases mintings that were already delayed and started
// before that timestamp - including large-minting delays. It does not
// exempt new mintings from the hourly/daily limiter, so it plays no part
// in the pre-flight below.
console.log("=== Other flags ===");
if (unblockUntilTimestamp > 0n) {
console.log(
"Delayed mintings started before",
formatTimestamp(unblockUntilTimestamp, now),
"have been released by governance.",
);
} else {
console.log("No governance unblock recorded (unblockUntilTimestamp = 0).");
}
console.log(
"Large minting threshold:",
formatUba(largeThresholdUBA),
"(inclusive)",
);
console.log(
"Large minting delay: ",
formatDuration(Number(largeDelaySeconds)),
);
console.log();
// 14. Pre-flight gate: largest single mint that is neither delayed nor
// rejected - one AMG below the large-mint threshold, one AMG below each
// window cap, and within the global cap headroom.
const largestNonLargeUBA = subOrZero(largeThresholdUBA, amgGranularityUBA);
let safeRemainingUBA = bigintMin(
bigintMin(hourly.maxWithoutDelayUBA, daily.maxWithoutDelayUBA),
largestNonLargeUBA,
);
if (mintingCapEnabled) {
safeRemainingUBA = bigintMin(safeRemainingUBA, capHeadroomUBA);
}
console.log(
"Maximum single mint with no delay and no revert:",
formatUba(safeRemainingUBA),
);
console.log();
// 15. Pre-flight a concrete mint amount.
const preflightMintXrp = Number(
process.env.MINT_PREFLIGHT_XRP ?? DEFAULT_PREFLIGHT_MINT_XRP,
);
// The contract floors the underlying amount to whole AMG before comparing it
// against the thresholds, so mirror that here.
const receivedAmountUBA = BigInt(xrpToDrops(preflightMintXrp.toString()));
const proposedAmountUBA =
(receivedAmountUBA / amgGranularityUBA) * amgGranularityUBA;
console.log(`=== Pre-flight: ${preflightMintXrp} XRP mint ===`);
if (mintingCapEnabled && proposedAmountUBA > capHeadroomUBA) {
console.log(
"Result: would revert with MintingCapExceeded - the global minting cap has no headroom.",
);
console.log(
"The minting cap is checked before the rate limits, so no delay event is emitted.",
);
console.log();
return;
}
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,
});
if (!preflight.delayed) {
console.log("Result: executes immediately (no rate-limit delay).");
} else {
console.log(
`Result: would emit ${preflight.event} with executionAllowedAt`,
formatTimestamp(preflight.executionAllowedAt, now),
);
console.log("Binding delay:", preflight.delayReason);
if (preflight.hourlyAt !== undefined && preflight.dailyAt !== undefined) {
console.log(
"Hourly window would allow at:",
formatTimestamp(preflight.hourlyAt, now),
);
console.log(
"Daily window would allow at: ",
formatTimestamp(preflight.dailyAt, now),
);
} else {
console.log(
"Large mintings skip the hourly/daily limiter entirely and consume no window capacity.",
);
}
console.log(
"Execution needs a second executeDirectMinting call with the same FDC proof, at or after executionAllowedAt.",
);
}
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. Both are fixed when the asset manager is initialized (1 hoursand1 days) and there is no getter for them. - Format helpers.
formatUbaprints raw UBA (units of basis assets) alongside the XRP equivalent viadropsToXrp.formatDurationturns a number of seconds into1d 1h 1m 1s, keeping interior zeros so a two-hour delay reads2h 0m 0s, andformatTimestamppairs ISO time with that duration as a relative offset (in …/… ago) 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. It reports both the headroom to the cap andmaxWithoutDelayUBA, which is one AMG lower because the on-chain check isminted < limit. - Window delay math.
computeWindowExecutionAllowedAtapplies the proportional overflow formula fromMintingRateLimiter: whenused + proposedreaches the cap,executionAllowedAt = effectiveStart + windowSize * (used + proposed) / limit. - Large-mint branch.
isLargeMintinguses the same inclusive>=comparison as the contract. - Combine the mechanisms the way the contract does.
computeDirectMintingExecutionAllowedAtreturns the large-mint timestamp on its own when the amount is large, and otherwise takes the maximum of the hourly and daily timestamps. It also reports which event the contract would emit. - Print one window's live state.
printWindowshows the live cap, used amount and percentage, remaining headroom, the largest non-delayed mint, the effective window start, and the next UTC tumble. - Resolve
AssetManagerFXRP.getContractAddressByName("AssetManagerFXRP")looks up the asset manager through the Flare Contract Registry. - Pin the snapshot to one block.
getLatestBlockprovides the block number every read is pinned to, and itstimestampis used asnow. The limiter compares againstblock.timestamp, so replaying the windows against the local clock would drift, and unpinned reads could straddle blocks. - Read everything in parallel.
Promise.allfetches the asset manager settings (AMG granularity, FAsset address, minting cap), 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. - Global minting cap headroom.
checkMintingCapworks in AMG, so the supply is floored to whole AMG the wayConversion.convertUBAToAmgdoes before subtracting it frommintingCapAMG. The result is still an upper bound, because reserved capacity is not readable off-chain; beyond it the mint reverts instead of being delayed. - Report the governance unblock.
getDirectMintingsUnblockUntilTimestampis always a past timestamp: governance uses it to release mintings that were already delayed and started before it, not to switch the limiter off. New mintings are still rate-limited, so it does not enter the pre-flight math. - Safe maximum.
The largest amount that neither delays nor reverts is the minimum of both windows'
maxWithoutDelayUBA, one AMG below the large-mint threshold, and the cap headroom. - Pre-flight a concrete amount.
Set
MINT_PREFLIGHT_XRP(defaults to10) to see whether that mint executes immediately, reverts withMintingCapExceeded, or emits a delay event. The amount is floored to whole AMG first, exactly asConversion.convertUBAToAmgdoes on chain. A delayed mint does not revert — the executor retries with the same FDC proof afterexecutionAllowedAt.
Important Notes
- The minting cap rejects, the rest throttle.
MintingCapExceededis a revert raised before the limiters, so a pre-flight that only checks the windows can advertise an amount the chain refuses outright. - Large mintings take their own branch.
Amounts at or above
directMintingLargeMintingThresholdUBAare delayed bydirectMintingLargeMintingDelaySeconds, skip the hourly and daily limiter, and consume no window capacity. - Boundaries are inclusive. Minting exactly at the large-mint threshold is delayed, and so is a mint that lands exactly on a window cap.
- Governance unblock is retroactive.
unblockDirectMintingsUntilonly accepts timestamps in the past and releases already-delayed mintings that started before that timestamp — including large-minting delays. It never exempts a new minting from the limiter. - Reads are stale between writes. Always advance the window off-chain when displaying live limiter state.
- Plan UI flows around
DirectMintingDelayedandLargeDirectMintingDelayedso 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.