MagicBlock by example: the delegate → ER → commit loop
A code walk through magicblock-engine-examples — the canonical repo for Ephemeral Rollups. The same Anchor program runs on base chain and the ER; you delegate a PDA, run fast txs, then commit/undelegate. Program + client side, and which example to start with.
devrels.xyz/a/68short linkIf you've read the Ephemeral Rollups overview, you know the pitch: 10ms blocks, near-zero fees, no new chain to bridge to. What that article doesn't give you is the muscle memory. magicblock-engine-examples does — it's the repo that turns "ephemeral rollup" from a concept into a loop you can run.

The whole mental model fits in one sentence: it's one Anchor program, served by two RPC endpoints. A PDA lives on Solana like normal. You delegate it to an Ephemeral Rollup validator, which means write-authority temporarily moves to the ER. You fire transactions at the ER's RPC — fast and effectively free — and when you're done you commit the state back to base chain and undelegate. Same program ID, same instructions; only the endpoint changes.
The lifecycle, on the program side
The canonical example is anchor-counter — a PDA counter. Beyond the boring initialize/increment, it adds the four instructions that are Ephemeral Rollups. The #[ephemeral] macro and the ephemeral_rollups_sdk crate do the heavy lifting:
use ephemeral_rollups_sdk::anchor::{commit, delegate, ephemeral};
use ephemeral_rollups_sdk::cpi::DelegateConfig;
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;
#[ephemeral]
#[program]
pub mod public_counter {
use super::*;
// 1. Hand the PDA's write-authority to an ER validator.
pub fn delegate(ctx: Context<DelegateInput>) -> Result<()> {
ctx.accounts.delegate_pda(
&ctx.accounts.payer,
&[COUNTER_SEED],
DelegateConfig {
validator: ctx.remaining_accounts.first().map(|a| a.key()),
..Default::default()
},
)?;
Ok(())
}
// 2. Push the ER's current state back to base chain (still delegated).
pub fn commit(ctx: Context<IncrementAndCommit>) -> Result<()> {
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit(&[ctx.accounts.counter.to_account_info()])
.build_and_invoke()?;
Ok(())
}
// 3. Commit AND return write-authority to base chain.
pub fn undelegate(ctx: Context<IncrementAndCommit>) -> Result<()> {
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit_and_undelegate(&[ctx.accounts.counter.to_account_info()])
.build_and_invoke()?;
Ok(())
}
}Note the combo instructions in the real source — increment_and_commit and increment_and_undelegate — which mutate state and flush it in one transaction. That's the common shape in a real app: do work on the ER, periodically (or at session end) commit it down.
The lifecycle, on the client side
This is where the "two endpoints" idea becomes concrete. The test sets up two providers — one for Solana devnet, one for the ER — and just routes the same program calls to whichever it wants:
import * as anchor from "@coral-xyz/anchor"
import { GetCommitmentSignature } from "@magicblock-labs/ephemeral-rollups-sdk"
// Base layer: ordinary Solana devnet
const provider = new anchor.AnchorProvider(
new anchor.web3.Connection("https://api.devnet.solana.com"),
wallet,
)
// Ephemeral Rollup: a different RPC + WS endpoint
const erProvider = new anchor.AnchorProvider(
new anchor.web3.Connection(
"https://devnet-as.magicblock.app/",
{ wsEndpoint: "wss://devnet-as.magicblock.app/" },
),
wallet,
)
// 1. Delegate on the BASE layer
await program.methods.delegate().rpc() // via `provider`
// 2. Increment on the ER — same instruction, ER endpoint
let tx = await program.methods.increment().transaction()
tx.feePayer = erProvider.wallet.publicKey
tx.recentBlockhash = (await erProvider.connection.getLatestBlockhash()).blockhash
tx = await erProvider.wallet.signTransaction(tx)
const erTxHash = await erProvider.sendAndConfirm(tx) // ~10ms, ~free
// 3. Read the base-chain commit signature once it lands
const commitSig = await GetCommitmentSignature(erProvider.connection, /* ... */)
// 4. increment_and_undelegate on the ER returns authority to base chain
await program.methods.incrementAndUndelegate().rpc() // via `erProvider`That's the entire trick. No bridge, no separate token, no rollup SDK in your frontend — an AnchorProvider pointed at a different URL. The ER endpoint above (devnet-as.magicblock.app) is the public devnet one; the rollups are still under testing, so grab a current endpoint from MagicBlock's Discord.
The map: where to start
The repo has ~17 examples. They're not a tutorial sequence — they're a pattern library. Roughly:
- Counters (start here) —
anchor-counteris the reference. Then the same program inrust-counter(native) andpinocchio-counter(no heap, no Borsh) if you care about CU budget. - Auth & permissions —
session-keys(gpl-session delegated signers across base chain and ER),private-counterandpinocchio-private-counter(state gated by an on-rollup permission account). - Scheduling & ephemeral state —
crank-counter(MagicBlock's scheduled crank drives the tx),ephemeral-account-chats(state that lives only on the ER). - Tokens —
dummy-token-transferandspl-tokensdelegate token accounts and move value on both layers. - VRF & games —
roll-diceandpinocchio-roll-dice(verifiable randomness on the ER),rewards-delegated-vrf, androck-paper-scissor(hidden moves until reveal — a clean real-time-game shape). - Advanced patterns —
magic-actions(trigger base-chain actions from inside an ER) andoncurve-delegation(delegate non-PDA, on-curve accounts).
Pre-Anchor-1.0 versions live under 00-LEGACY_EXAMPLES if you're still on Anchor 0.32.1. For full end-to-end demos rather than primitives, MagicBlock points you at their separate starter-kits repo.
The honest read
Two things to keep straight. First, delegation is a real ownership change — while a PDA is delegated, base-chain transactions can't mutate it; the ER is the source of truth until you commit. That's the right model for a game session or an order book, and the wrong one for state that arbitrary base-chain programs need to write concurrently. Second, commit cadence is a design decision: commit too rarely and a validator hiccup risks more un-flushed state; commit every tx and you give back some of the latency win. The examples deliberately show both commit and increment_and_commit so you can pick.
And the honest scope: Ephemeral Rollups earn their keep for real-time, high-frequency state — games, live markets, agent loops. A CRUD app that writes a few times a minute doesn't need any of this. But if you've been reaching for an L2 or an off-chain server to get interactivity, this repo is the argument that you can stay on Solana and just change the endpoint.
References
- magicblock-engine-examples — the repo
- Ephemeral Rollups docs
- MagicBlock starter-kits — full demos
- MagicBlock: ephemeral rollups for real-time Solana (the concepts)
Ephemeral Rollups are easy to over-mystify. The examples repo cuts through it: delegate a PDA, point your provider at the ER, do your fast work, commit it home. One program, two endpoints.
Keep reading
MagicBlock did not stop at Ephemeral Rollups. They built the people stack: a public builder directory, monthly Blitz hackathons, Forge for teams that keep shipping, Hacker House for GTM and raise, and Champions paid monthly to grow the ecosystem.
An audit report is worthless if you can't confirm the deployed bytecode is what was audited. Solana verified builds fix that: a Docker-pinned toolchain produces a deterministic .so, its hash is compared to the on-chain program data, and the result is written to a PDA anyone can read. Solana Explorer shows a verified badge. Here's the full workflow.
Anchor optimises for developer experience. Pinocchio optimises for compute units. Steel sits in the middle. Bundle size, CU cost, and ergonomics — compared.
Get new articles in your inbox
Technical deep-dives on Solana tooling, infrastructure, and ecosystem. No noise.
