Skip to main content

Handler not executing

  • Verify instruction discriminator
  • Allocate sufficient compute_units

Deserialization errors

  • Use UncheckedAccount for committed accounts in action context
  • Manually deserialize via try_deserialize on borrowed data
  • Check account discriminator matches expected type

Transaction failures

  • Ensure all action accounts are listed in ShortAccountMeta
  • Match is_writable flags to actual usage
  • Increase compute budget for all actions in the commit

Security: authenticate the caller

An action is a delegation-program instruction the committor runs on the base layer after a commit; the validator pays its transaction fee. The handler may still need an on-chain payer for any rent it creates (e.g. initializing an ATA). For that the delegation program uses an ephemeral balance escrow — a SOL-holding PDA derived from [b"balance", escrow_auth, escrow_index] — and injects two accounts into your #[action] context: escrow_auth (the payer identity that owns the balance — a user wallet or a program PDA) and escrow (the SOL PDA itself, which the delegation program signs via invoke_signed when it runs the action, so the handler can spend it for rent). That escrow signature is also the authentication anchor. A #[action] handler is otherwise a normal base-layer instruction — for any work it performs (writing a PDA it owns, leaderboard updates, mints, settlements, token transfers; no action type is special). The attribute makes it dispatchable from a post-commit action, not exclusively so — anyone can invoke it directly. #[account(address = ...)], seeds, and owner constraints only pin which accounts are passed; only the injected escrow signer proves the delegation program dispatched the call (no wallet or other program can sign for that PDA). ActionArgs::new defaults escrow_index to 255. On every handler, require escrow as signer pinned to its derivation. When the handler acts with a program-owned PDA, also bind escrow_auth to that PDA:
signer alone is not enough — a different program could still schedule an action into your handler with its own escrow authority; binding escrow_auth to your program’s PDA closes that. For user-paid actions (escrow authority = the user’s wallet) the signer + derivation check is the guarantee, and there is no fixed PDA to bind against. Need the same logic callable outside an action too (an admin settlement, the user directly)? The escrow constraints above make the instruction action-only. The default fix is two thin instructions over one shared function: keep the #[action] entrypoint with its escrow checks, add a normal Signer-authorized entrypoint, and have both call the same internal function. A single dual-mode instruction is possible — declare escrow/escrow_auth as Option<...>, add an optional authority, and branch in the handler requiring exactly one path (via_action ^ via_direct) — but injected/optional accounts are positional (the delegation program appends its accounts last), so two instructions is the cleaner default.

Limitations & considerations

  • Handlers execute on base layer and consume base-layer fees
  • Standard Solana limits apply (compute, account locks)
  • Atomicity: any action failure reverts the commit
  • First two action accounts are injected (escrow, escrow_auth)

Ephemeral Rollups

Delegation, Commitment & Undelegation

Magic Router

Router overview and flow

Magic Actions Example

Explore reference implementation on GitHub

Community Support

Chat with the team and community