Skip to main content
Ephemeral accounts are accounts that exist only within the Ephemeral Rollup. A sponsor account (which is delegated to the ER) pays rent on behalf of ephemeral accounts at 32 lamports/byte — 109x cheaper than Solana’s base rent. Key properties:
  • Born, live, and die entirely on the ER
  • Owned by the calling program (inferred from CPI context)
  • Funded by a sponsor account’s lamports
  • Can be created, resized, and closed

The #[ephemeral_accounts] Macro

This proc-macro attribute goes on an Anchor Accounts struct. It recognizes two custom markers inside #[account(...)]:
MarkerPurpose
sponsorMarks the account that pays rent for ephemeral accounts
ephMarks an account as ephemeral (ER-only)

Validation Rules

  • At least one sponsor is required if any eph fields exist
  • Only one sponsor is allowed per struct
  • eph cannot be combined with init or init_if_needed (use the generated methods instead)
  • If the sponsor is a PDA (not a Signer), it must have seeds for PDA signing

Generated Methods

For a field named conversation, the macro generates:
MethodSignatureDescription
create_ephemeral_conversation(data_len: u32) -> Result<()>Creates the ephemeral account
init_if_needed_ephemeral_conversation(data_len: u32) -> Result<()>Creates only if data_len == 0
resize_ephemeral_conversation(new_data_len: u32) -> Result<()>Grows or shrinks the account
close_ephemeral_conversation() -> Result<()>Closes account, refunds rent to sponsor

Signing Requirements

  • Sponsor: Must be a signer for all operations (create, resize, close)
  • Ephemeral: Must be a signer only on create (prevents pubkey squatting). Not required for resize or close.
  • For PDA accounts, the macro auto-derives signer seeds via find_program_address

Rent Model

pub const EPHEMERAL_RENT_PER_BYTE: u64 = 32;
const ACCOUNT_OVERHEAD: u32 = 60;

// rent = (data_len + 60) * 32
pub const fn rent(data_len: u32) -> u64 {
    (data_len as u64 + ACCOUNT_OVERHEAD as u64) * EPHEMERAL_RENT_PER_BYTE
}
  • Growing: sponsor pays additional rent to vault
  • Shrinking: vault refunds excess rent to sponsor
  • Close: all rent refunded from vault to sponsor

Create an Ephemeral Account

use ephemeral_rollups_sdk::anchor::ephemeral_accounts;

pub fn create_conversation(ctx: Context<CreateConversation>) -> Result<()> {
    ctx.accounts
        .create_ephemeral_conversation((8 + Conversation::space_for_message_count(0)) as u32)?;

    let conversation = Conversation {
        handle_owner: ctx.accounts.profile_owner.handle.clone(),
        handle_other: ctx.accounts.profile_other.handle.clone(),
        bump: ctx.bumps.conversation,
        messages: Vec::new(),
    };
    let mut data = ctx.accounts.conversation.try_borrow_mut_data()?;
    conversation.try_serialize(&mut &mut data[..])?;

    Ok(())
}

#[ephemeral_accounts]
#[derive(Accounts)]
pub struct CreateConversation<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,

    #[account(
        mut,
        sponsor,
        seeds = [b"profile", profile_owner.handle.as_bytes()],
        bump = profile_owner.bump,
        has_one = authority
    )]
    pub profile_owner: Account<'info, Profile>,

    #[account(
        seeds = [b"profile", profile_other.handle.as_bytes()],
        bump = profile_other.bump,
    )]
    pub profile_other: Account<'info, Profile>,

    /// CHECK: Ephemeral conversation PDA sponsored by the profile.
    #[account(
        mut,
        eph,
        seeds = [b"conversation", profile_owner.handle.as_bytes(), profile_other.handle.as_bytes()],
        bump
    )]
    pub conversation: AccountInfo<'info>,
    // vault and magic_program are auto-injected by the macro
}
After calling create_ephemeral_*, you must manually serialize your data struct into the raw account data. The macro allocates space but does not initialize the data.

Resize an Ephemeral Account

pub fn extend_conversation(
    ctx: Context<ExtendConversation>,
    additional_messages: u32,
) -> Result<()> {
    let current_capacity =
        Conversation::message_capacity(ctx.accounts.conversation.to_account_info().data_len());
    let new_capacity = current_capacity + additional_messages as usize;

    ctx.accounts.resize_ephemeral_conversation(
        (8 + Conversation::space_for_message_count(new_capacity)) as u32,
    )?;

    Ok(())
}

#[ephemeral_accounts]
#[derive(Accounts)]
pub struct ExtendConversation<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    #[account(mut, sponsor, seeds = [...], bump = ..., has_one = authority)]
    pub profile_sender: Account<'info, Profile>,
    #[account(seeds = [...], bump = ...)]
    pub profile_other: Account<'info, Profile>,
    /// CHECK: Ephemeral conversation PDA
    #[account(mut, eph, seeds = [...], bump)]
    pub conversation: AccountInfo<'info>,
}

Close an Ephemeral Account

pub fn close_conversation(ctx: Context<CloseConversation>) -> Result<()> {
    let profile = &mut ctx.accounts.profile_owner;
    profile.active_conversation_count = profile
        .active_conversation_count
        .checked_sub(1)
        .ok_or(ChatError::ConversationCountUnderflow)?;

    ctx.accounts.close_ephemeral_conversation()?;

    Ok(())
}

#[ephemeral_accounts]
#[derive(Accounts)]
pub struct CloseConversation<'info> {
    #[account(mut)]
    pub authority: Signer<'info>,
    #[account(mut, sponsor, seeds = [...], bump = ..., has_one = authority)]
    pub profile_owner: Account<'info, Profile>,
    #[account(seeds = [...], bump = ...)]
    pub profile_other: Account<'info, Profile>,
    /// CHECK: Ephemeral conversation PDA
    #[account(mut, eph, seeds = [...], bump)]
    pub conversation: AccountInfo<'info>,
}

Using a Wallet as Sponsor

A Signer can be used directly as the sponsor instead of a PDA:
#[ephemeral_accounts]
#[derive(Accounts)]
pub struct CreateGame<'info> {
    #[account(mut, sponsor)]
    pub payer: Signer<'info>,

    /// CHECK: Ephemeral PDA
    #[account(mut, eph, seeds = [b"game", payer.key().as_ref()], bump)]
    pub game_state: AccountInfo<'info>,
}

TypeScript Client Usage

All ephemeral account operations are sent to the ER connection, not the base layer:
// Create
await erProgram.methods
    .createConversation()
    .accounts({
        authority: userA.publicKey,
        profileOwner: profileAPda,
        profileOther: profileBPda,
        conversation: conversationPda,
        systemProgram: SystemProgram.programId,
    })
    .rpc();

// Resize
await erProgram.methods
    .extendConversation(5)
    .accounts({
        authority: userA.publicKey,
        profileSender: profileAPda,
        profileOther: profileBPda,
    })
    .rpc();

// Close
await erProgram.methods
    .closeConversation()
    .accounts({
        authority: userA.publicKey,
        profileOwner: profileAPda,
        profileOther: profileBPda,
        conversation: conversationPda,
    })
    .rpc();

Common Gotchas

eph fields must use AccountInfo<'info>, not Account<'info, T>. The account doesn’t exist yet at validation time, so Anchor cannot deserialize it.
After calling create_ephemeral_*, you must serialize your data struct into the raw account data yourself. The macro allocates space but does not write any data.
The macro enforces this at compile time. Use the generated create_ephemeral_* method instead of Anchor’s init constraint.
Transfer extra SOL to the sponsor account before delegating it, so it has enough lamports to fund ephemeral accounts on the ER.
You don’t need to declare them in your struct, but they appear in the IDL and must be passed from the client. Anchor resolves them automatically if named correctly.

Learn More

Ephemeral Accounts Demo

Full example program on GitHub

Delegation & Undelegation

How delegation and state synchronization work

Quickstart

Build your first program with Ephemeral Rollups