Handler not executing
- Verify
instruction discriminator - Allocate sufficient
compute_units
Deserialization errors
- Use
UncheckedAccountfor committed accounts in action context - Manually deserialize via
try_deserializeon borrowed data - Check account discriminator matches expected type
Transaction failures
- Ensure all action accounts are listed in
ShortAccountMeta - Match
is_writableflags 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)
Helpful links
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

