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

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 only keccak256(userOp) in fixed 42 bytes; an off-chain executor delivers the ABI-encoded custom instruction (userOp) via executeDirectMintingWithData.
  • Raw Custom Instruction - opcode 0xFF. The XRPL memo contains the ABI-encoded PackedUserOperation in 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

DimensionCustom Instruction (0xFE)Raw Custom Instruction (0xFF)
XRPL memo payload10-byte header + keccak256(userOp) (fixed 42 bytes)10-byte header + abi.encode(PackedUserOperation)
XRPL memo sizeConstant 42 bytes regardless of batch sizeGrows linearly with batch and argument sizes; capped at ~1024 bytes
Privacy of call payloadOnly the hash is public; bytes travel off-chain to the executortarget, value, data are public on XRPL
ActorsTwo: the XRPL user plus an off-chain executor who calls executeDirectMintingWithDataOne: the XRPL user; an indexer relays the payment
Where the UserOp is decodedThe MasterAccountController decodes from _data after verifying keccak256(_data) == hashThe MasterAccountController decodes from _memoData in handleMintedFAssets
AssetManager entry pointexecuteDirectMintingWithData(proof, data)executeDirectMinting(proof)
msg.value on the inner callWhatever the executor attaches; forwarded to executeUserOpWhatever the relayer attaches; forwarded to executeUserOp
ConfirmationSame event lives in the executor's transaction receiptWatch for UserOperationExecuted after the MasterAccountController dispatches the memo
Replay protectionSame as raw - plus the on-chain hash check pins _data to the memo's commitmentPersonal account nonce + usedTransactionIds
Failure modes specific to the flowCustomInstructionHashMismatch if keccak256(_data) differs from the commitmentNone beyond shared ones

Use the custom instruction (0xFE) when

  • The user operation's ABI encoding exceeds the XRPL memo cap (long bytes arguments, 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 PackedUserOperation is built the same way in both - only sender, nonce, and callData are validated; the rest can be empty.
  • The Call[] struct is identical.
  • The personal account's executeUserOp runs identically: each call is dispatched in order with the personal account as msg.sender, and any inner revert surfaces as CallFailed.
  • The XRPL Payment must 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 Payment amount; 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:

A practical decision rule when integrating:

  1. Default to the custom instruction (0xFE). It fits every call batch and keeps the call payload off the public XRPL ledger.
  2. Reach for the raw custom instruction (0xFF) only when you do not want to operate or coordinate with an executor service, and you can guarantee abi.encode(userOp) stays well under ~900 bytes (leaving room for the 10-byte header and XRPL framing).

Reference