Custom Instruction Comparison
Flare Smart Accounts expose two custom instruction memo opcodes that ultimately execute the same PackedUserOperation in the personal account scope:
- Custom Instruction - opcode
0xFE. The XRPL memo carries onlykeccak256(userOp)in fixed 42 bytes; an off-chain executor delivers the ABI-encoded custom instruction (userOp) viaexecuteDirectMintingWithData. - Raw Custom Instruction - opcode
0xFF. The XRPL memo contains the ABI-encodedPackedUserOperationin full.
Both flows are validated on-chain against the same (sender, nonce) rules and emit the same UserOperationExecuted event.
The difference is purely in how the user-operation bytes travel to Flare, and which actor performs which step.
The hash-based flow was added because the XRPL Payment memo is capped at 1024 bytes - small enough that non-trivial user operations either had to be split across multiple XRPL payments (each paying its own minting and executor fees) or routed through purpose-built shim contracts on Flare that compressed the call into something memo-sized.
The 0xFE memo is a constant 42 bytes regardless of the user's operation size, removing the need for both workarounds.
Comparison
| Dimension | Custom Instruction (0xFE) | Raw Custom Instruction (0xFF) |
|---|---|---|
| XRPL memo payload | 10-byte header + keccak256(userOp) (fixed 42 bytes) | 10-byte header + abi.encode(PackedUserOperation) |
| XRPL memo size | Constant 42 bytes regardless of batch size | Grows linearly with batch and argument sizes; capped at ~1024 bytes |
| Privacy of call payload | Only the hash is public; bytes travel off-chain to the executor | target, value, data are public on XRPL |
| Actors | Two: the XRPL user plus an off-chain executor who calls executeDirectMintingWithData | One: the XRPL user; an indexer relays the payment |
| Where the UserOp is decoded | The MasterAccountController decodes from _data after verifying keccak256(_data) == hash | The MasterAccountController decodes from _memoData in handleMintedFAssets |
| AssetManager entry point | executeDirectMintingWithData(proof, data) | executeDirectMinting(proof) |
msg.value on the inner call | Whatever the executor attaches; forwarded to executeUserOp | Whatever the relayer attaches; forwarded to executeUserOp |
| Confirmation | Same event lives in the executor's transaction receipt | Watch for UserOperationExecuted after the MasterAccountController dispatches the memo |
| Replay protection | Same as raw - plus the on-chain hash check pins _data to the memo's commitment | Personal account nonce + usedTransactionIds |
| Failure modes specific to the flow | CustomInstructionHashMismatch if keccak256(_data) differs from the commitment | None beyond shared ones |
Use the custom instruction (0xFE) when
- The user operation's ABI encoding exceeds the XRPL memo cap (long
bytesarguments, large call batches, or deeply nested structs). Without it, you would either split the logical user operation into multiple XRPL payments (paying minting and executor fees on each) or deploy a Flare-side shim contract to compress the call - the custom instruction eliminates both. - You want the call payload to remain off the public XRPL ledger.
- You already operate an executor that observes XRPL payments and submits Flare transactions - the custom instruction lets that executor batch deliveries or rate-limit submissions without splitting user operations into multiple XRPL payments.
- You need fee predictability: the XRPL memo is always 42 bytes, so the XRPL fee is constant regardless of how complex the user operation is.
Use the raw custom instruction (0xFF) when
- The call batch fits comfortably inside the XRPL ~1024-byte memo (most single-call user operations and small batches do).
- You do not need to hide the call payload from public XRPL observers.
- You do not want to operate or coordinate with an executor service.
- You want the simplest possible on-chain integration: a single FAssets transaction
executeDirectMinting(proof)carries the whole thing.
In short, the custom instruction trades a small amount of off-chain coordination (and one extra on-chain proof-binding check) for a fixed memo size and private payloads. The raw custom instruction trades memo bandwidth for end-to-end simplicity.
Similarities
The two flows share more than they differ:
- The
PackedUserOperationis built the same way in both - onlysender,nonce, andcallDataare validated; the rest can be empty. - The
Call[]struct is identical. - The personal account's
executeUserOpruns identically: each call is dispatched in order with the personal account asmsg.sender, and any inner revert surfaces asCallFailed. - The XRPL
Paymentmust be untagged in both flows; a destination tag forces the FAssets minting to credit the tag holder instead of the smart account. - Both flows pay the FAssets minting fee and executor fee out of the XRPL
Paymentamount; the rest is minted as FXRP to the personal account. - The mint succeeds even if the user operation reverts, because the FAsset transfer runs before the memo is decoded (
DirectMintingExecuted).
Choosing in TypeScript
The example helpers in flare-viem-starter mirror the two flows directly:
- Custom Instruction:
sendHashInstruction+executeDirectMintingWithData+findUserOperationExecuted- three calls, one per actor. - Raw Custom Instruction:
sendMemoFieldInstruction- one call, waits for the event.
A practical decision rule when integrating:
- Default to the custom instruction (
0xFE). It fits every call batch and keeps the call payload off the public XRPL ledger. - Reach for the raw custom instruction (
0xFF) only when you do not want to operate or coordinate with an executor service, and you can guaranteeabi.encode(userOp)stays well under ~900 bytes (leaving room for the 10-byte header and XRPL framing).
Reference
IMasterAccountController— memo dispatch,UserOperationExecuted, errors.IPersonalAccount—executeUserOp,Call,CallFailed.- FAssets direct minting —
executeDirectMinting,executeDirectMintingWithData,directMintingPaymentAddress.