# Custom Instruction Comparison

> Comparing the raw (0xFF) and hash-based (0xFE) custom instruction flows for Flare Smart Accounts.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.

Source: https://dev.flare.network/smart-accounts/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**](/smart-accounts/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**](/smart-accounts/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`](/smart-accounts/reference/IMasterAccountController#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[​](#comparison "Direct link to Comparison")

Dimension

[Custom Instruction](/smart-accounts/custom-instruction) (`0xFE`)

[Raw Custom Instruction](/smart-accounts/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 `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[​](#similarities "Direct link to 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`](/smart-accounts/reference/IMasterAccountController#directmintingexecuted)).

## Choosing in TypeScript[​](#choosing-in-typescript "Direct link to Choosing in TypeScript")

The example helpers in [`flare-viem-starter`](https://github.com/flare-foundation/flare-viem-starter) mirror the two flows directly:

-   Custom Instruction: [`sendHashInstruction`](/smart-accounts/guides/typescript-viem/custom-instruction-ts#step-1-send-the-hash-memo-on-xrpl) + [`executeDirectMintingWithData`](/smart-accounts/guides/typescript-viem/custom-instruction-ts#step-2-executor-submits-the-proof-and-the-bytes) + [`findUserOperationExecuted`](/smart-accounts/guides/typescript-viem/custom-instruction-ts#step-3-confirm-execution-from-the-receipt) - three calls, one per actor.
-   Raw Custom Instruction: [`sendMemoFieldInstruction`](/smart-accounts/guides/typescript-viem/raw-custom-instruction-ts#sending-the-user-operation) - one call, waits for the event.

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[​](#reference "Direct link to Reference")

-   [`IMasterAccountController`](/smart-accounts/reference/IMasterAccountController) — memo dispatch, `UserOperationExecuted`, errors.
-   [`IPersonalAccount`](/smart-accounts/reference/IPersonalAccount) — `executeUserOp`, `Call`, `CallFailed`.
-   [FAssets direct minting](/fassets/direct-minting) — `executeDirectMinting`, `executeDirectMintingWithData`, `directMintingPaymentAddress`.
