The dead zone does not end where the docs end.
It ends at the sum of the minting fee and the executor fee, and that sum appears in no single protocol setting.
Open source · Dart, with a WASM build for TypeScript
An XRPL payment aimed at Flare is one shot and final. Every byte that decides the outcome is committed before you sign, and every way it can go wrong resolves afterwards, on a different chain, with the money already gone. Plimsoll computes the outcome first, from live chain state, and refuses the ones that lose money.
$ plimsoll forecast \ --xrp 0.15 --to 0x38d5…8e83 # reads live Coston2, forecasts a # payment that does not exist yet verdict unsafe reason ConsumedAsFeeZeroMint you send 0.15 XRP fee taken 0.10 XRP (minting) executor 0.05 XRP (whatever is left) delivered 0.00 XRP warning paymentTooSmall: false
A Plimsoll line is the mark painted on a ship's hull showing how deep it may be loaded before it stops being safe. Samuel Plimsoll got it written into law in 1876, after too many overloaded ships sank with crews who had no way to know. It is now on every hull in the world.
It is the right name because our first finding is a load line, and it sits well above where the documentation puts it.
Silently. No event, no revert, the transaction succeeds. On Coston2 today, FAssets direct
minting takes a minimum fee of 0.1 XRP and an executor fee of 0.1 XRP, but the protocol's
own paymentTooSmall flag only checks against the first.
| You send | Protocol warns? | You receive |
|---|---|---|
| 0.05 XRP | yes | 0 |
| 0.10 XRP | no | 0 |
| 0.15 XRP | no | 0 |
| 0.20 XRP | no | 0 |
| 0.200001 XRP | no | 0.000001 XRP |
The real waterline is 0.2 XRP plus one drop, the sum of two independent settings, which moves whenever governance changes either.
What the documentation already says, and where it stops. Flare's FAssets minting docs do say that a payment below the minimum minting fee mints nothing and that the whole payment goes to the fee receiver. The bottom of this range is documented and we are not claiming to have found an unknown bug. Three things are not documented, and each of them is what actually costs money.
It ends at the sum of the minting fee and the executor fee, and that sum appears in no single protocol setting.
paymentTooSmall is computed against the minimum fee alone. Across the whole 0.1 to 0.2 XRP band it reads false while delivery is zero.
So a value hardcoded from today's docs is a latent bug rather than a wrong constant. The number has to be read from the chain at the moment of use.
Anyone who reads "minimum 0.1 XRP" and hardcodes it ships this.
This was not inferred from source. It was paid for, on chain, on purpose.
Three ways, shortest first. None of them needs anything from us, and the third one needs no chain either.
export RPC=https://coston2-api.flare.network/ext/C/rpc AM=0xc1Ca88b937d0b528842F95d5731ffB586f4fbDFA cast call $AM "getDirectMintingMinimumFeeUBA()(uint256)" --rpc-url $RPC # 100000 UBA = 0.1 XRP cast call $AM "getDirectMintingExecutorFeeUBA()(uint256)" --rpc-url $RPC # 100000 UBA = 0.1 XRP
cast receipt 0x8aa1437fc1ce9cb6d08c191ed34dd92ac9774f3e48d3a0bbe65804773f19b8df --rpc-url $RPC
The transaction succeeded. Its logs contain exactly two Transfer events,
100,000 UBA to the fee receiver and 50,000 to the executor, and a
DirectMintingExecuted carrying mintedAmountUBA: 0. The intended recipient
appears nowhere and no warning event fired. Note the executor took 50,000 rather than its
configured 100,000: it is paid whatever remains. Every party except the recipient was paid
in full.
cd packages/plimsoll_cli && dart pub get # Reads live Coston2 and forecasts a payment that does not exist yet. dart run bin/plimsoll.dart forecast \ --from rDE4JUm2jaue31VwidRXWuWzf5dQkUxcsB \ --xrp 0.15 --to 0x38d58d1bea8ff21fd8397494f17f64a99bcf8e83 \ -o /tmp/deadzone.json # UNSAFE, do not sign this payment ... delivered 0 XRP (exit 1) # Re-derives that forecast from the record alone. No chain, no clock, no network. dart run bin/plimsoll.dart reproduce /tmp/deadzone.json # exit 0
Now break it, and watch the check fail.
python3 -c "
import json; d=json.load(open('/tmp/deadzone.json'))
d['forecast']['verdict']='info'; d['forecast']['safeToSign']=True
json.dump(d, open('/tmp/tampered.json','w'))"
dart run bin/plimsoll.dart reproduce /tmp/tampered.json
# DOES NOT REPRODUCE
# forecast.verdict: recorded info, re-derived unsafe
# forecast.safeToSign: recorded true, re-derived false (exit 1)
That asymmetry is the point of the tool. forecast reads the chain.
reproduce reads nothing. A forecast is only worth something if the person
relying on it can check it without also having to rely on the machine that produced it.
Seven answers, before the signature rather than after it.
FAssets routes on memo length and destination tag, in a specific precedence order, and a registered destination tag silently overrides the memo entirely. Plimsoll tells you who really receives the funds.
Integer arithmetic against live parameters. The recipient path subtracts the executor fee and the smart-account path does not. One formula for both is wrong for one of them.
And the allowedAt each one implies, so a payment that will sit for hours says so before it is sent.
For Smart Accounts 0xFE and 0xFF instructions: nonce currency, executor pinning, hash commitment, and every inner call run through eth_call, so a batch that would revert is caught before the XRP is gone.
With a reason code and a plain explanation. There is no boolean success flag anywhere in the model, because a successful transaction that minted nothing is not a success.
The exact 0xE0 sequence, with the bytes and the ordering constraints, and a refusal to propose one where recovery does not apply.
What it is not. No signing. No custody. No new bridge, wrapped asset, token, or vault.
It does not reimplement Smart Accounts. Flare shipped those; Plimsoll makes them safe to aim at. Our being offline must never stop someone completing or recovering a payment through the canonical path, and that is demonstrated by test rather than asserted.
Each of these contradicted the documentation, and each of them would have cost money.
| Finding | Section |
|---|---|
| The dead zone. 0.1 to 0.2 XRP delivers zero, silently | §1, §2.2 |
PackedUserOperation is the nine field EIP-4337 struct, not the ten field one in the docs' own example | §5 |
0xE2 memos are 50 bytes, 0xD0 is 30 and 0xD1 is 10, not the documented "same 42-byte shape" | §8.1 |
A recovery payment needs received − systemFee ≥ executorFee, not "a positive net mint amount" | §8.3 |
DirectMintingExecuted is two different events on two contracts, with fields in different places | §6.2 |
| The smart-account branch mints to the controller, which forwards what is left, often nothing | §6.1 |
Direct-minting events index nothing, so payments are located via PaymentConfirmed instead | §7.2 |
| Coston2 runs a public executor that mints in about two minutes | §10 |
| Flare's own published preflight is off by one on all three delay bounds. The contracts compare inclusively, the tool exclusively, and the amount it calls "the largest that avoids delay" is delayed | §19 |
Four transactions, each sent to provoke one specific failure.
| Claim | Transaction |
|---|---|
| 0.15 XRP delivered zero, no warning | 0x8aa1437f…19b8df |
The recipient branch has no callback. A contract whose entire runtime is 0xfd received 800,000 UBA | 0x4504b9d4…da409e |
| 1.8 XRP redirected to a stranger by a destination tag, silently. The memo's target received nothing | 0x063a0028…fb260707 |
A stranded payment recovered: IgnoreMemoSet, then the mint with no UserOperationExecuted | 0x54e81978…3c8f121c |
Built and exercised on live Coston2. Every suite re-run and green on 5 August 2026.
| Package | Tests | Covering |
|---|---|---|
plimsoll_core, pure, zero dependencies | 332 | forecasting, classification, recovery planning |
plimsoll_flare, the chain-reading shell | 20 + 67 live | snapshots, dry runs, lifecycle |
plimsoll_cli | 18 + 4 live | the artefact and its verification |
plimsoll_js, WASM bindings | 7 | the same engine, callable from npm |
plimsoll_mcp, the agent boundary | 25 | protocol, and the read-only guarantee enforced |
plimsoll_app, Flutter reference client | 7 | an unsafe verdict cannot be signed |
Over the public Coston2 RPC, of which the engine itself is 1 ms. The rest is twenty contract reads collapsed into one multicall.
XRPL validation to FXRP minted, over 30 payments, all of which were picked up. Dominated by the FDC voting round.
The dead zone, the 0.05 XRP warning, the one-drop boundary, both ends of the zone, a stranded payment recovered via 0xE0, and the tag override.
What is not proven, stated here rather than left for someone to discover.
Dry-run fidelity is partial. eth_simulateV1 and debug_traceCall
are unavailable on Coston2. eth_call with state overrides reproduces the batch
atomically under the real caller, but not the reentrancy-re-enabled context, the advanced
nonce, or a first-ever mint where the account is not yet deployed. It predicts most reverts,
not all, every result carries a fidelity record naming its gaps, and a test asserts that none
ever claims to be complete.
Delays cannot be provoked on testnet. Both thresholds sit at 100,000 XRP against usage of about ten. Delay prediction is verified against pinned source and live limiter state, never by provocation. Reaching the threshold needs roughly 1,000 faucet top-ups; that was attempted and stopped at 59% after two hours, because the faucet throttles and three more hours of load on a shared resource was not worth an event the source already predicts.
The executor-fee fallback is unconfirmed. A skipped memo is claimed to fall back to the asset manager's default fee. Our run cannot tell, because the memo asked for 100,000 and the default is also 100,000.
Three projects, one build
Know what your XRPL payment will do on Flare before you sign it.
plimsoll.trimmy.xyz The SDKTyped bindings for Flare's own contracts. Flare publishes guides for five languages and none of them is Dart.
flaredart.trimmy.xyz ↗ The productAI-friendly automation for XRP. One XRPL payment arms a standing rule on Flare that afterwards executes itself.
trimmy.xyz ↗