Agent documentation index: llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
Skip to main content

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:

  1. Global minting cap — mintingCapAMG is checked and reserved before the rate limits, and it reverts with MintingCapExceeded
  2. Hourly window — directMintingHourlyLimitUBA via the MintingRateLimiter library
  3. Daily window — directMintingDailyLimitUBA via the same library
  4. Large-mint rule — amounts at or above directMintingLargeMintingThresholdUBA incur a fixed directMintingLargeMintingDelaySeconds hold

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:

  1. Unused capacity does not roll over. subOrZero clamps at zero, so an idle hour does not give twice the cap the next hour — every fresh window starts at exactly maxPerWindow.
  2. Over-cap mints are delayed, not rejected. executionAllowedAt is set to windowStartTimestamp + windowSize * mintedInCurrentWindow / maxPerWindow, so overflow drains hour-by-hour through the subOrZero step.
  3. 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 publicClient pointing at the Flare network you want to query (Coston2 by default in the starter).

Minting Limits Script​

src/fassets/direct-minting-limits.ts
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​

  1. Window sizes. HOURLY_WINDOW_SECONDS and DAILY_WINDOW_SECONDS are the windowSizeSeconds values the MintingRateLimiter.sol library uses — 3600 aligns to UTC hour boundaries, 86400 aligns to 00:00 UTC. Both are fixed when the asset manager is initialized (1 hours and 1 days) and there is no getter for them.
  2. Format helpers. formatUba prints raw UBA (units of basis assets) alongside the XRP equivalent via dropsToXrp. formatDuration turns a number of seconds into 1d 1h 1m 1s, keeping interior zeros so a two-hour delay reads 2h 0m 0s, and formatTimestamp pairs 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.
  3. Replay the limiter slide. computeWindowState mirrors the window-advancement logic in MintingRateLimiter.sol: it advances windowStart past every tumble that has elapsed and drains mintedInCurrentWindow by windowsElapsed * limit. Without this off-chain replay, you would see stale numbers between writes. It reports both the headroom to the cap and maxWithoutDelayUBA, which is one AMG lower because the on-chain check is minted < limit.
  4. Window delay math. computeWindowExecutionAllowedAt applies the proportional overflow formula from MintingRateLimiter: when used + proposed reaches the cap, executionAllowedAt = effectiveStart + windowSize * (used + proposed) / limit.
  5. Large-mint branch. isLargeMinting uses the same inclusive >= comparison as the contract.
  6. Combine the mechanisms the way the contract does. computeDirectMintingExecutionAllowedAt returns 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.
  7. Print one window's live state. printWindow shows the live cap, used amount and percentage, remaining headroom, the largest non-delayed mint, the effective window start, and the next UTC tumble.
  8. Resolve AssetManagerFXRP. getContractAddressByName("AssetManagerFXRP") looks up the asset manager through the Flare Contract Registry.
  9. Pin the snapshot to one block. getLatestBlock provides the block number every read is pinned to, and its timestamp is used as now. The limiter compares against block.timestamp, so replaying the windows against the local clock would drift, and unpinned reads could straddle blocks.
  10. Read everything in parallel. Promise.all fetches the asset manager settings (AMG granularity, FAsset address, minting cap), the hourly and daily caps, the raw limiter state for hourly and daily windows, the unblockUntilTimestamp, and the large-minting threshold and delay in one round trip.
  11. Convert AMG to UBA. The limiter stores mintedInCurrentWindow in AMG (uint64) for cheap on-chain storage. Multiplying by assetMintingGranularityUBA rebases it into UBA (units of basis assets) so it can be compared against the UBA-denominated cap.
  12. Global minting cap headroom. checkMintingCap works in AMG, so the supply is floored to whole AMG the way Conversion.convertUBAToAmg does before subtracting it from mintingCapAMG. The result is still an upper bound, because reserved capacity is not readable off-chain; beyond it the mint reverts instead of being delayed.
  13. Report the governance unblock. getDirectMintingsUnblockUntilTimestamp is 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.
  14. 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.
  15. Pre-flight a concrete amount. Set MINT_PREFLIGHT_XRP (defaults to 10) to see whether that mint executes immediately, reverts with MintingCapExceeded, or emits a delay event. The amount is floored to whole AMG first, exactly as Conversion.convertUBAToAmg does on chain. A delayed mint does not revert — the executor retries with the same FDC proof after executionAllowedAt.

Important Notes​

  • The minting cap rejects, the rest throttle. MintingCapExceeded is 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 directMintingLargeMintingThresholdUBA are delayed by directMintingLargeMintingDelaySeconds, 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. unblockDirectMintingsUntil only 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 DirectMintingDelayed and LargeDirectMintingDelayed so 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.
What's next

To continue your FAssets development journey, you can: