# getAccountInfo
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/er/getAccountInfo
pages/ephemeral-rollups-ers/api-reference/er/openapi/openapi-getAccountInfo.json POST /
Get AccountInfo for a single account.
# getBlockhashForAccounts
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/er/getBlockhashForAccounts
pages/ephemeral-rollups-ers/api-reference/er/openapi/openapi-getBlockhashForAccounts.json POST /
Get blockhash for multiple account addresses.
# getDelegationStatus
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/er/getDelegationStatus
pages/ephemeral-rollups-ers/api-reference/er/openapi/openapi-getDelegationStatus.json POST /
Get delegation status for a single account from Magic Router.
# getIdentity
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/er/getIdentity
pages/ephemeral-rollups-ers/api-reference/er/openapi/openapi-getIdentity.json POST /
Get the identity information of the current ER Validator
# getRoutes
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/er/getRoutes
pages/ephemeral-rollups-ers/api-reference/er/openapi/openapi-getRoutes.json POST /
Get available ephemeral rollup nodes from the Magic Router
# getSignatureStatuses
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/er/getSignatureStatuses
pages/ephemeral-rollups-ers/api-reference/er/openapi/openapi-getSignatureStatuses.json POST /
Returns the confirmation status for one or more signatures
# Introduction
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/er/introduction
MagicBlock Router API documentation
## Overview
The MagicBlock Router API is a JSON-RPC API that implements almost all standard Solana RPC methods and adds router-specific methods. It simplifies multi-node setups by handling node selection and routing behind a single endpoint.
It can be used to retrieve delegation status for accounts and identify the closest node for request execution. For example, `getBlockhashForAccounts` returns a blockhash from the appropriate node based on the provided accounts and their delegation status.
**Mainnet URL:** `https://router.magicblock.app`
**Devnet URL:** `https://devnet-router.magicblock.app`
**API Version:** 2.0
## Main Operations
### Network Information
* [**Get Routes**](/pages/ephemeral-rollups-ers/api-reference/er/getRoutes) - Query available routing information
* [**Get Identity**](/pages/ephemeral-rollups-ers/api-reference/er/getIdentity) - Retrieve identity information
### Account Operations
* [**Get Account Info**](/pages/ephemeral-rollups-ers/api-reference/er/getAccountInfo) - Fetch account information
* [**Get Blockhash For Accounts**](/pages/ephemeral-rollups-ers/api-reference/er/getBlockhashForAccounts) - Get blockhash for specific accounts
### Status & Delegation
* [**Get Signature Statuses**](/pages/ephemeral-rollups-ers/api-reference/er/getSignatureStatuses) - Check transaction signature statuses
* [**Get Delegation Status**](/pages/ephemeral-rollups-ers/api-reference/er/getDelegationStatus) - Query delegation information
## Key Features
* **Solana RPC Coverage** - Implements almost all standard Solana RPC methods through the router
* **Router-Specific Methods** - Adds methods such as `getBlockhashForAccounts` and `getDelegationStatus`
* **Simplified Multi-Node Setup** - Use one router endpoint instead of manually managing multiple node endpoints
* **Fast Route Query** - Efficiently determine routing paths for transactions
* **Account State** - Access current account information and balances
* **Transaction Tracking** - Monitor transaction status and confirmation
* **Delegation Queries** - Check delegation relationships through the router
## Use Cases
* Retrieve account balances and state
* Check transaction confirmation status
* Query route information for transaction routing
* Verify delegation configurations
* Avoid manual multi-node endpoint management
# getAccountInfo
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getAccountInfo
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getAccountInfo.json POST /
Returns all information associated with an account of provided Pubkey.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"method": "getAccountInfo",
"jsonrpc": "2.0",
"params": [
"5RgeA5P8bRaynJovch3zQURfJxXL3QK2JYg1YamSvyLb",
{
"encoding": "base64"
}
],
"id": 0
}'
```
# getBalance
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getBalance
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getBalance.json POST /
Solana JSON-RPC method getBalance.
# getBlock
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getBlock
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getBlock.json POST /
Returns identity and transaction information about a confirmed block in the ledger.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBlock",
"params": [
378967388,
{
"commitment": "finalized",
"encoding": "json",
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0,
"rewards": false
}
]
}'
```
# getBlockCommitment
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getBlockCommitment
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getBlockCommitment.json POST /
Returns commitment for particular block.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBlockCommitment",
"params": [
5
]
}'
```
# getBlockHeight
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getBlockHeight
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getBlockHeight.json POST /
Returns the current block height of the node.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBlockHeight",
"params": [
{
"commitment": "finalized"
}
]
}'
```
# getBlockTime
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getBlockTime
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getBlockTime.json POST /
Returns the estimated production time of a block.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBlockTime",
"params": [
5
]
}'
```
# getBlocks
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getBlocks
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getBlocks.json POST /
Returns a list of confirmed blocks between two slots.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBlocks",
"params": [
5,
10,
{
"commitment": "finalized"
}
]
}'
```
# getBlocksWithLimit
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getBlocksWithLimit
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getBlocksWithLimit.json POST /
Returns a list of confirmed blocks starting at the given slot for a given limit.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBlocksWithLimit",
"params": [
5,
3
]
}'
```
# getFirstAvailableBlock
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getFirstAvailableBlock
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getFirstAvailableBlock.json POST /
Solana JSON-RPC method getFirstAvailableBlock.
# getGenesisHash
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getGenesisHash
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getGenesisHash.json POST /
Solana JSON-RPC method getGenesisHash.
# getHealth
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getHealth
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getHealth.json POST /
Solana JSON-RPC method getHealth.
# getIdentity
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getIdentity
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getIdentity.json POST /
Solana JSON-RPC method getIdentity.
# getLargestAccounts
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getLargestAccounts
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getLargestAccounts.json POST /
Returns the 20 largest accounts, by lamport balance.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getLargestAccounts",
"params": [
{
"commitment": "finalized"
}
]
}'
```
# getLatestBlockhash
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getLatestBlockhash
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getLatestBlockhash.json POST /
Returns the latest blockhash.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getLatestBlockhash",
"params": [
{
"commitment": "processed"
}
]
}'
```
# getLeaderSchedule
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getLeaderSchedule
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getLeaderSchedule.json POST /
Returns the leader schedule for an epoch.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getLeaderSchedule",
"params": [
null,
{
"commitment": "processed",
"identity": "dv2eQHeP4RFrJZ6UeiZWoc3XTtmtZCUKxxCApCDcRNV"
}
]
}'
```
# getMinimumBalanceForRentExemption
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getMinimumBalanceForRentExemption
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getMinimumBalanceForRentExemption.json POST /
Returns minimum balance required to make account rent exempt.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getMinimumBalanceForRentExemption",
"params": [
50,
{
"commitment": "processed"
}
]
}'
```
# getMultipleAccounts
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getMultipleAccounts
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getMultipleAccounts.json POST /
Returns the account information for a list of Pubkeys.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getMultipleAccounts",
"params": [
[
"vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg",
"4fYNw3dojWmQ4dXtSGE9epjRGy9pFSx62YypT7avPYvA"
],
{
"encoding": "base58",
"commitment": "finalized"
}
]
}'
```
# getProgramAccounts
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getProgramAccounts
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getProgramAccounts.json POST /
Returns all accounts owned by the provided program Pubkey.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getProgramAccounts",
"params": [
"4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T",
{
"commitment": "finalized",
"filters": [
{
"dataSize": 17
},
{
"memcmp": {
"offset": 4,
"bytes": "3Mc6vR"
}
}
]
}
]
}'
```
# getRecentPerformanceSamples
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getRecentPerformanceSamples
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getRecentPerformanceSamples.json POST /
Returns a list of recent performance samples, in reverse slot order.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getRecentPerformanceSamples",
"params": [
2
]
}'
```
# getSignatureStatuses
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getSignatureStatuses
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getSignatureStatuses.json POST /
Returns the statuses of a list of signatures.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getSignatureStatuses",
"params": [
[
"4cdd1oX7cfVALfr26tP52BZ6cSzrgnNGtYD7BFhm6FFeZV5sPTnRvg6NRn8yC6DbEikXcrNChBM5vVJnTgKhGhVu"
],
{
"searchTransactionHistory": true
}
]
}'
```
# getSignaturesForAddress
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getSignaturesForAddress
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getSignaturesForAddress.json POST /
Returns signatures for confirmed transactions that include the given address.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getSignaturesForAddress",
"params": [
"Vote111111111111111111111111111111111111111",
{
"commitment": "finalized",
"limit": 1
}
]
}'
```
# getSlot
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getSlot
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getSlot.json POST /
Returns the slot that has reached the given or default commitment level.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getSlot",
"params": [
{
"commitment": "finalized"
}
]
}'
```
# getTokenAccountBalance
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getTokenAccountBalance
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getTokenAccountBalance.json POST /
Returns the token balance of an SPL Token account.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenAccountBalance",
"params": [
"7fUAJdStEuGbc3sM84cKRL6yYaaSstyLSU4ve5oovLS7",
{
"commitment": "finalized"
}
]
}'
```
# getTokenAccountsByDelegate
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getTokenAccountsByDelegate
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getTokenAccountsByDelegate.json POST /
Returns all SPL Token accounts by approved Delegate.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenAccountsByDelegate",
"params": [
"4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T",
{
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
},
{
"commitment": "finalized",
"encoding": "jsonParsed"
}
]
}'
```
# getTokenAccountsByOwner
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getTokenAccountsByOwner
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getTokenAccountsByOwner.json POST /
Returns all SPL Token accounts by token owner.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenAccountsByOwner",
"params": [
"A1TMhSGzQxMr1TboBKtgixKz1sS6REASMxPo1qsyTSJd",
{
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
},
{
"commitment": "finalized",
"encoding": "jsonParsed"
}
]
}'
```
# getTokenLargestAccounts
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getTokenLargestAccounts
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getTokenLargestAccounts.json POST /
Returns the 20 largest accounts of a particular SPL Token type.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenLargestAccounts",
"params": [
"3wyAj7Rt1TWVPZVteFJPLa26JmLvdb1CAKEFZm3NY75E",
{
"commitment": "finalized"
}
]
}'
```
# getTokenSupply
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getTokenSupply
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getTokenSupply.json POST /
Returns the total supply of an SPL Token type.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenSupply",
"params": [
"3wyAj7Rt1TWVPZVteFJPLa26JmLvdb1CAKEFZm3NY75E",
{
"commitment": "finalized"
}
]
}'
```
# getTransaction
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getTransaction
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getTransaction.json POST /
Returns transaction details for a confirmed transaction.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTransaction",
"params": [
"4ReKprwf3WdLHRrzp4ctPWNBsQDPL3VZz3zMmoZfcGJMJCHh5Vq937mPdyxhCbw54wNnA6hZ7KfNpQdpt13yY7A9",
{
"commitment": "confirmed",
"maxSupportedTransactionVersion": 0,
"encoding": "json"
}
]
}'
```
# getTransactionCount
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getTransactionCount
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getTransactionCount.json POST /
Returns the current transaction count from the ledger.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTransactionCount",
"params": [
{
"commitment": "finalized"
}
]
}'
```
# getVersion
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getVersion
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getVersion.json POST /
Solana JSON-RPC method getVersion.
# RPC API
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/introduction
Solana JSON-RPC HTTP methods on MagicBlock devnet RPC endpoints.
This section documents the Solana JSON-RPC HTTP method catalog on MagicBlock endpoints:
**Devnet:**
* `https://devnet-as.magicblock.app/`
* `https://devnet-us.magicblock.app/`
* `https://devnet-eu.magicblock.app/`
* `https://devnet-tee.magicblock.app/`
**Mainnet:**
* `https://as.magicblock.app/`
* `https://us.magicblock.app/`
* `https://eu.magicblock.app/`
* `https://mainnet-tee.magicblock.app/`
The method catalog is aligned with Solana RPC HTTP methods:
* [Solana RPC HTTP Methods](https://solana.com/docs/rpc/http)
TEE RPC endpoints may require an authentication token for certain methods. See
[Authorization
guide](/pages/private-ephemeral-rollups-pers/how-to-guide/quickstart#4-authorize)
for details.
# isBlockhashValid
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/isBlockhashValid
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-isBlockhashValid.json POST /
Returns whether a blockhash is still valid or not.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "isBlockhashValid",
"params": [
"J7rBdM6AecPDEZp8aPq5iPSNKVkU5Q76F3oAV4eW5wsW",
{
"commitment": "processed"
}
]
}'
```
# sendTransaction
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/sendTransaction
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-sendTransaction.json POST /
Submits a signed transaction to the cluster for processing.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "sendTransaction",
"params": [
"4hXTCkRzt9WyecNzV1XPgCDfGAZzQKNxLXgynz5QDuWWPSAZBZSHptvWRL3BjCvzUXRdKvHL2b7yGrRQcWyaqsaBCncVG7BFggS8w9snUts67BSh3EqKpXLUm5UMHfD7ZBe9GhARjbNQMLJ1QD3Spr6oMTBU6EhdB4RD8CP2xUxr2u3d6fos36PD98XS6oX8TQjLpsMwncs5DAMiD4nNnR8NBfyghGCWvCVifVwvA8B8TJxE1aiyiv2L429BCWfyzAme5sZW8rDb14NeCQHhZbtNqfXhcp2tAnaAT"
]
}'
```
# simulateTransaction
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/simulateTransaction
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-simulateTransaction.json POST /
Simulate sending a transaction.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "simulateTransaction",
"params": [
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEEjNmKiZGiOtSZ+g0//wH5kEQo3+UzictY+KlLV8hjXcs44M/Xnr+1SlZsqS6cFMQc46yj9PIsxqkycxJmXT+veJjIvefX4nhY9rY+B5qreeqTHu4mG6Xtxr5udn4MN8PnBt324e51j94YQl285GzN2rYa/E2DuQ0n/r35KNihi/zamQ6EeyeeVDvPVgUO2W3Lgt9hT+CfyqHvIa11egFPCgEDAwIBAAkDZAAAAAAAAAA=",
{
"commitment": "confirmed",
"encoding": "base64",
"replaceRecentBlockhash": true
}
]
}'
```
# Anchor Guide
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/how-to-guide/anchor
Learn how to write a simple Anchor program that increments a counter on Solana
**Building with an AI coding agent?** Install the MagicBlock Dev Skill to give your agent MagicBlock-specific patterns — delegation flows, Magic Actions, cranks, VRF, and more.
**Hit an error?** Ask your coding agent with the skill installed, not the docs assistant. The assistant only sees the docs, so it cannot debug your code.
Quick install for Claude Code:
```bash theme={null}
npx skills add https://github.com/magicblock-labs/magicblock-dev-skill
```
Using Cursor, Codex, Windsurf, Cline, or another agent? See the [AI Dev Skill](/pages/overview/additional-information/ai-dev-skill) page for all install targets.
This guide will walk you through the process of writing a simple Anchor program that increments a counter. You'll learn how to deploy this program on Solana and interact with it using a React client.
## Software Packages
This program is developed and tested with the following software packages. Other versions may also be compatible.
| Software | Version | Installation Guide |
| ---------- | ------- | --------------------------------------------------------------- |
| **Solana** | 3.1.9 | [Install Solana](https://docs.anza.xyz/cli/install) |
| **Rust** | 1.89.0 | [Install Rust](https://www.rust-lang.org/tools/install) |
| **Anchor** | 1.0.2 | [Install Anchor](https://www.anchor-lang.com/docs/installation) |
## Quick Access to Source Code
If you prefer to dive straight into the code:
## Writing the Anchor Program
Let's break down the key components of our counter program:
## Core Functionality
The program implements two main instructions:
1. `initialize`: Sets the counter to 0
2. `increment`: Increments the counter by 1
Here's the core structure of our program:
```rust theme={null}
#[ephemeral]
#[program]
pub mod public_counter {
use super::*;
/// Initialize the counter.
pub fn initialize(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = 0;
Ok(())
}
/// Increment the counter.
pub fn increment(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
}
// ... Additional instructions will be added here
}
```
Nothing special here, just a simple Anchor program that increments a counter. The only difference is that we're adding the `delegate` macro to inject some useful logic to interact with the delegation program.
### Delegating the Counter PDA
In order to delegate the counter PDA, and make it writable in an Ephemeral Rollup session, we need to add an instruction which
internally calls the `delegate_account` function. `delegate_account` will CPI to the delegation program, which upon validation will gain ownership of the account.
After this step, an ephemeral validator can start processing transactions on the counter PDA and propose state diff trough the delegation program.
Inspect transactions details on Solana Explorer
```rust theme={null}
/// Add delegate function to the context
#[delegate]
#[derive(Accounts)]
pub struct DelegateInput<'info> {
pub payer: Signer<'info>,
/// CHECK The pda to delegate
#[account(mut, del)]
pub pda: AccountInfo<'info>,
}
```
```rust theme={null}
/// Delegate the account to the delegation program
/// Set specific validator based on ER, see https://docs.magicblock.gg/pages/ephemeral-rollups-ers/how-to-guide/local-development
pub fn delegate(ctx: Context) -> Result<()> {
ctx.accounts.delegate_pda(
&ctx.accounts.payer,
&[COUNTER_SEED],
DelegateConfig {
// Optionally set a specific validator from the first remaining account
validator: ctx.remaining_accounts.first().map(|acc| acc.key()),
..Default::default()
},
)?;
Ok(())
}
```
### Committing while the PDA is delegated
The ephemeral runtime allows committing the state of the PDA while it is delegated. This is done by building a `MagicIntentBundleBuilder` with the `commit` intent and invoking it.
Inspect transaction details on Solana Explorer
Inspect transaction details on Solana Explorer
```rust theme={null}
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;
/// Increment the counter and manually commit the account in the Ephemeral Rollup session.
pub fn increment_and_commit(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
// Serialize the Anchor account before the CPI sees it
counter.exit(&crate::ID)?;
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(())
}
```
### Undelegating the PDA
Undelegating the PDA is done by building a `MagicIntentBundleBuilder` with the `commit_and_undelegate` intent.
This commits the latest state and returns ownership of the PDA to the owner program.
Inspect transaction details on Solana Explorer
Inspect transaction details on Solana Explorer
```rust theme={null}
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;
/// Undelegate the account from the delegation program
pub fn undelegate(ctx: Context) -> 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(())
}
```
## Connecting the React Client
The React client is a simple interface that allows you to interact with the Anchor program.
It uses the Anchor bindings to interact with the program and the MagicBlock SDK to interact with the Ephemeral Rollup session. Source lives alongside the program at [`anchor-counter/app`](https://github.com/magicblock-labs/magicblock-engine-examples/tree/main/anchor-counter/app).
Iframes only work with some wallets (e.g. Backpack). Alternatively, try the
deployed demo here: [https://counter-example.magicblock.app/](https://counter-example.magicblock.app/)
### Ephemeral Endpoint Configuration
To interact with the Ephemeral Rollup session, you need to configure the appropriate endpoint:
* For devnet, use the following ephemeral endpoint:
[https://devnet.magicblock.app](https://devnet.magicblock.app)
* For mainnet, please reach out to the MagicBlock team to receive the appropriate endpoint.
* For localhost, download, install, and run the ephemeral validator locally with the appropriate environment variables.
Make sure to update your client configuration to use the correct endpoint based on your development or production environment.
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Solana Devnet: [https://api.devnet.solana.com](https://api.devnet.solana.com)
ER Devnet (Asia): [https://devnet-as.magicblock.app](https://devnet-as.magicblock.app)
ER Devnet (EU): [https://devnet-eu.magicblock.app](https://devnet-eu.magicblock.app)
ER Devnet (US): [https://devnet-us.magicblock.app](https://devnet-us.magicblock.app)
TEE Devnet: [https://devnet-tee.magicblock.app/](https://devnet-tee.magicblock.app/)
Find out more details
here
.
# Local Development
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/how-to-guide/local-development
Run and test your Native Rust or Anchor programs with a fully local Ephemeral Rollup stack, Surfpool, or a local VRF oracle.
***
### Quick Access
Explore program and test scripts for both Anchor and Native Rust:
Anchor Implementation
Native Rust Implementation
***
## Local Setup Options
You can run Ephemeral Rollups locally in three ways:
* A fully local stack with `mb-test-validator` as the base layer and a local `ephemeral-validator`.
* A local Surfpool instance as the base layer alternative, while still running the rollup locally.
* A local `ephemeral-validator` connected directly to a public base layer such as Devnet.
Use the fully local path when you want everything on your machine. Use Surfpool when you want to keep the Surfpool workflow while testing against a local Ephemeral Rollup. Use the Devnet option when you want a local rollup process without running a local Solana validator.
### Important: upgrade your program with the correct validator identity
When using a local ER validator, connect it to the base layer where the accounts are delegated. If you delegate your PDA to a specific ER validator identity, update the delegation config in your program so commits and undelegations can complete correctly on the base layer.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
```bash theme={null}
npm install -g @magicblock-labs/ephemeral-validator@latest
```
`mb-test-validator` starts a local Solana validator you can use as the base layer for a fully local setup.
```bash theme={null}
mb-test-validator --reset
```
This setup uses `http://localhost:8899` for RPC and `ws://localhost:8900` for WebSocket connections.
```bash theme={null}
cargo build-sbf
solana config set --url localhost
solana program deploy YOUR_PROGRAM_PATH
```
```bash theme={null}
anchor build && anchor deploy \
--provider.cluster localnet
```
Connect the local Ephemeral Rollup to the local Solana validator:
```bash theme={null}
ephemeral-validator --remotes "http://localhost:8899" --remotes "ws://localhost:8900" -l "7799" --lifecycle ephemeral
```
The local rollup is exposed on `http://localhost:7799` for RPC and `ws://localhost:7800` for WebSocket connections.
```bash theme={null}
EPHEMERAL_PROVIDER_ENDPOINT=http://localhost:7799 \
EPHEMERAL_WS_ENDPOINT=ws://localhost:7800 \
PROVIDER_ENDPOINT=http://localhost:8899 \
WS_ENDPOINT=ws://localhost:8900 \
yarn test
```
```bash theme={null}
EPHEMERAL_PROVIDER_ENDPOINT="http://localhost:7799" \
EPHEMERAL_WS_ENDPOINT="ws://localhost:7800" \
anchor test \
--provider.cluster localnet \
--skip-local-validator \
--skip-build \
--skip-deploy
```
```bash theme={null}
curl -sL https://run.surfpool.run/ | bash
```
```bash theme={null}
npm install -g @magicblock-labs/ephemeral-validator@latest
```
This example keeps Surfpool local while using Solana Devnet as the upstream base layer:
```bash theme={null}
surfpool start --rpc-url https://api.devnet.solana.com
```
Surfpool exposes the local RPC and WebSocket endpoints that the ephemeral validator connects to.
Point the validator to Surfpool's local RPC and WebSocket endpoints:
```bash theme={null}
ephemeral-validator --remotes "http://localhost:8899" --remotes "ws://localhost:8900" -l "7799" --lifecycle ephemeral
```
```bash theme={null}
solana transfer 0 -u "http://localhost:7799"
```
The transaction should appear in the ER TUI, where you can inspect it and open it in the explorer.
This flow is based on [Running Ephemeral Rollups locally with Surfpool](https://x.com/PiccoGabriele/status/2030045550230524212).
Upgrade your program with MagicBlock delegation and deploy it to Devnet:
```bash theme={null}
cargo build-sbf
solana config set --url devnet
solana program deploy YOUR_PROGRAM_PATH
```
```bash theme={null}
anchor build && anchor deploy \
--provider.cluster devnet
```
```bash theme={null}
npm install -g @magicblock-labs/ephemeral-validator@latest
```
```bash theme={null}
RUST_LOG=info ephemeral-validator \
--lifecycle ephemeral \
--remote-url "https://rpc.magicblock.app/devnet" \
--rpc-port 7799
```
```bash theme={null}
EPHEMERAL_PROVIDER_ENDPOINT=http://localhost:7799 \
EPHEMERAL_WS_ENDPOINT=ws://localhost:7800 \
yarn test
```
```bash theme={null}
EPHEMERAL_PROVIDER_ENDPOINT="http://localhost:7799" \
EPHEMERAL_WS_ENDPOINT="ws://localhost:7800" \
anchor test \
--provider.cluster devnet \
--skip-local-validator \
--skip-build \
--skip-deploy
```
## Run the VRF Oracle Locally
If you also need to test VRF end to end, run a local `vrf-oracle` against a local test queue.
```bash theme={null}
npm install -g @magicblock-labs/ephemeral-validator@latest
```
```bash theme={null}
mb-test-validator --reset
```
```bash theme={null}
ephemeral-validator --remote-url "http://localhost:8899" --rpc-port 7799 --lifecycle ephemeral
```
This oracle adds requests to the local test queue:
```bash theme={null}
VRF_ORACLE_SKIP_PREFLIGHT="true" RPC_URL="http://localhost:8899" WEBSOCKET_URL="ws://localhost:8999" RUST_LOG=info vrf-oracle
```
If your local validator exposes a different WebSocket port, update `WEBSOCKET_URL` accordingly.
***
# Quickstart
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/how-to-guide/quickstart
Any Solana program can be upgraded with Ephemeral Rollups by adding delegation capabilities.
***
**Building with an AI coding agent?** Install the MagicBlock Dev Skill to give your agent MagicBlock-specific patterns — delegation flows, Magic Actions, cranks, VRF, and more.
**Hit an error?** Ask your coding agent with the skill installed, not the docs assistant. The assistant only sees the docs, so it cannot debug your code.
Quick install for Claude Code:
```bash theme={null}
npx skills add https://github.com/magicblock-labs/magicblock-dev-skill
```
Using Cursor, Codex, Windsurf, Cline, or another agent? See the [AI Dev Skill](/pages/overview/additional-information/ai-dev-skill) page for all install targets.
### Quick Access
Check out basic counter example:
Anchor Implementation
React Implementation
***
***
## Step-By-Step Guide
Build your program and upgrade it with delegation hooks with MagicBlock's Delegation Program `DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh`:
Write your program}>
Write your Solana program as you normally would.
Add delegation and undelegation hooks in your program
}
>
Add CPI hooks to delegate, commit and undelegate state accounts through
Ephemeral Rollup sessions.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Deploy your program on Solana.}>
Deploy your program directly on Solana using Anchor or Solana CLI.
Ready to execute transactions for delegation and real-time speed
}
>
Send transactions without modifications on-chain and off-chain that also
comply with the SVM RPC specification.
***
## Counter Example
The following software packages may be required, other versions may also be compatible:
| Software | Version | Installation Guide |
| ---------- | ------- | --------------------------------------------------------------- |
| **Solana** | 3.1.9 | [Install Solana](https://docs.anza.xyz/cli/install) |
| **Rust** | 1.89.0 | [Install Rust](https://www.rust-lang.org/tools/install) |
| **Anchor** | 1.0.2 | [Install Anchor](https://www.anchor-lang.com/docs/installation) |
| **Node** | 24.10.0 | [Install Node](https://nodejs.org/en/download/current) |
### Code Snippets
The program implements two main instructions:
1. `initialize`: Sets the counter to 0
2. `increment`: Increments the counter by 1
The program implements specific instructions for delegating and undelegating the counter:
1. `Delegate`: Delegates counter from Base Layer to ER (called on Base Layer)
2. `CommitAndUndelegate`: Schedules sync of counter from ER to Base Layer, and undelegates counter on ER (called on ER)
3. `Commit`: Schedules sync of counter from ER to Base Layer (called on ER)
4. `Undelegate`:
* Schedules sync and undelegation of counter (called on ER)
* Undelegation triggered through callback instruction injected through #\[ephemeral] (called on Base Layer through validator CPI)
The undelegation callback discriminator `[196, 28, 41, 206, 48, 37, 51, 167]`
and its instruction processor must be specified in your program. This
instruction triggered by Delegation Program reverts account ownership on the
Base Layer after calling undelegation on ER.
With [`[#ephemeral]`](/pages/ephemeral-rollups-ers/how-to-guide/quickstart#1-write-program) Anchor macro from MagicBlock's Ephemeral Rollup SDK, the undelegation callback discriminator and processor are injected into your program.
```rust theme={null}
#[ephemeral]
#[program]
pub mod public_counter {
use super::*;
/// Initialize the counter.
pub fn initialize(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = 0;
Ok(())
}
/// Increment the counter.
pub fn increment(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
}
/// Delegate the account to the delegation program
/// Set specific validator based on ER, see https://docs.magicblock.gg/pages/get-started/how-integrate-your-program/local-setup
pub fn delegate(ctx: Context) -> Result<()> {
// ...
}
/// Manually commit the counter state in the Ephemeral Rollup session.
pub fn commit(ctx: Context) -> Result<()> {
// ...
}
/// Increment the counter and commit in the same instruction.
pub fn increment_and_commit(ctx: Context) -> Result<()> {
// ...
}
/// Undelegate the account from the delegation program.
pub fn undelegate(ctx: Context) -> Result<()> {
// ...
}
}
pub const COUNTER_SEED: &[u8] = b"counter";
/// Context for initializing counter
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init_if_needed, payer = user, space = 8 + 8, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
/// Context for incrementing counter
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
}
/// Counter struct
#[account]
pub struct Counter {
pub count: u64,
}
/// Other context for delegation
```
Nothing special here, just a simple Anchor program that increments a counter. The only difference is that we're adding the `ephemeral` macro for undelegation and `delegate` macro to inject some useful logic to interact with the delegation program.
[⬆️ Back to Top](#code-snippets)
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
1. Add `ephemeral-rollups-sdk` with Anchor features to your program
```bash theme={null}
cargo add ephemeral-rollups-sdk --features anchor
```
Import `delegate`, `commit`, `ephemeral`, `DelegateConfig`, and `MagicIntentBundleBuilder` (which replaces the deprecated `commit_accounts` and `commit_and_undelegate_accounts` helpers):
```rust theme={null}
use ephemeral_rollups_sdk::anchor::{
commit,
delegate,
ephemeral
};
use ephemeral_rollups_sdk::cpi::DelegateConfig;
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;
```
2. Add `delegate` macro and instruction, `ephemeral` macro, and `undelegate` instruction to your program. Specify your preferred delegation config such as auto commits and specific ER validator:
```rust theme={null}
/// Add delegate function to the context
#[delegate]
#[derive(Accounts)]
pub struct DelegateInput<'info> {
pub payer: Signer<'info>,
/// CHECK: The pda to delegate
#[account(mut, del)]
pub pda: AccountInfo<'info>,
}
```
```rust theme={null}
/// Delegate the account to the delegation program
/// Set specific validator based on ER, see https://docs.magicblock.gg/pages/get-started/how-integrate-your-program/local-setup
pub fn delegate(ctx: Context) -> Result<()> {
ctx.accounts.delegate_pda(
&ctx.accounts.payer,
&[COUNTER_SEED],
DelegateConfig {
// Optionally set a specific validator from the first remaining account
validator: ctx.remaining_accounts.first().map(|acc| acc.key()),
..Default::default()
},
)?;
Ok(())
}
```
```rust theme={null}
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;
/// Manually commit the counter state in the Ephemeral Rollup session.
pub fn commit(ctx: Context) -> 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(())
}
/// Increment the counter and commit the new state in the same instruction.
pub fn increment_and_commit(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
// Serialize the Anchor account before the CPI sees it
counter.exit(&crate::ID)?;
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(())
}
```
```rust theme={null}
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;
/// Undelegate the account from the delegation program.
/// Commits the latest state and returns ownership of the PDA back to the owner program.
pub fn undelegate(ctx: Context) -> 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(())
}
```
> `Delegation` is the process of transferring ownership of one or more of your program's `PDAs` to the delegation program. Ephemeral Validators will then be able to use the `PDAs` to perform transactions in the SVM runtime.
> `Commit` is the process of updating the state of the `PDAs` from ER to the base layer. After the finalization process, the `PDAs` remain locked on base layer.
> `Undelegation` is the process of transferring ownership of the `PDAs` back to your program. On undelegation, the state is committed and it trigger the finalization process. Once state it validated, the `PDAs` are unlocked and can be used as normal on base layer.
[⬆️ Back to Top](#code-snippets)
Now you’re program is upgraded and ready! Fund your deployer wallet, then build and deploy to the
desired cluster:
```bash theme={null}
# Devnet: airdrop SOL to your configured keypair before deploying
solana airdrop 2 --url https://api.devnet.solana.com
anchor build && anchor deploy
```
[⬆️ Back to Top](#code-snippets)
Ready to execute transactions for delegation and real-time speed.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
```bash theme={null}
anchor test --skip-build --skip-deploy --skip-local-validator
```
Run the following test:
```typescript theme={null}
const COUNTER_SEED = "counter";
// Set Anchor providers
const provider = new anchor.AnchorProvider(
new anchor.web3.Connection(
process.env.PROVIDER_ENDPOINT || "https://api.devnet.solana.com",
{
wsEndpoint: process.env.PROVIDER_WS_ENDPOINT || undefined,
commitment: "confirmed",
},
),
anchor.Wallet.local(),
);
anchor.setProvider(provider);
const providerEphemeralRollup = new anchor.AnchorProvider(
new anchor.web3.Connection(
process.env.EPHEMERAL_PROVIDER_ENDPOINT ||
"https://devnet-as.magicblock.app/",
{
wsEndpoint:
process.env.EPHEMERAL_WS_ENDPOINT || "wss://devnet-as.magicblock.app/",
commitment: "confirmed",
},
),
anchor.Wallet.local(),
);
// Set program and PDA
const program = anchor.workspace.PublicCounter as Program;
const [counterPDA] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from(COUNTER_SEED)],
program.programId,
);
// Initialize counter on base layer
let initTx = await program.methods
.initialize()
.accounts({
user: provider.wallet.publicKey,
})
.transaction();
const initTxHash = await provider.sendAndConfirm(initTx, [
provider.wallet.payer,
]);
// Increment counter on base layer
let incBaseTx = await program.methods
.increment()
.accounts({
counter: counterPDA,
})
.transaction();
const incBaseTxHash = await provider.sendAndConfirm(incBaseTx, [
provider.wallet.payer,
]);
// Delegate counter to ER
// Pin a specific validator by passing it in remaining_accounts
const ER_VALIDATOR = new anchor.web3.PublicKey(
"MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57", // Asia ER validator
);
let delTx = await program.methods
.delegate()
.accounts({
payer: provider.wallet.publicKey,
pda: counterPDA,
})
.remainingAccounts([
{ pubkey: ER_VALIDATOR, isSigner: false, isWritable: false },
])
.transaction();
const delTxHash = await provider.sendAndConfirm(delTx, [
provider.wallet.payer,
]);
// Increment counter in real time on ER
let incErTx = await program.methods
.increment()
.accounts({
counter: counterPDA,
})
.transaction();
incErTx.feePayer = providerEphemeralRollup.wallet.publicKey;
incErTx.recentBlockhash = (
await providerEphemeralRollup.connection.getLatestBlockhash()
).blockhash;
incErTx = await providerEphemeralRollup.wallet.signTransaction(incErTx);
const incErTxHash = await providerEphemeralRollup.sendAndConfirm(incErTx);
// Commit and undelegate counter from ER back to base layer
let undelTx = await program.methods
.undelegate()
.accounts({
payer: providerEphemeralRollup.wallet.publicKey,
})
.transaction();
undelTx.feePayer = providerEphemeralRollup.wallet.publicKey;
undelTx.recentBlockhash = (
await providerEphemeralRollup.connection.getLatestBlockhash()
).blockhash;
undelTx = await providerEphemeralRollup.wallet.signTransaction(undelTx);
const undelTxHash = await providerEphemeralRollup.sendAndConfirm(undelTx);
```
To make it easier to integrate via the frontend, we created the [Magic Router](/pages/ephemeral-rollups-ers/introduction/magic-router). You send transactions directly to the magic router, and we can determine for you whether it should be routed to the [Ephemeral Rollup](/pages/ephemeral-rollups-ers/introduction/ephemeral-rollup) or base layer.
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Solana Devnet: [https://api.devnet.solana.com](https://api.devnet.solana.com)
ER Devnet (Asia): [https://devnet-as.magicblock.app](https://devnet-as.magicblock.app)
ER Devnet (EU): [https://devnet-eu.magicblock.app](https://devnet-eu.magicblock.app)
ER Devnet (US): [https://devnet-us.magicblock.app](https://devnet-us.magicblock.app)
TEE Devnet: [https://devnet-tee.magicblock.app/](https://devnet-tee.magicblock.app/)
Find out more details
here
.
[⬆️ Back to Top](#code-snippets)
***
### Advanced Code Snippets
When resizing a delegated PDA:
* PDA must have enough lamports to remain rent-exempt for the new account size.
* If additional lamports are needed, the **payer account must be delegated** to provide the difference.
* PDA must be owned by the program, and the transaction must include any signer(s) required for transferring lamports.
* Use `system_instruction::allocate`
```rust theme={null}
#[account]
pub struct Counter {
pub count: u64,
pub extra_data: Vec,
}
#[derive(Accounts)]
pub struct ResizeCounter<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
// Resize the counter (e.g., to store more extra_data)
pub fn resize_counter(ctx: Context, new_size: usize) -> Result<()> {
let account_to_resize = &mut ctx.accounts.counter.to_account_info();
let payer = &mut ctx.accounts.payer.to_account_info();
// Calculate rent-exemption for the new size
let rent = Rent::get()?;
let min_balance = rent.minimum_balance(new_size);
// Top up lamports if needed
let current_lamports = **account_to_resize.lamports.borrow();
if current_lamports < min_balance {
let to_transfer = min_balance - current_lamports;
**payer.try_borrow_mut_lamports()? -= to_transfer;
**account_to_resize.try_borrow_mut_lamports()? += to_transfer;
}
// Resize account
account_to_resize.resize(new_size)?;
Ok(())
}
```
[⬆️ Back to Top](#advanced-code-snippets)
Initialize connection with Magic Router before you send transactions dynamically.
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Choose your preferred SDK to initialize, send and confirm transactions:
* `ephemeral-rollups-kit` for `@solana/kit`
* `ephemeral-rollups-sdk` for `@solana/web.js`
```typescript Kit theme={null}
import { Connection } from "@magicblock-labs/ephemeral-rollups-kit";
// Initialize connection
const connection = await Connection.create(
"https://devnet-router.magicblock.app",
"wss://devnet-router.magicblock.app"
);
// ... create transaction
// Send and confirm transaction
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
import { sendAndConfirmTransaction } from "@solana/web3.js";
import { ConnectionMagicRouter } from "@magicblock-labs/ephemeral-rollups-sdk";
// Initialize connection
const connection = new ConnectionMagicRouter(
"https://devnet-router.magicblock.app/",
{ wsEndpoint: "wss://devnet-router.magicblock.app/" }
);
// ... create transaction
// Send and confirm transaction
const txHash = await sendAndConfirmTransaction(connection, tx, [payer], {
skipPreflight: true,
commitment: "confirmed",
});
```
[Learn more about Magic Router](/pages/ephemeral-rollups-ers/introduction/magic-router)
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
Explore reference implementation on GitHub
Attach one or more instructions that run automatically on the Solana base layer immediately after an Ephemeral Rollup
(ER) commit.
[Learn more about Magic Action](/pages/ephemeral-rollups-ers/magic-actions/overview)
### 1) Create action instruction
The instruction `update_leaderboard` runs on the base layer immediately after the commit lands. The `#[action]` attribute on its accounts context marks it as callable from a post-commit action.
`#[action]` makes the instruction **callable from** a post-commit action — it
does not make it callable **only** that way. The handler is an ordinary
base-layer instruction, so anyone can invoke it directly with a wallet.
Address, `seeds`, and `owner` constraints only pin *which* accounts are passed;
they do not authenticate *who* called it. Any handler that moves value or
changes authoritative state must verify the injected `escrow` signer — see
[Authenticate the caller](/pages/ephemeral-rollups-ers/magic-actions/troubleshooting#security-authenticate-the-caller).
```rust theme={null}
// program instruction
pub fn update_leaderboard(ctx: Context) -> Result<()> {
let leaderboard = &mut ctx.accounts.leaderboard;
let counter_info = &mut ctx.accounts.counter.to_account_info();
let mut data: &[u8] = &counter_info.try_borrow_data()?;
let counter = Counter::try_deserialize(&mut data)?;
if counter.count > leaderboard.high_score {
leaderboard.high_score = counter.count;
}
msg!(
"Leaderboard updated! High score: {}",
leaderboard.high_score
);
Ok(())
}
// instruction context
#[action]
#[derive(Accounts)]
pub struct UpdateLeaderboard<'info> {
#[account(mut, seeds = [LEADERBOARD_SEED], bump)]
pub leaderboard: Account<'info, Leaderboard>,
/// CHECK: PDA owner depends on: 1) Delegated: Delegation Program; 2) Undelegated: Your program ID
pub counter: UncheckedAccount<'info>,
}
```
### 2) Build the commit instruction with the action
The commit instruction `commit_and_update_leaderboard` runs on the ER. It uses `MagicIntentBundleBuilder` to schedule both the commit and the post-commit action onto `magic_context` — both are applied together when the ER transaction is sealed back to the base layer.
```rust theme={null}
// commit action instruction on ER
pub fn commit_and_update_leaderboard(ctx: Context) -> Result<()> {
// Build the post-commit action that updates the leaderboard on base layer
let instruction_data =
anchor_lang::InstructionData::data(&crate::instruction::UpdateLeaderboard {});
let action_args = ActionArgs::new(instruction_data);
let action_accounts = vec![
ShortAccountMeta {
pubkey: ctx.accounts.leaderboard.key(),
is_writable: true,
},
ShortAccountMeta {
pubkey: ctx.accounts.counter.key(),
is_writable: false,
},
];
let action = CallHandler {
destination_program: crate::ID,
accounts: action_accounts,
args: action_args,
// Signer that pays transaction fees for the action from its escrow PDA
escrow_authority: ctx.accounts.payer.to_account_info(),
compute_units: 200_000,
};
// Schedule commit + post-commit action on magic_context
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()])
.add_post_commit_actions([action])
.build_and_invoke()?;
Ok(())
}
// commit action context on ER
#[commit]
#[derive(Accounts)]
pub struct CommitAndUpdateLeaderboard<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
/// CHECK: Leaderboard PDA - not mut here, writable set in handler
#[account(seeds = [LEADERBOARD_SEED], bump)]
pub leaderboard: UncheckedAccount<'info>,
/// CHECK: Your program ID
pub program_id: AccountInfo<'info>,
}
```
### Execute multiple actions
You can commit multiple accounts and chain several actions in one call. Actions execute sequentially in the order they're passed to `add_post_commit_actions`.
```rust theme={null}
// Chain several actions — they execute sequentially on base layer after the commit lands.
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(),
// ... additional committed accounts
])
.add_post_commit_actions([action_1, action_2, action_3])
.build_and_invoke()?;
```
### Undelegate with actions
Actions can also be chained onto an undelegation — the counter commits, undelegates, and the actions run, all atomically in one ER transaction.
```rust theme={null}
// Commit, undelegate, AND execute actions — all atomically on base layer after the ER transaction seals.
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()])
.add_post_commit_actions([action])
.build_and_invoke()?;
```
[⬆️ Back to Top](#advanced-code-snippets)
Top up a delegated account's lamports on the ER side. The transaction is submitted on the **base layer** and uses the Ephemeral SPL Token program to shuttle lamports to the destination's delegated balance via a single-use lamports PDA.
Common use case: keeping a delegated fee payer funded for a long session. Without a fee payer, an account stops after 10 commits. With a fee payer and `magic_fee_vault`, it can keep committing, and the payer starts paying live commit fees on commit 26.
Notes:
* Generate a fresh 32-byte salt per top-up via `crypto.getRandomValues` — re-using a salt collides with an existing PDA.
* Submit to the base-layer RPC, not the ER.
* The destination must already be delegated.
```typescript theme={null}
import {
Connection,
Keypair,
PublicKey,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
lamportsDelegatedTransferIx,
deriveLamportsPda,
} from "@magicblock-labs/ephemeral-rollups-sdk";
/**
* Top up a delegated account with lamports.
*
* The transaction is submitted on the BASE LAYER. The Ephemeral SPL Token
* program creates a single-use lamports PDA, funds it from the payer, and
* delegates it so the ER credits the destination's delegated balance.
*/
async function topUpDelegatedAccount(
connection: Connection, // base-layer connection
payer: Keypair,
destination: PublicKey, // delegated account to top up
amountLamports: bigint,
) {
// Generate a fresh 32-byte salt per top-up.
// Re-using a salt collides with an existing lamports PDA and the call fails.
const salt = crypto.getRandomValues(new Uint8Array(32));
const [lamportsPda] = deriveLamportsPda(payer.publicKey, destination, salt);
const ix = await lamportsDelegatedTransferIx(
payer.publicKey,
destination,
amountLamports,
salt,
);
const tx = new Transaction().add(ix);
tx.feePayer = payer.publicKey;
// CRITICAL: send to the base-layer RPC, not the ER.
const sig = await sendAndConfirmTransaction(connection, tx, [payer], {
commitment: "confirmed",
skipPreflight: true,
});
return { sig, lamportsPda };
}
```
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
On-Curve Delegation
Required signers for delegating an on-curve account:
1. On-curve account to be delegated
2. Fee payer
Required instructions for delegating on-curve accounts:
1. Assign System Account to Delegation Program
2. Delegate to Delegation Program
```typescript Kit theme={null}
// Create assign instruction
// The on-curve account must sign this instruction to change its owner
const accountSigner = await cryptoKeyPairToTransactionSigner(userKeypair);
const delegationProgramAddress = address(DELEGATION_PROGRAM_ID.toString());
const assignInstruction = getAssignInstruction({
account: accountSigner,
programAddress: delegationProgramAddress,
});
// Create delegate instruction
const delegateInstruction = await createDelegateInstruction({
payer: feePayerAddress,
delegatedAccount: userAddress,
ownerProgram: ownerProgramAddress,
validator: validatorAddress,
});
// Prepare transaction
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(feePayerAddress, tx),
(tx) =>
appendTransactionMessageInstructions(
[assignInstruction, delegateInstruction],
tx
)
);
// Send and confirm transaction (fee payer need to sign, on-curve account cannot be signer since delegated)
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair, feePayerKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// Create assign instruction
const assignInstruction = SystemProgram.assign({
accountPubkey: userPubkey,
programId: DELEGATION_PROGRAM_ID,
});
// Create delegate instruction
const delegateInstruction = createDelegateInstruction({
payer: feePayerKeypair.publicKey,
delegatedAccount: userPubkey,
ownerProgram: ownerProgram,
validator: validator,
});
// Create and send transaction (fee payer need to sign, on-curve account cannot be signer since delegated)
const tx = new Transaction().add(assignInstruction, delegateInstruction);
tx.feePayer = feePayerKeypair.publicKey;
const txSignature = await sendAndConfirmTransaction(
connectionBaseLayer,
tx,
[userKeypair, feePayerKeypair],
{
skipPreflight: true,
}
);
```
Direct commit and undelegate through Magic Program only.
```typescript Kit theme={null}
// Create commit and undelegate instruction
const commitAndUndelegateInstruction = createCommitAndUndelegateInstruction(
userAddress,
[userAddress]
);
// Prepare transaction
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(feePayerAddress, tx),
(tx) =>
appendTransactionMessageInstructions([commitAndUndelegateInstruction], tx)
);
// Send and confirm transaction on ephemeral connection
const txHash = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair, feePayerKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// Create commit and undelegate instruction
const commitAndUndelegateInstruction = createCommitAndUndelegateInstruction(
userPubkey,
[userPubkey]
);
// Send and confirm transaction on ephemeral connection
const tx = new Transaction().add(commitAndUndelegateInstruction);
tx.feePayer = feePayerKeypair.publicKey;
const txSignature = await sendAndConfirmTransaction(
ephemeralConnection,
tx,
[userKeypair, feePayerKeypair],
{
skipPreflight: true,
}
);
```
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
Explore reference implementation on GitHub
Attach instructions to a delegation that the ER validator runs automatically
inside the rollup, right after the account is delegated — no extra transaction:
* Build the action(s) as standard `Instruction`s and convert them to the
compact payload with `.cleartext()` (public) — encrypted actions are built
off-chain by a client holding the validator key.
* The base-layer delegation program stores the payload in the delegation
record; the ER validator executes it once the account lands in the rollup.
* CPI with `delegate_account_with_actions` instead of the plain `delegate_pda`
helper; the `#[delegate]` macro still provides the buffer/record/metadata
accounts.
```rust theme={null}
/// Reuse the same accounts context as a normal delegation
#[delegate]
#[derive(Accounts)]
pub struct DelegateInput<'info> {
pub payer: Signer<'info>,
/// CHECK: The pda to delegate
#[account(mut, del)]
pub pda: AccountInfo<'info>,
}
```
```rust theme={null}
use anchor_lang::solana_program::instruction::{AccountMeta, Instruction};
use anchor_lang::InstructionData;
use ephemeral_rollups_sdk::cpi::{
delegate_account_with_actions, DelegateAccounts, DelegateConfig,
};
use ephemeral_rollups_sdk::dlp_api::compact::ClearText;
/// Delegate the account AND attach a post-delegation action. The action is stored
/// in the delegation record on the base layer and executed automatically by the ER
/// validator inside the rollup, right after the account is delegated — no extra
/// transaction. Here the action is a self-CPI back into `increment`.
pub fn delegate_with_actions(ctx: Context) -> Result<()> {
let counter_key = ctx.accounts.pda.key();
// The instruction the ER validator runs post-delegation, inside the rollup.
let increment_action = Instruction {
program_id: crate::ID,
accounts: vec![AccountMeta::new(counter_key, false)],
data: crate::instruction::Increment {}.data(),
};
// Convert to the compact, cleartext post-delegation actions payload.
// (Use `cleartext` for public actions; encrypted actions are built off-chain
// by a client that holds the validator key.)
let actions = vec![increment_action].cleartext();
let payer = ctx.accounts.payer.to_account_info();
let pda = ctx.accounts.pda.to_account_info();
let delegate_accounts = DelegateAccounts {
payer: &payer,
pda: &pda,
owner_program: &ctx.accounts.owner_program,
buffer: &ctx.accounts.buffer_pda,
delegation_record: &ctx.accounts.delegation_record_pda,
delegation_metadata: &ctx.accounts.delegation_metadata_pda,
delegation_program: &ctx.accounts.delegation_program,
system_program: &ctx.accounts.system_program,
};
delegate_account_with_actions(
delegate_accounts,
&[COUNTER_SEED],
DelegateConfig {
// Optionally set a specific validator from the first remaining account
validator: ctx.remaining_accounts.first().map(|acc| acc.key()),
..Default::default()
},
actions,
// No extra signers are required by the increment action.
&[],
)?;
Ok(())
}
```
[⬆️ Back to Top](#advanced-code-snippets)
***
### Quick Access
Learn more about private ER, Rust Native implementation, and local development:
Quickstart
Quickstart
Local Development
***
## Solana Explorer
Get insights about your transactions and accounts on Solana:
Official Solana Explorer
Explore Solana Blockchain
## Solana RPC Providers
Send transactions and requests through existing RPC providers:
Free Public Nodes
Free Shared Nodes
Dedicated High-Performance Nodes
## Solana Validator Dashboard
Find real-time updates on Solana's validator infrastructure:
Get Validator Insights
Discover Validator Metrics
## Server Status
Subscribe to Solana's and MagicBlock's server status:
Subscribe to Solana Server Updates
Subscribe to MagicBlock Server Status
***
## MagicBlock Products
Execute real-time, zero-fee transactions securely on Solana.
Protect sensitive data with compliance — built on top of Ephemeral Rollups.
Move SPL tokens at rollup speed — public or private transfers, swaps, and private payments for trading and DeFi apps.
Combine real-time execution, session keys, token custody, price feeds, automation, and settlement.
Add provably fair onchain randomness to games, raffles, and real-time apps.
Access low-latency onchain price feeds for trading and DeFi.
***
# Solana Program
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/how-to-guide/rust-program
Learn how to write a simple Rust program that delegates and increments a counter on Solana
**Building with an AI coding agent?** Install the MagicBlock Dev Skill to give your agent MagicBlock-specific patterns — delegation flows, Magic Actions, cranks, VRF, and more.
**Hit an error?** Ask your coding agent with the skill installed, not the docs assistant. The assistant only sees the docs, so it cannot debug your code.
Quick install for Claude Code:
```bash theme={null}
npx skills add https://github.com/magicblock-labs/magicblock-dev-skill
```
Using Cursor, Codex, Windsurf, Cline, or another agent? See the [AI Dev Skill](/pages/overview/additional-information/ai-dev-skill) page for all install targets.
***
### Quick Access
Check out basic counter example in other implementations:
Native Rust Implementation
Pinocchio Implementation
Local Development
***
***
## Step-By-Step Guide
Build your program and upgrade it with delegation hooks with MagicBlock’s Delegation Program `DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh`:
Write program and add delegation instructions
}
>
Write your Solana program as you normally would.
Delegate PDA on Base Layer}>
Add CPI hooks to delegate, commit and undelegate state accounts through
Ephemeral Rollup sessions.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Commit PDA on ER}>
Deploy your program directly on Solana using Solana CLI.
Undelegate PDA on ER}>
Send transactions without modifications on-chain and off-chain that also
comply with the SVM RPC specification.
***
## Counter Example
The following software packages may be required, other versions may also be compatible:
| Software | Version | Installation Guide |
| ---------- | ------- | ------------------------------------------------------- |
| **Solana** | 3.1.9 | [Install Solana](https://docs.anza.xyz/cli/install) |
| **Rust** | 1.89.0 | [Install Rust](https://www.rust-lang.org/tools/install) |
| **Node** | 24.10.0 | [Install Node](https://nodejs.org/en/download/current) |
### Code Snippets
The program implements two main instructions:
1. `InitializeCounter`: Initialize and sets the counter to 0 (called on Base Layer)
2. `IncreaseCounter`: Increments the initialized counter by X amount (called on Base Layer or ER)
The program implements specific instructions for delegating and undelegating the counter:
1. `Delegate`: Delegates counter from Base Layer to ER (called on Base Layer)
2. `CommitAndUndelegate`: Schedules sync of counter from ER to Base Layer, and undelegates counter on ER (called on ER)
3. `Commit`: Schedules sync of counter from ER to Base Layer (called on ER)
4. `Undelegate`: Undelegates counter on the Base Layer (called on Base Layer through validator CPI)
5. `IncrementAndCommit`: Increments and commits in a single ER transaction (called on ER)
6. `IncrementAndUndelegate`: Increments, commits, and undelegates in a single ER transaction (called on ER)
The undelegation callback discriminator `[196, 28, 41, 206, 48, 37, 51, 167]`
and its instruction processor must be specified in your program. This
instruction triggered by Delegation Program reverts account ownership on the
Base Layer after calling undelegation on ER.
With [`[#ephemeral]`](/pages/ephemeral-rollups-ers/how-to-guide/quickstart#1-write-program) Anchor macro from MagicBlock's Ephemeral Rollup SDK, the undelegation callback discriminator and processor are injected into your program.
Here's the core structure of our program:
```rust theme={null}
use borsh::BorshDeserialize;
use solana_program::program_error::ProgramError;
pub enum ProgramInstruction {
InitializeCounter,
IncreaseCounter { increase_by: u64 },
Delegate,
CommitAndUndelegate,
Commit,
Undelegate { pda_seeds: Vec> },
IncrementAndCommit { increase_by: u64 },
IncrementAndUndelegate { increase_by: u64 },
}
#[derive(BorshDeserialize)]
struct IncreaseCounterPayload {
increase_by: u64,
}
impl ProgramInstruction {
pub fn unpack(input: &[u8]) -> Result {
// Ensure the input has at least 8 bytes for the variant
if input.len() < 8 {
return Err(ProgramError::InvalidInstructionData);
}
// Extract the first 8 bytes as variant
let (ix_discriminator, rest) = input.split_at(8);
// Match instruction discriminator with process and deserialize payload
Ok(match ix_discriminator {
[0, 0, 0, 0, 0, 0, 0, 0] => Self::InitializeCounter,
[1, 0, 0, 0, 0, 0, 0, 0] => {
let payload = IncreaseCounterPayload::try_from_slice(rest)?;
Self::IncreaseCounter {
increase_by: payload.increase_by,
}
}
[2, 0, 0, 0, 0, 0, 0, 0] => Self::Delegate,
[3, 0, 0, 0, 0, 0, 0, 0] => Self::CommitAndUndelegate,
[4, 0, 0, 0, 0, 0, 0, 0] => Self::Commit,
[5, 0, 0, 0, 0, 0, 0, 0] => {
let payload = IncreaseCounterPayload::try_from_slice(rest)?;
Self::IncrementAndCommit {
increase_by: payload.increase_by,
}
}
[6, 0, 0, 0, 0, 0, 0, 0] => {
let payload = IncreaseCounterPayload::try_from_slice(rest)?;
Self::IncrementAndUndelegate {
increase_by: payload.increase_by,
}
}
[196, 28, 41, 206, 48, 37, 51, 167] => {
let pda_seeds: Vec> = Vec::>::try_from_slice(rest)?;
Self::Undelegate { pda_seeds }
}
_ => return Err(ProgramError::InvalidInstructionData),
})
}
}
```
Your "Undelegate" instruction must have the exact discriminator. It is never
called by you, instead the validator on the Base Layer will callback with a
CPI into your program after undelegating your account on ER.
[⬆️ Back to Top](#code-snippets)
### Delegating the Counter PDA
In order to delegate the counter PDA, and make it writable in an Ephemeral Rollup session, we need to add an instruction which
internally calls the `delegate_account` function. `delegate_account` will CPI to the delegation program, which upon validation will gain ownership of the account.
After this step, an ephemeral validator can start processing transactions on the counter PDA and propose state diff trough the delegation program.
Inspect transactions details on Solana Explorer
```rust theme={null}
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
pubkey::Pubkey,
};
use ephemeral_rollups_sdk::cpi::{delegate_account, DelegateAccounts, DelegateConfig};
// For Base Layer only
// Set specific validator based on ER, see https://docs.magicblock.gg/pages/get-started/how-integrate-your-program/local-setup
pub fn process_delegate(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
// Get accounts
let account_info_iter = &mut accounts.iter();
let initializer = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
let pda_to_delegate = next_account_info(account_info_iter)?;
let owner_program = next_account_info(account_info_iter)?;
let delegation_buffer = next_account_info(account_info_iter)?;
let delegation_record = next_account_info(account_info_iter)?;
let delegation_metadata = next_account_info(account_info_iter)?;
let delegation_program = next_account_info(account_info_iter)?;
let validator_account = account_info_iter.next();
// Optional: client-provided validator or default validator
let validator_pubkey: Option = validator_account.map(|acc_info| acc_info.key.clone());
// Prepare counter pda seeds
let seed_1 = b"counter";
let seed_2 = initializer.key.as_ref();
let pda_seeds: &[&[u8]] = &[seed_1, seed_2];
let delegate_accounts = DelegateAccounts {
payer: initializer,
pda: pda_to_delegate,
owner_program,
buffer: delegation_buffer,
delegation_record,
delegation_metadata,
delegation_program,
system_program,
};
let delegate_config = DelegateConfig {
validator: validator_pubkey, // Set delegating ER validator
..Default::default()
};
delegate_account(delegate_accounts, pda_seeds, delegate_config)?;
Ok(())
}
```
[⬆️ Back to Top](#code-snippets)
### Committing while the PDA is delegated
The ephemeral runtime allows committing the state of the PDA while it is delegated. This is done by building a `MagicIntentBundleBuilder` with the `commit` intent.
Inspect transaction details on Solana Explorer
Inspect transaction details on Solana Explorer
```rust theme={null}
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
use ephemeral_rollups_sdk::ephem::{FoldableIntentBuilder, MagicIntentBundleBuilder};
// For ER only
pub fn process_commit(
_program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
// Get accounts
let account_info_iter = &mut accounts.iter();
let initializer = next_account_info(account_info_iter)?;
let counter_account = next_account_info(account_info_iter)?;
let magic_program = next_account_info(account_info_iter)?;
let magic_context = next_account_info(account_info_iter)?;
// Signer should be the same as the initializer
if !initializer.is_signer {
msg!("Initializer {} should be the signer", initializer.key);
return Err(ProgramError::MissingRequiredSignature);
}
MagicIntentBundleBuilder::new(
initializer.clone(),
magic_context.clone(),
magic_program.clone(),
)
.commit(&[counter_account.clone()])
.build_and_invoke()?;
Ok(())
}
```
[⬆️ Back to Top](#code-snippets)
### Undelegating the PDA
Undelegating the PDA is done by building a `MagicIntentBundleBuilder` with the `commit_and_undelegate` intent as part of some instruction.
This commits the latest state and returns ownership of the PDA to the owner program. After undelegating and finalizing the state, the validator will create a callback CPI into `undelegate` on the base layer.
The undelegation callback discriminator `[196, 28, 41, 206, 48, 37, 51, 167]`
and its instruction processor must be specified in your program. This
instruction triggered by Delegation Program reverts account ownership on the
Base Layer after calling undelegation on ER.
With [`[#ephemeral]`](/pages/ephemeral-rollups-ers/how-to-guide/quickstart#1-write-program) Anchor macro from MagicBlock's Ephemeral Rollup SDK, the undelegation callback discriminator and processor are injected into your program.
Inspect transaction details on Solana Explorer
Inspect transaction details on Solana Explorer
```rust theme={null}
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
use ephemeral_rollups_sdk::cpi::undelegate_account;
use ephemeral_rollups_sdk::ephem::{FoldableIntentBuilder, MagicIntentBundleBuilder};
// For ER only
pub fn process_commit_and_undelegate(
_program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
// Get accounts
let account_info_iter = &mut accounts.iter();
let initializer = next_account_info(account_info_iter)?;
let counter_account = next_account_info(account_info_iter)?;
let magic_program = next_account_info(account_info_iter)?;
let magic_context = next_account_info(account_info_iter)?;
// Signer should be the same as the initializer
if !initializer.is_signer {
msg!("Initializer {} should be the signer", initializer.key);
return Err(ProgramError::MissingRequiredSignature);
}
// Commit and undelegate counter_account on ER
MagicIntentBundleBuilder::new(
initializer.clone(),
magic_context.clone(),
magic_program.clone(),
)
.commit_and_undelegate(&[counter_account.clone()])
.build_and_invoke()?;
Ok(())
}
// For Base Layer CPI callback
pub fn process_undelegate(
program_id: &Pubkey,
accounts: &[AccountInfo],
pda_seeds: Vec>,
) -> ProgramResult {
// Get accounts
let account_info_iter = &mut accounts.iter();
let delegated_pda = next_account_info(account_info_iter)?;
let delegation_buffer = next_account_info(account_info_iter)?;
let initializer = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// CPI on Solana
undelegate_account(
delegated_pda,
program_id,
delegation_buffer,
initializer,
system_program,
pda_seeds,
)?;
Ok(())
}
```
[⬆️ Back to Top](#code-snippets)
***
### Advanced Code Snippets
When resizing a delegated PDA:
* PDA must have enough lamports to remain rent-exempt for the new account size.
* If additional lamports are needed, the **payer account must be delegated** to provide the difference.
* PDA must be owned by the program, and the transaction must include any signer(s) required for transferring lamports.
* Use `system_instruction::allocate`
```rust theme={null}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct Counter {
pub count: u64,
}
// Resize counter account
pub fn resize_counter_account(
counter_acc: &AccountInfo,
payer: &AccountInfo,
program_id: &Pubkey,
new_size: usize,
bump: u8,
) -> ProgramResult {
let rent = Rent::get()?;
let lamports_required = rent.minimum_balance(new_size);
let current_lamports = counter_acc.lamports();
if lamports_required > current_lamports {
let lamports_to_add = lamports_required - current_lamports;
invoke_signed(
&system_instruction::transfer(
&payer.key,
&counter_acc.key,
lamports_to_add,
),
&[payer.clone(), counter_acc.clone()],
&[&[COUNTER_SEED, &[bump]]],
)?;
}
// Allocate new size
invoke_signed(
&system_instruction::allocate(&counter_acc.key, new_size as u64),
&[counter_acc.clone()],
&[&[COUNTER_SEED, &[bump]]],
)?;
// Assign back to program
invoke_signed(
&system_instruction::assign(&counter_acc.key, program_id),
&[counter_acc.clone()],
&[&[COUNTER_SEED, &[bump]]],
)?;
msg!("Counter account resized to {} bytes", new_size);
Ok(())
}
```
[⬆️ Back to Top](#advanced-code-snippets)
Initialize connection with Magic Router before you send transactions dynamically.
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Choose your preferred SDK to initialize, send and confirm transactions:
* `ephemeral-rollups-kit` for `@solana/kit`
* `ephemeral-rollups-sdk` for `@solana/web.js`
```typescript Kit theme={null}
import { Connection } from "@magicblock-labs/ephemeral-rollups-kit";
// Initialize connection
const connection = await Connection.create(
"https://devnet-router.magicblock.app",
"wss://devnet-router.magicblock.app"
);
// ... create transaction
// Send and confirm transaction
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
import { sendAndConfirmTransaction } from "@solana/web3.js";
import { ConnectionMagicRouter } from "@magicblock-labs/ephemeral-rollups-sdk";
// Initialize connection
const connection = new ConnectionMagicRouter(
"https://devnet-router.magicblock.app/",
{ wsEndpoint: "wss://devnet-router.magicblock.app/" }
);
// ... create transaction
// Send and confirm transaction
const txHash = await sendAndConfirmTransaction(connection, tx, [payer], {
skipPreflight: true,
commitment: "confirmed",
});
```
[Learn more about Magic Router](/pages/ephemeral-rollups-ers/introduction/magic-router)
[⬆️ Back to Top](#advanced-code-snippets)
Attach one or more instructions that run automatically on the Solana base
layer immediately after an ER commit, scheduled with `MagicIntentBundleBuilder`.
[Learn more about Magic Action](/pages/ephemeral-rollups-ers/magic-actions/overview)
```rust theme={null}
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
};
use ephemeral_rollups_sdk::ephem::{CallHandler, MagicIntentBundleBuilder};
use ephemeral_rollups_sdk::{ActionArgs, ShortAccountMeta};
// Runs on the ER. Commits the counter to the base layer and attaches a
// post-commit action (`update_leaderboard`) that the base layer executes
// automatically once the commit lands — both applied in one ER transaction.
pub fn process_commit_with_action(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let payer = next_account_info(account_info_iter)?;
let counter = next_account_info(account_info_iter)?;
let leaderboard = next_account_info(account_info_iter)?;
let magic_context = next_account_info(account_info_iter)?;
let magic_program = next_account_info(account_info_iter)?;
// The instruction that runs on the base layer after the commit.
// `args` is your program's own instruction payload (e.g. update_leaderboard).
let action = CallHandler {
destination_program: *program_id,
accounts: vec![
ShortAccountMeta { pubkey: *leaderboard.key, is_writable: true },
ShortAccountMeta { pubkey: *counter.key, is_writable: false },
],
args: ActionArgs::new(update_leaderboard_instruction_data()),
// Signer that pays the action's fees from its escrow PDA.
escrow_authority: payer.clone(),
compute_units: 200_000,
};
// Schedule the commit + post-commit action on magic_context.
MagicIntentBundleBuilder::new(
payer.clone(),
magic_context.clone(),
magic_program.clone(),
)
.commit(&[counter.clone()])
.add_post_commit_actions([action])
.build_and_invoke()?;
Ok(())
}
```
[⬆️ Back to Top](#advanced-code-snippets)
Top up a delegated account's lamports on the ER side. The transaction is submitted on the **base layer** and uses the Ephemeral SPL Token program to shuttle lamports to the destination's delegated balance via a single-use lamports PDA.
Common use case: keeping a delegated fee payer funded for a long session. Without a fee payer, an account stops after 10 commits. With a fee payer and `magic_fee_vault`, it can keep committing, and the payer starts paying live commit fees on commit 26.
Notes:
* Generate a fresh 32-byte salt per top-up via `crypto.getRandomValues` — re-using a salt collides with an existing PDA.
* Submit to the base-layer RPC, not the ER.
* The destination must already be delegated.
```typescript theme={null}
import {
Connection,
Keypair,
PublicKey,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
lamportsDelegatedTransferIx,
deriveLamportsPda,
} from "@magicblock-labs/ephemeral-rollups-sdk";
/**
* Top up a delegated account with lamports.
*
* The transaction is submitted on the BASE LAYER. The Ephemeral SPL Token
* program creates a single-use lamports PDA, funds it from the payer, and
* delegates it so the ER credits the destination's delegated balance.
*/
async function topUpDelegatedAccount(
connection: Connection, // base-layer connection
payer: Keypair,
destination: PublicKey, // delegated account to top up
amountLamports: bigint,
) {
// Generate a fresh 32-byte salt per top-up.
// Re-using a salt collides with an existing lamports PDA and the call fails.
const salt = crypto.getRandomValues(new Uint8Array(32));
const [lamportsPda] = deriveLamportsPda(payer.publicKey, destination, salt);
const ix = await lamportsDelegatedTransferIx(
payer.publicKey,
destination,
amountLamports,
salt,
);
const tx = new Transaction().add(ix);
tx.feePayer = payer.publicKey;
// CRITICAL: send to the base-layer RPC, not the ER.
const sig = await sendAndConfirmTransaction(connection, tx, [payer], {
commitment: "confirmed",
skipPreflight: true,
});
return { sig, lamportsPda };
}
```
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
On-Curve Delegation
Required signers for delegating an on-curve account:
1. On-curve account to be delegated
2. Fee payer
Required instructions for delegating on-curve accounts:
1. Assign System Account to Delegation Program
2. Delegate to Delegation Program
```typescript Kit theme={null}
// Create assign instruction
// The on-curve account must sign this instruction to change its owner
const accountSigner = await cryptoKeyPairToTransactionSigner(userKeypair);
const delegationProgramAddress = address(DELEGATION_PROGRAM_ID.toString());
const assignInstruction = getAssignInstruction({
account: accountSigner,
programAddress: delegationProgramAddress,
});
// Create delegate instruction
const delegateInstruction = await createDelegateInstruction({
payer: feePayerAddress,
delegatedAccount: userAddress,
ownerProgram: ownerProgramAddress,
validator: validatorAddress,
});
// Prepare transaction
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(feePayerAddress, tx),
(tx) =>
appendTransactionMessageInstructions(
[assignInstruction, delegateInstruction],
tx
)
);
// Send and confirm transaction (fee payer need to sign, on-curve account cannot be signer since delegated)
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair, feePayerKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// Create assign instruction
const assignInstruction = SystemProgram.assign({
accountPubkey: userPubkey,
programId: DELEGATION_PROGRAM_ID,
});
// Create delegate instruction
const delegateInstruction = createDelegateInstruction({
payer: feePayerKeypair.publicKey,
delegatedAccount: userPubkey,
ownerProgram: ownerProgram,
validator: validator,
});
// Create and send transaction (fee payer need to sign, on-curve account cannot be signer since delegated)
const tx = new Transaction().add(assignInstruction, delegateInstruction);
tx.feePayer = feePayerKeypair.publicKey;
const txSignature = await sendAndConfirmTransaction(
connectionBaseLayer,
tx,
[userKeypair, feePayerKeypair],
{
skipPreflight: true,
}
);
```
Direct commit and undelegate through Magic Program only.
```typescript Kit theme={null}
// Create commit and undelegate instruction
const commitAndUndelegateInstruction = createCommitAndUndelegateInstruction(
userAddress,
[userAddress]
);
// Prepare transaction
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(feePayerAddress, tx),
(tx) =>
appendTransactionMessageInstructions([commitAndUndelegateInstruction], tx)
);
// Send and confirm transaction on ephemeral connection
const txHash = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair, feePayerKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// Create commit and undelegate instruction
const commitAndUndelegateInstruction = createCommitAndUndelegateInstruction(
userPubkey,
[userPubkey]
);
// Send and confirm transaction on ephemeral connection
const tx = new Transaction().add(commitAndUndelegateInstruction);
tx.feePayer = feePayerKeypair.publicKey;
const txSignature = await sendAndConfirmTransaction(
ephemeralConnection,
tx,
[userKeypair, feePayerKeypair],
{
skipPreflight: true,
}
);
```
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
Explore reference implementation on GitHub
Attach instructions to a delegation that the ER validator runs automatically
inside the rollup, right after the account is delegated — no extra transaction:
* Build the action(s) as standard `Instruction`s and convert them to the
compact payload with `.cleartext()` (public) — encrypted actions are built
off-chain by a client holding the validator key.
* The base-layer delegation program stores the payload in the delegation
record; the ER validator executes it once the account lands in the rollup.
* CPI with `delegate_account_with_actions` (same `DelegateAccounts` /
`DelegateConfig` as a normal delegation, plus the actions payload).
```rust theme={null}
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use ephemeral_rollups_sdk::cpi::{
delegate_account_with_actions, DelegateAccounts, DelegateConfig,
};
use ephemeral_rollups_sdk::dlp_api::compact::ClearText;
// Delegate a PDA AND attach a post-delegation action. The action is stored in the
// delegation record on the base layer and executed automatically by the ER
// validator inside the rollup, right after the account is delegated — no extra
// transaction. Here the action is a self-CPI back into the program's increment.
pub fn process_delegate_with_actions(
_program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let initializer = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
let pda_to_delegate = next_account_info(account_info_iter)?;
let owner_program = next_account_info(account_info_iter)?;
let delegation_buffer = next_account_info(account_info_iter)?;
let delegation_record = next_account_info(account_info_iter)?;
let delegation_metadata = next_account_info(account_info_iter)?;
let delegation_program = next_account_info(account_info_iter)?;
let validator_account = account_info_iter.next();
let pda_seeds: &[&[u8]] = &[b"counter", initializer.key.as_ref()];
// The instruction the ER validator runs post-delegation, inside the rollup.
// `data` is your program's own instruction payload (e.g. the increment ix).
let increment_action = Instruction {
program_id: *owner_program.key,
accounts: vec![AccountMeta::new(*pda_to_delegate.key, false)],
data: increment_instruction_data(),
};
// Convert to the compact, cleartext post-delegation actions payload.
// (Use `cleartext` for public actions; encrypted actions are built off-chain
// by a client that holds the validator key.)
let actions = vec![increment_action].cleartext();
let delegate_accounts = DelegateAccounts {
payer: initializer,
pda: pda_to_delegate,
owner_program,
buffer: delegation_buffer,
delegation_record,
delegation_metadata,
delegation_program,
system_program,
};
let delegate_config = DelegateConfig {
validator: validator_account.map(|acc| *acc.key),
..Default::default()
};
delegate_account_with_actions(
delegate_accounts,
pda_seeds,
delegate_config,
actions,
// No extra signers are required by the increment action.
&[],
)?;
Ok(())
}
```
[⬆️ Back to Top](#advanced-code-snippets)
***
## Solana Explorer
Get insights about your transactions and accounts on Solana:
Official Solana Explorer
Explore Solana Blockchain
## Solana RPC Providers
Send transactions and requests through existing RPC providers:
Free Public Nodes
Free Shared Nodes
Dedicated High-Performance Nodes
## Solana Validator Dashboard
Find real-time updates on Solana's validator infrastructure:
Get Validator Insights
Discover Validator Metrics
## Server Status
Subscribe to Solana's and MagicBlock's server status:
Subscribe to Solana Server Updates
Subscribe to MagicBlock Server Status
***
# Test Your Program
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/how-to-guide/rust-tests
Learn how to test a simple Rust program
***
### Quick Access
If you prefer to dive straight into the code:
Tests for Rust Counter
Tests for Pinocchio Counter
Local Development
***
## Step-By-Step Guide
Build valid transactions that calls your program instructions for delegation and undelegation.
The complete test for this project can be found in the [Typescript Test Script](https://github.com/magicblock-labs/magicblock-engine-examples/tree/main/counter/native-rust/tests).
Import SDK and initialize Connection}>
Set up connection and accounts
Delegate PDA on Base Layer}>
Test CPI hook to delegate state account on Base Layer
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Commit PDA on ER}>
Test CPI hook to commit state account on ER
Undelegate PDA on ER}>
Test CPI hook to undelegate state account on ER
***
## Counter Example
The following software packages may be required, other versions may also be compatible:
| Software | Version | Installation Guide |
| ---------- | ------- | ------------------------------------------------------- |
| **Solana** | 3.1.9 | [Install Solana](https://docs.anza.xyz/cli/install) |
| **Rust** | 1.89.0 | [Install Rust](https://www.rust-lang.org/tools/install) |
| **Node** | 24.10.0 | [Install Node](https://nodejs.org/en/download/current) |
### Code Snippets
### Import SDK and create Connection
Import relevant libraries `@magicblock-labs/ephemeral-rollups-sdk` or `@magicblock-labs/ephemeral-rollups-kit`. Initialize connection before testing and sending transactions.
```bash kit theme={null}
yarn add @magicblock-labs/ephemeral-rollups-kit@latest
```
```bash web3.js theme={null}
yarn add @magicblock-labs/ephemeral-rollups-sdk@latest
```
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Solana Devnet: [https://api.devnet.solana.com](https://api.devnet.solana.com)
ER Devnet (Asia): [https://devnet-as.magicblock.app](https://devnet-as.magicblock.app)
ER Devnet (EU): [https://devnet-eu.magicblock.app](https://devnet-eu.magicblock.app)
ER Devnet (US): [https://devnet-us.magicblock.app](https://devnet-us.magicblock.app)
TEE Devnet: [https://devnet-tee.magicblock.app/](https://devnet-tee.magicblock.app/)
Find out more details
here
.
```typescript Kit theme={null}
import {
Instruction,
getAddressEncoder,
getProgramDerivedAddress,
AccountRole,
createKeyPairFromBytes,
getAddressFromPublicKey,
address,
createTransactionMessage,
appendTransactionMessageInstructions,
pipe,
setTransactionMessageFeePayer,
} from "@solana/kit";
import {
Connection,
DELEGATION_PROGRAM_ID,
delegationRecordPdaFromDelegatedAccount,
delegationMetadataPdaFromDelegatedAccount,
delegateBufferPdaFromDelegatedAccountAndOwnerProgram,
MAGIC_CONTEXT_ID,
MAGIC_PROGRAM_ID,
} from "@magicblock-labs/ephemeral-rollups-kit";
// Set up a base and ephemeral connection (alternatively use router, see Magic Router)
const connection = await Connection.create(
process.env.PROVIDER_ENDPOINT || "https://api.devnet.solana.com",
process.env.WS_ENDPOINT || "wss://api.devnet.solana.com"
);
const ephemeralConnection = await Connection.create(
process.env.EPHEMERAL_PROVIDER_ENDPOINT || "https://devnet-as.magicblock.app",
process.env.EPHEMERAL_WS_ENDPOINT || "wss://devnet-as.magicblock.app"
);
// Prepare user
const userKeypair = await initializeSolSignerKeypair();
const userPubkey = await getAddressFromPublicKey(userKeypair.publicKey);
// Get PDA
const addressEncoder = getAddressEncoder();
const [counterPda, bump] = await getProgramDerivedAddress({
programAddress: PROGRAM_ID,
seeds: [Buffer.from("counter"), addressEncoder.encode(userPubkey)],
});
```
```typescript Web3.js theme={null}
import {
Keypair,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
Connection,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
DELEGATION_PROGRAM_ID,
delegationRecordPdaFromDelegatedAccount,
delegationMetadataPdaFromDelegatedAccount,
delegateBufferPdaFromDelegatedAccountAndOwnerProgram,
MAGIC_CONTEXT_ID,
MAGIC_PROGRAM_ID,
GetCommitmentSignature,
} from "@magicblock-labs/ephemeral-rollups-sdk";
// Set up a base and ephemeral connection (alternatively use router, see Magic Router)
const connectionBaseLayer = new Connection(
process.env.PROVIDER_ENDPOINT || "https://api.devnet.solana.com",
{ wsEndpoint: process.env.WS_ENDPOINT || "wss://api.devnet.solana.com" }
);
const connectionEphemeralRollup = new Connection(
process.env.EPHEMERAL_PROVIDER_ENDPOINT ||
"https://devnet-as.magicblock.app/",
{
wsEndpoint:
process.env.EPHEMERAL_WS_ENDPOINT || "wss://devnet-as.magicblock.app/",
}
);
// Create user keypair and airdrop SOL if needed
const userKeypair = initializeSolSignerKeypair();
// Get pda
let [counterPda, bump] = PublicKey.findProgramAddressSync(
[Buffer.from("counter"), userKeypair.publicKey.toBuffer()],
PROGRAM_ID
);
```
[⬆️ Back to Top](#code-snippets)
### Test `delegation` transaction
Create a instruction with the right order and attributes of accounts, and the instruction discriminator for `delegation` of your program. Send the transaction with the instruction to Base Layer (Solana) network.
Inspect transactions details on Solana Explorer
```typescript Kit theme={null}
// "Delegate" transaction
// Add local validator identity to the remaining accounts if running on localnet
const remainingAccounts =
connection.clusterUrlHttp.includes("localhost") ||
connection.clusterUrlHttp.includes("127.0.0.1")
? [
{
address: address("mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev"),
role: AccountRole.READONLY,
},
]
: [];
const accounts = [
{ address: userPubkey, role: AccountRole.WRITABLE_SIGNER },
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
{ address: counterPda, role: AccountRole.WRITABLE },
{ address: PROGRAM_ID, role: AccountRole.READONLY },
{
address: await delegateBufferPdaFromDelegatedAccountAndOwnerProgram(
counterPda,
PROGRAM_ID
),
role: AccountRole.WRITABLE,
},
{
address: await delegationRecordPdaFromDelegatedAccount(counterPda),
role: AccountRole.WRITABLE,
},
{
address: await delegationMetadataPdaFromDelegatedAccount(counterPda),
role: AccountRole.WRITABLE,
},
{ address: DELEGATION_PROGRAM_ID, role: AccountRole.READONLY },
...remainingAccounts,
];
const serializedInstructionData = Buffer.from(
CounterInstruction.Delegate,
"hex"
);
const delegateIx: Instruction = {
accounts,
programAddress: PROGRAM_ID,
data: serializedInstructionData,
};
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(userPubkey, tx),
(tx) => appendTransactionMessageInstructions([delegateIx], tx)
);
// Send and confirm transaction on base layer
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// "Delegate" transaction
// Add local validator identity to the remaining accounts if running on localnet
const remainingAccounts =
connectionEphemeralRollup.rpcEndpoint.includes("localhost") ||
connectionEphemeralRollup.rpcEndpoint.includes("127.0.0.1")
? [
{
pubkey: new PublicKey("mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev"),
isSigner: false,
isWritable: false,
},
]
: [];
const tx = new web3.Transaction();
const keys = [
// Initializer
{
pubkey: userKeypair.publicKey,
isSigner: true,
isWritable: true,
},
// System Program
{
pubkey: web3.SystemProgram.programId,
isSigner: false,
isWritable: false,
},
// Counter Account
{
pubkey: counterPda,
isSigner: false,
isWritable: true,
},
// Owner Program
{
pubkey: PROGRAM_ID,
isSigner: false,
isWritable: false,
},
// Delegation Buffer
{
pubkey: getDelegationBufferPda(counterPda, PROGRAM_ID),
isSigner: false,
isWritable: true,
},
// Delegation Record
{
pubkey: getDelegationRecordPda(counterPda),
isSigner: false,
isWritable: true,
},
// Delegation Metadata
{
pubkey: getDelegationMetadataPda(counterPda),
isSigner: false,
isWritable: true,
},
// Delegation Program
{
pubkey: DELEGATION_PROGRAM_ID,
isSigner: false,
isWritable: false,
},
// ER Validator
...remainingAccounts,
];
const serializedInstructionData = Buffer.from(
CounterInstruction.Delegate,
"hex"
);
const delegateIx = new web3.TransactionInstruction({
keys: keys,
programId: PROGRAM_ID,
data: serializedInstructionData,
});
tx.add(delegateIx);
// Send and confirm transaction to Base Layer
const txHash = await sendAndConfirmTransaction(
connectionBaseLayer,
tx,
[userKeypair],
{
skipPreflight: true,
commitment: "confirmed",
}
);
```
[⬆️ Back to Top](#code-snippets)
### Test `commit` transaction
Create a instruction with the right order and attributes of accounts, and the instruction discriminator for `commit` of your program. Send the transaction with the instruction to ER network.
Inspect transaction details on Solana Explorer
Inspect transaction details on Solana Explorer
```typescript Kit theme={null}
// "Commit" transaction
const accounts = [
{ address: userPubkey, role: AccountRole.WRITABLE_SIGNER },
{ address: counterPda, role: AccountRole.WRITABLE },
{ address: address(MAGIC_PROGRAM_ID.toString()), role: AccountRole.READONLY },
{ address: address(MAGIC_CONTEXT_ID.toString()), role: AccountRole.WRITABLE },
];
const serializedInstructionData = Buffer.from(CounterInstruction.Commit, "hex");
const commitIx: Instruction = {
accounts,
programAddress: PROGRAM_ID,
data: serializedInstructionData,
};
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(userPubkey, tx),
(tx) => appendTransactionMessageInstructions([commitIx], tx)
);
// Send and confirm transaction on ER
const txHash = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// "Commit" transaction
const tx = new web3.Transaction();
const keys = [
// Initializer
{
pubkey: userKeypair.publicKey,
isSigner: true,
isWritable: true,
},
// Counter Account
{
pubkey: counterPda,
isSigner: false,
isWritable: true,
},
// Magic Program
{
pubkey: MAGIC_PROGRAM_ID,
isSigner: false,
isWritable: false,
},
// Magic Context
{
pubkey: MAGIC_CONTEXT_ID,
isSigner: false,
isWritable: true,
},
];
const serializedInstructionData = Buffer.from(CounterInstruction.Commit, "hex");
const commitIx = new web3.TransactionInstruction({
keys: keys,
programId: PROGRAM_ID,
data: serializedInstructionData,
});
tx.add(commitIx);
// Send and confirm transaction to ER
const txHash = await sendAndConfirmTransaction(
connectionEphemeralRollup,
tx,
[userKeypair],
{
skipPreflight: true,
commitment: "confirmed",
}
);
```
[⬆️ Back to Top](#code-snippets)
### Test `undelegation` transaction
Create a instruction with the right order and attributes of accounts, and the instruction discriminator for `undelegation` of your program. Send the transaction with the instruction to ER network.
Inspect transaction details on Solana Explorer
Inspect transaction details on Solana Explorer
```typescript Kit theme={null}
// "Undelegate" transaction
const accounts = [
{ address: userPubkey, role: AccountRole.WRITABLE_SIGNER },
{ address: counterPda, role: AccountRole.WRITABLE },
{ address: address(MAGIC_PROGRAM_ID.toString()), role: AccountRole.READONLY },
{ address: address(MAGIC_CONTEXT_ID.toString()), role: AccountRole.WRITABLE },
];
const serializedInstructionData = Buffer.from(
CounterInstruction.CommitAndUndelegate,
"hex"
);
const undelegateIx: Instruction = {
accounts,
programAddress: PROGRAM_ID,
data: serializedInstructionData,
};
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(userPubkey, tx),
(tx) => appendTransactionMessageInstructions([undelegateIx], tx)
);
// Send and confirm transaction on ER
const txHash = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// "Undelegate" transaction
const tx = new web3.Transaction();
const keys = [
// Initializer
{
pubkey: userKeypair.publicKey,
isSigner: true,
isWritable: true,
},
// Counter Account
{
pubkey: counterPda,
isSigner: false,
isWritable: true,
},
// Magic Program
{
pubkey: MAGIC_PROGRAM_ID,
isSigner: false,
isWritable: false,
},
// Magic Context
{
pubkey: MAGIC_CONTEXT_ID,
isSigner: false,
isWritable: true,
},
];
const serializedInstructionData = Buffer.from(
CounterInstruction.CommitAndUndelegate,
"hex"
);
const undelegateIx = new web3.TransactionInstruction({
keys: keys,
programId: PROGRAM_ID,
data: serializedInstructionData,
});
tx.add(undelegateIx);
// Send and confirm transaction to ER. Afterwards CPI callback will be triggered to "Undelegate" instruction of your program on the Base Layer.
const txHash = await sendAndConfirmTransaction(
connectionEphemeralRollup,
tx,
[userKeypair],
{
skipPreflight: true,
commitment: "confirmed",
}
);
```
[⬆️ Back to Top](#code-snippets)
***
## Solana Explorer
Get insights about your transactions and accounts on Solana:
Official Solana Explorer
Explore Solana Blockchain
## Solana RPC Providers
Send transactions and requests through existing RPC providers:
Free Public Nodes
Free Shared Nodes
Dedicated High-Performance Nodes
## Solana Validator Dashboard
Find real-time updates on Solana's validator infrastructure:
Get Validator Insights
Discover Validator Metrics
## Server Status
Subscribe to Solana's and MagicBlock's server status:
Subscribe to Solana Server Updates
Subscribe to MagicBlock Server Status
***
# Ephemeral Accounts
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/introduction/ephemeral-accounts
Create, resize, and close accounts that live entirely on the Ephemeral Rollup.
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(...)]`:
| Marker | Purpose |
| --------- | ------------------------------------------------------- |
| `sponsor` | Marks the account that pays rent for ephemeral accounts |
| `eph` | Marks 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:
| Method | Signature | Description |
| --------------------------------------- | ----------------------------------- | --------------------------------------- |
| `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
```rust theme={null}
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
```rust theme={null}
use ephemeral_rollups_sdk::anchor::ephemeral_accounts;
pub fn create_conversation(ctx: Context) -> 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
```rust theme={null}
pub fn extend_conversation(
ctx: Context,
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
```rust theme={null}
pub fn close_conversation(ctx: Context) -> 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:
```rust theme={null}
#[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:
```typescript theme={null}
// 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.
The sponsor account needs lamports on the ER to pay ephemeral rent. It must be delegated before creating ephemeral accounts.
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
Full example program on GitHub
How delegation and state synchronization work
Build your first program with Ephemeral Rollups
# Delegation, Commitment & Undelegation
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/introduction/ephemeral-rollup
Execute Transactions, Synchronize States, all in real-time.
[Magicblock's Ephemeral Rollup](/pages/overview/additional-information/whitepaper) **leverages the Solana Virtual Machine (SVM)’s account-based structure and parallel execution** to optimize state management. By structuring the state into **clusters**, users can **lock one or multiple accounts** and temporarily shift state execution to a **dedicated auxiliary layer**— "Ephemeral Rollup (ER)". A dynamic fraud-proof mechanism enables fast state finalization through a decentralized Security Committee, see [whitepaper](/public/Ephemeral_Rollups_Fraud_Proof.pdf).
***
## Account Lifecycle for executing transactions in real-time with ER:
State accounts must be delegated to an specific ER validator first by
changing the account owner to the [Delegation
Program](https://github.com/magicblock-labs/delegation-program)
`DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh` and specifying parameters
like ER validator, account lifetime and synchronization frequency.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Delegated state accounts are updated in real-time with transactions on the
ER directly or via [Magic
Router](/pages/ephemeral-rollups-ers/introduction/magic-router). The
**initial transaction on ER clones the delegated account** from base layer
to the ephemeral rollup.
The operator commits the ephemeral state to the base layer **periodically or
on-demand**, including new state and relevant pointers. The account state is
finalized using a fraud-proof mechanism as detailed in the paper.
Delegated account states can continuously be updated in real-time on the ER
directly or via [Magic
Router](/pages/ephemeral-rollups-ers/introduction/magic-router).
Delegated account states are committed through ER validator to the base
layer and the account owner are reversed from the [Delegation
Program](https://github.com/magicblock-labs/delegation-program)
`DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh` to the original owner.
# FAQ
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/introduction/faq
Execute Transactions, Synchronize States, all in real-time.
**No.** Delegated account must exist on Solana beforehand. Delegated accounts are cloned after requesting the account on ER or submitting a transaction with the delegated account.
**No.** Program accounts are never delegated. Only state accounts can be delegated, while **program accounts are cloned** when a transaction is submitted on ER and subscribed for updates thereafter.
**Yes.** Delegated accounts can benefit from all Solana programs and accounts. Every account on Solana is readable on ER, while only delegated account can be changed on ER within an atomic transaction.
**Mostly yes.** The ER runs the SVM, so the default **200k CU per instruction**, the **1.4M CU per transaction** cap (via `SetComputeUnitLimit`), and the **10 MiB max account size** all match Solana. The exception is transaction size: the ER accepts serialized transactions up to **64 KB**, versus 1,232 bytes on the base layer. See [Runtime Limits](/pages/ephemeral-rollups-ers/introduction/runtime-limits).
**No.** As of 2026-08-05, Solana uses a **400 ms slot time**, while MagicBlock’s Execution Runtime (ER) operates with a **10 ms slot time**. Developers may be tempted to rely on slot time as a fixed-duration measure; however, **slot times are not guaranteed and may change over time**. Code or logic that depends on a specific slot duration should therefore be written with caution.
# Fees, Commits, and Refunds
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/introduction/fees-and-commit-economics
A plain-language guide to ER fees, commit limits, delegation deposits, and refunds
This page explains what your app pays when it delegates an account to an Ephemeral Rollup (ER),
commits that account back to Solana, and later undelegates it.
## The short version
There are two separate fee systems:
1. **A deposit on Solana.** You fund this deposit when you delegate an account. When you undelegate,
MagicBlock takes the session and commit charges from it and returns anything left over.
2. **A live commit limit inside the ER.** Without a delegated fee payer, an account can commit 10
times. For a longer session, add a delegated fee payer and `magic_fee_vault`. This removes the
10-commit stop. The fee payer starts paying an extra live fee on commit 26.
These systems work together. A commit with no immediate fee can still be charged against the Solana
deposit when the account is undelegated.
The values on this page were checked against the source on August 20, 2026.
## Current prices
One SOL contains 1 billion lamports.
| What you pay for | Price | When you pay |
| :----------------------------------- | -------------------------------------: | :------------------------------------------------ |
| Normal ER transaction | `0` in the current release | No charge |
| One delegation session | `300,000` lamports (`0.0003 SOL`) | Taken from the Solana deposit when you undelegate |
| Commits after the first | `100,000` lamports (`0.0001 SOL`) each | Taken from the Solana deposit when you undelegate |
| Live commits starting with commit 26 | `100,000` lamports per account | Taken immediately from the delegated fee payer |
| Base Actions | Based on the requested compute units | Taken immediately from the delegated fee payer |
| Adding a callback | `5,000` lamports | Taken immediately from the delegated fee payer |
| Temporary Ephemeral Account storage | Based on account size; refundable | Reserved when the account is created or grows |
Normal ER transactions cost `0` in the current release. Solana transaction fees are separate from
the prices above.
## 1. The deposit you fund on Solana
Delegating an account creates two small Solana accounts: a **delegation record** and **delegation
metadata**. The delegation payer funds both accounts so they are rent-exempt.
Think of that money as a refundable balance, not a one-time fee that buys unlimited commits.
When you undelegate, MagicBlock calculates the charge:
```text theme={null}
session charge = 300,000 lamports
commit charge = 100,000 lamports for each commit after commit 1
total charge = session charge + commit charge
```
MagicBlock takes no more than the amount held in the two deposit accounts. It sends the unused amount
back to the wallet recorded as the `rent_payer` when the account was delegated.
In practical terms:
* even a session with no commit can use up to `300,000` lamports from the deposit;
* commit 1 adds no commit charge;
* commits 2, 3, 4, and so on add `100,000` lamports each;
* if the calculated charge is larger than the deposit, MagicBlock takes the deposit but does not
create a debt or fail undelegation;
* any money left in the deposit is refunded.
The exact deposit size varies because it depends on Solana rent, the account's stored seeds, and any
actions included with the delegation.
## 2. Committing without a fee payer
* Commits 1 through 10 are accepted.
* Commit 11 fails with custom error `0xA0000000`.
* The accepted commits are still included in the deposit charge when you undelegate.
A final **commit-and-undelegate** can still run after the limit so the account is not trapped inside
the ER. It does not allow more normal commits.
## 3. Committing with a fee payer
For a longer session, provide:
* a delegated account that will pay the fees; and
* the `magic_fee_vault` that belongs to the ER validator you are using.
This path does not stop after 10 commits. Instead:
* commits 1 through 25 have no **extra live commit fee**;
* commit 26 is the first live commit fee;
* commit 26 and every later commit cost `100,000` lamports per committed account.
Live fees are taken from the delegated fee payer when the bundle is scheduled. Deposit charges are
calculated separately when the account is undelegated, so your app may pay both.
If one bundle commits several accounts, MagicBlock checks each account separately. For example, a
bundle that contains two accounts on commit 26 costs `200,000` lamports in live commit fees.
If the fee payer cannot cover the full charge, the instruction fails with `InsufficientFunds`. No
partial payment is taken.
The committed account and the fee payer do not have to be the same account. If your app pays for
users, set spending limits and rate limits so one user cannot drain the shared payer.
## Worked examples
The examples below do not include normal Solana transaction fees. `D` means the deposit balance just
before undelegation.
### One commit, then undelegate
```text theme={null}
calculated deposit charge = 300,000 lamports
amount taken = the smaller of D and 300,000
refund = D - amount taken
live commit fee = 0
```
### Ten simple commits, then undelegate
```text theme={null}
session charge = 300,000
commit charge = 9 * 100,000
calculated deposit charge = 1,200,000 lamports
amount taken = the smaller of D and 1,200,000
refund = D - amount taken
live commit fee = 0
```
An 11th simple commit fails before it is scheduled.
### Twenty-six commits with a delegated fee payer
```text theme={null}
calculated deposit charge at undelegation = 300,000 + (25 * 100,000)
= 2,800,000 lamports
amount taken from deposit = the smaller of D and 2,800,000
live fee taken on commit 26 = 100,000 lamports
refund = D - amount taken from deposit
```
## Keeping the fee payer funded
Use `lamportsDelegatedTransferIx` to add lamports to a delegated fee payer. You submit the top-up
transaction on Solana, and the Ephemeral SPL Token program moves the balance into the ER.
See the [top-up example](/pages/ephemeral-rollups-ers/how-to-guide/quickstart#top-up-delegated-account).
Include these costs in your budget:
* the lamports you are transferring;
* the top-up helper's current `300,000`-lamport setup charge;
* the normal Solana transaction fee.
The top-up setup charge and the `300,000`-lamport delegation session charge are different charges.
They currently happen to have the same value.
## Base Actions and callbacks
A Base Action is an instruction that the ER asks MagicBlock to execute on Solana after a commit. On
the fee-payer path, its price depends on the compute units requested by the action:
```text theme={null}
price = round up(requested compute units * 50,000 / 1,000,000) lamports
```
For example:
* one action requesting `200,000` compute units costs `10,000` lamports;
* two such actions cost `20,000` lamports.
Adding a callback costs another `5,000` lamports. The callback's requested compute units are not
included in the Base Action calculation above.
## Refundable Ephemeral Account storage
Ephemeral Accounts exist only inside the ER and never commit to Solana. They use a separate,
refundable storage balance:
```text theme={null}
storage balance = (account data bytes + 60) * 32 lamports
```
The sponsor provides this balance when the account is created or grows. The corresponding amount is
returned to the sponsor when the account shrinks or closes.
For example:
* an account with no data reserves `1,920` lamports;
* an account with 1,000 bytes of data reserves `33,920` lamports.
See [Ephemeral Accounts](/pages/ephemeral-rollups-ers/introduction/ephemeral-accounts) for the full
account lifecycle.
## Other balance movements that are not app fees
When a commit changes an account's lamport balance, MagicBlock must make the ER balance and Solana
balance match. Lamports may move between the account and the validator during this settlement. This
is balance reconciliation, not an extra fixed or percentage fee.
The older two-step commit path also uses temporary Solana accounts. The validator funds them and gets
the unused balance back after finalization. These are validator operating costs, not additional app
fees.
Validators also pay Solana priority fees when submitting commit transactions. Those operator costs
are separate from the Base Action price charged to an app.
If a delegated account has no data and no lamports, the Delegation Program may add `890,880` lamports
to keep it rent-exempt. That money funds the account; it is not protocol revenue.
## Common errors
| What happened | Result |
| :--------------------------------------------------------- | :------------------------------------------------------- |
| You try a normal 11th commit without the fee-payer path | Custom error `0xA0000000` |
| MagicBlock cannot find the account's commit number | Custom error `0xA0000001` |
| The validator's `magic_fee_vault` is missing or incorrect | `MissingAccount` |
| The fee vault is not writable and delegated | `IllegalOwner` |
| The delegated fee payer cannot cover the whole charge | `InsufficientFunds` |
| An Ephemeral Account sponsor cannot fund storage | `InsufficientFunds` |
| The Solana deposit is smaller than the undelegation charge | MagicBlock takes at most the deposit; no debt is created |
## Where the fees go
When the deposit is settled, about 10% of the collected amount goes to the protocol fee vault and
about 90% goes to the validator fee vault. Small rounding differences are possible.
When a validator later withdraws its accumulated fees, 10% of that withdrawal goes to the protocol
and the validator receives the rest. Validators wait until their vault balance is above
`100,000,000` lamports before automatically withdrawing. This threshold is only for batching; it is
not an app fee.
## Verify the current numbers
The fee rules are split between two repositories. Check both when verifying production behavior:
| Repository | What to check |
| :---------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------- |
| [Delegation Program](https://github.com/magicblock-labs/delegation-program/tree/6898ef4b82ba1f2b6fbb5d91eca578729edbbeb8) | Fee constants, deposit settlement, refunds, and validator fee withdrawals |
| [MagicBlock validator](https://github.com/magicblock-labs/magicblock-validator/tree/cec4cf574ace267029e9487b61780d5218256b42) | Commit limits, live fees, Base Action charges, callback charges, and Ephemeral Account storage |
# Magic Router
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/introduction/magic-router
Learn how Magicblock's Magic Router accelerates transactions by routing them to the right execution environment.
***
## Dynamic Transaction Routing
**Magicblock's Magic Router** is an dynamic transaction routing engine that accelerates transactions by intelligently deciding where they should be executed — either on **Ephemeral Rollups** or **Solana** — based on transaction metadata.
This eliminates the need for manual routing logic from the developer, providing significant benefits in transaction speed and development experience.
* ✅ **Simple Integration with Single Endpoint**: Just connect to a single RPC endpoint.
* ✅ **Seamless Wallet Experience**: Connect, sign, and submit — no need to know what’s happening behind the scenes.
* ✅ **Faster Confirmations**: Magic Router routes to the fastest available endpoint.
***
## Quick Access
Integrate with an Anchor program
Integrate with a Native Rust program
Experiment with Magic Router API
***
## Code Snippets
Initialize connection with Magic Router before you send transactions dynamically.
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Choose your preferred SDK to initialize, send and confirm transactions:
* `ephemeral-rollups-kit` for `@solana/kit`
* `ephemeral-rollups-sdk` for `@solana/web.js`
```typescript Kit theme={null}
import { Connection } from "@magicblock-labs/ephemeral-rollups-kit";
// Initialize connection
const connection = await Connection.create(
"https://devnet-router.magicblock.app",
"wss://devnet-router.magicblock.app"
);
// ... create transaction
// Send and confirm transaction
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
import { sendAndConfirmTransaction } from "@solana/web3.js";
import { ConnectionMagicRouter } from "@magicblock-labs/ephemeral-rollups-sdk";
// Initialize connection
const connection = new ConnectionMagicRouter(
"https://devnet-router.magicblock.app/",
{ wsEndpoint: "wss://devnet-router.magicblock.app/" }
);
// ... create transaction
// Send and confirm transaction
const txHash = await sendAndConfirmTransaction(connection, tx, [payer], {
skipPreflight: true,
commitment: "confirmed",
});
```
The Magic Router analyzes each transaction’s metadata (e.g. writable accounts, owner, and signer) and automatically routes it to the closest dedicated endpoint:
1. **Client - Transaction Submission** The dApp or user sends a transaction to the
Magic Router RPC endpoint.
2. **RPC - Metadata Inspection**
The Magic Router inspects the transaction metadata and checks the owner of writable accounts.
3. **Validator - Smart Routing and Execution**
Based on the metadata, the router determines whether to send it to:
* **Ephemeral Rollup** for fast, low-latency, zero-cost execution
* **Solana** for persistent, high-throughput execution
***
# Runtime Limits
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/introduction/runtime-limits
Compute, transaction size, and account size limits on the Ephemeral Rollup compared to Solana
The Ephemeral Rollup (ER) runs the **Solana Virtual Machine (SVM)**, so programs execute under the same runtime rules as on the base layer. The key difference is the transaction size limit, which the ER raises significantly.
| Limit | Solana Base Layer | Ephemeral Rollup |
| :------------------------------------------------------------- | :---------------- | :--------------- |
| Compute units per instruction (default) | 200,000 CU | 200,000 CU |
| Compute units per transaction (max, via `SetComputeUnitLimit`) | 1,400,000 CU | 1,400,000 CU |
| Serialized transaction size (max) | 1,232 bytes | **64 KB** |
| Account size (max) | 10 MiB | 10 MiB |
| Slot time | \~400 ms | \~10 ms |
Slot times are not guaranteed and may change over time. Avoid writing logic
that depends on a specific slot duration — see the
[FAQ](/pages/ephemeral-rollups-ers/introduction/faq).
## Requesting more compute
As on Solana, the default compute budget is 200,000 CU per instruction. Request a higher limit (up to 1.4M CU per transaction) by prepending a `SetComputeUnitLimit` instruction:
```typescript theme={null}
import { ComputeBudgetProgram, Transaction } from "@solana/web3.js";
const tx = new Transaction().add(
ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }),
yourProgramInstruction
);
```
## Larger transactions
The ER accepts serialized transactions up to **64 KB**, compared to the 1,232-byte packet limit on the base layer. This removes the need for address lookup tables or transaction splitting in most cases, and allows instructions with many accounts or large instruction data to execute in a single transaction.
The 64 KB limit only applies to transactions executed on the ER (i.e. all
writable accounts are delegated). Transactions routed to the base layer —
including delegation and undelegation transactions — remain subject to the
1,232-byte Solana limit.
# Ephemeral Rollup
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/introduction/why
The high-performance engine for real-time applications on Solana
[MagicBlock' Ephemeral
Rollup](/pages/ephemeral-rollups-ers/introduction/ephemeral-rollup) is an
extension of the Solana network designed for high-performance decentralized
applications. It enhances Solana’s capabilities while preserving its
composability and integrity.
***
## Why Ephemeral Rollups?
While blockchain technology is revolutionizing decentralized applications, it still faces fundamental challenges in:
* **Latency** – Blockchain transaction speeds are too slow for real-time applications.
* **Cost** – Even "low-fee" blockchains can become expensive at scale.
* **Scalability** – Current architectures struggle to handle high-throughput applications.
* **Privacy** – Blockchains are by default public, meaning all data onchain can be read.
## **Benefits of MagicBlock's Ephemeral Rollup**
The ephemeral rollup functions as a **specialized SVM runtime** and can be **customized** to include:
* ✅ **Gasless Transactions** – Zero fees enabling scalability and mass adoption.
* ✅ **Faster Block Times** – Sub 10ms latency for seamless UX.
* ✅ **High-precision Scheduling** – Built-in automation to execute transactions.
* ✅ **Program and States Synchronization** – No fragmentation, composable and upgradable.
* ✅ **Horizontal Scaling** – Multiple rollups on-demand for millions of transactions.
* ✅ **Familiar Tooling** – Reusability of existing and familiar programming language, libraries and testing tools.
Learn how Ephemeral Rollups works
Learn how Magic Router works
Try out with Rust, Anchor, and Typescript
Build private, verifiable applications with TEEs
# Client Implementation
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/magic-actions/client
Client-side guide to building with Magic Actions
***
### Quick Access
Explore reference implementation on GitHub
***
### Setup Router Connection
Use Magic Router to route and send transactions to ER and base layer.
```typescript Kit theme={null}
import { Connection } from "@magicblock-labs/ephemeral-rollups-kit";
// Initialize connection
const connection = await Connection.create(
"https://devnet-router.magicblock.app",
"wss://devnet-router.magicblock.app"
);
// ... create transaction
// Send and confirm transaction
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
import { sendAndConfirmTransaction } from "@solana/web3.js";
import { ConnectionMagicRouter } from "@magicblock-labs/ephemeral-rollups-sdk";
// Initialize connection
const connection = new ConnectionMagicRouter(
"https://devnet-router.magicblock.app/",
{ wsEndpoint: "wss://devnet-router.magicblock.app/" }
);
// ... create transaction
// Send and confirm transaction
const txHash = await sendAndConfirmTransaction(connection, tx, [payer], {
skipPreflight: true,
commitment: "confirmed",
});
```
### Transaction Flow
1. Delegate counter to ER
```ts theme={null}
const delegateTx = await program.methods
.delegate()
.accounts({
payer: anchor.Wallet.local().publicKey,
pda: pda,
})
.transaction();
```
2. Increment counter on ER in real-time
```ts theme={null}
const incrementTx = await program.methods
.increment()
.accounts({
counter: pda,
})
.transaction();
```
3. Commit with Magic Action
```ts theme={null}
const commitTx = await program.methods
.commitAndUpdateLeaderboard()
.accounts({ payer: wallet.publicKey /* your accounts */ })
.transaction();
```
### Examples
End-to-end walkthrough on ER usage
Router overview and flow
# Program Implementation
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/magic-actions/implementation
Program-side guide to building with Magic Actions
***
### Quick Access
Explore reference implementation on GitHub
***
### 1) Create action instruction
The instruction `update_leaderboard` runs on the base layer immediately after the commit lands. The `#[action]` attribute on its accounts context marks it as callable from a post-commit action.
`#[action]` makes the instruction **callable from** a post-commit action — it
does not make it callable **only** that way. The handler is an ordinary
base-layer instruction, so anyone can invoke it directly with a wallet.
Address, `seeds`, and `owner` constraints only pin *which* accounts are passed;
they do not authenticate *who* called it. Any handler that moves value or
changes authoritative state must verify the injected `escrow` signer — see
[Authenticate the caller](/pages/ephemeral-rollups-ers/magic-actions/troubleshooting#security-authenticate-the-caller).
```rust theme={null}
// program instruction
pub fn update_leaderboard(ctx: Context) -> Result<()> {
let leaderboard = &mut ctx.accounts.leaderboard;
let counter_info = &mut ctx.accounts.counter.to_account_info();
let mut data: &[u8] = &counter_info.try_borrow_data()?;
let counter = Counter::try_deserialize(&mut data)?;
if counter.count > leaderboard.high_score {
leaderboard.high_score = counter.count;
}
msg!(
"Leaderboard updated! High score: {}",
leaderboard.high_score
);
Ok(())
}
// instruction context
#[action]
#[derive(Accounts)]
pub struct UpdateLeaderboard<'info> {
#[account(mut, seeds = [LEADERBOARD_SEED], bump)]
pub leaderboard: Account<'info, Leaderboard>,
/// CHECK: PDA owner depends on: 1) Delegated: Delegation Program; 2) Undelegated: Your program ID
pub counter: UncheckedAccount<'info>,
}
```
### 2) Build the commit instruction with the action
The commit instruction `commit_and_update_leaderboard` runs on the ER. It uses `MagicIntentBundleBuilder` to schedule both the commit and the post-commit action onto `magic_context` — both are applied together when the ER transaction is sealed back to the base layer.
```rust theme={null}
// commit action instruction on ER
pub fn commit_and_update_leaderboard(ctx: Context) -> Result<()> {
// Build the post-commit action that updates the leaderboard on base layer
let instruction_data =
anchor_lang::InstructionData::data(&crate::instruction::UpdateLeaderboard {});
let action_args = ActionArgs::new(instruction_data);
let action_accounts = vec![
ShortAccountMeta {
pubkey: ctx.accounts.leaderboard.key(),
is_writable: true,
},
ShortAccountMeta {
pubkey: ctx.accounts.counter.key(),
is_writable: false,
},
];
let action = CallHandler {
destination_program: crate::ID,
accounts: action_accounts,
args: action_args,
// Signer that pays transaction fees for the action from its escrow PDA
escrow_authority: ctx.accounts.payer.to_account_info(),
compute_units: 200_000,
};
// Schedule commit + post-commit action on magic_context
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()])
.add_post_commit_actions([action])
.build_and_invoke()?;
Ok(())
}
// commit action context on ER
#[commit]
#[derive(Accounts)]
pub struct CommitAndUpdateLeaderboard<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
/// CHECK: Leaderboard PDA - not mut here, writable set in handler
#[account(seeds = [LEADERBOARD_SEED], bump)]
pub leaderboard: UncheckedAccount<'info>,
/// CHECK: Your program ID
pub program_id: AccountInfo<'info>,
}
```
### Execute multiple actions
You can commit multiple accounts and chain several actions in one call. Actions execute sequentially in the order they're passed to `add_post_commit_actions`.
```rust theme={null}
// Chain several actions — they execute sequentially on base layer after the commit lands.
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(),
// ... additional committed accounts
])
.add_post_commit_actions([action_1, action_2, action_3])
.build_and_invoke()?;
```
### Undelegate with actions
Actions can also be chained onto an undelegation — the counter commits, undelegates, and the actions run, all atomically in one ER transaction.
```rust theme={null}
// Commit, undelegate, AND execute actions — all atomically on base layer after the ER transaction seals.
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()])
.add_post_commit_actions([action])
.build_and_invoke()?;
```
***
# Introduction
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/magic-actions/overview
Automatically execute base-layer actions while Delegated
**Building with an AI coding agent?** Install the MagicBlock Dev Skill to give your agent MagicBlock-specific patterns — delegation flows, Magic Actions, cranks, VRF, and more.
**Hit an error?** Ask your coding agent with the skill installed, not the docs assistant. The assistant only sees the docs, so it cannot debug your code.
Quick install for Claude Code:
```bash theme={null}
npx skills add https://github.com/magicblock-labs/magicblock-dev-skill
```
Using Cursor, Codex, Windsurf, Cline, or another agent? See the [AI Dev Skill](/pages/overview/additional-information/ai-dev-skill) page for all install targets.
***
### Quick Access
Explore reference implementation on GitHub
Compare ER custody with post-commit base-layer payouts in a complete protocol flow.
***
### What are Magic Actions?
Magic Actions let you attach one or more call instructions that run automatically on the Solana base layer immediately after an Ephemeral Rollup
(ER) commit. Use committed state as inputs to orchestrate multi-step workflows in
a single transaction.
* **State sync**: propagate ER changes to base-layer accounts
* **Cross-program composition**: invoke other programs after commit
* **Automated workflows**: chain complex operations from one commit
* **Event-driven actions**: branch logic based on committed state
### How it works
1. Delegate accounts to an Ephemeral Rollup
2. Execute low-latency transactions on Ephemeral Rollup
3. Commit to base layer with attached instruction actions while staying delegated
4. Handlers execute automatically using the freshly committed state
***
### See also
Magic Action work along with Ephemeral Rollups.
Delegation, Commitment & Undelegation
# Troubleshooting
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/magic-actions/troubleshooting
Diagnose common issues with Magic Actions
### 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:
```rust theme={null}
pub const ACTION_ESCROW_INDEX: u8 = 255; // ActionArgs::new default
// In the #[action] accounts context:
/// CHECK: payer identity the action was scheduled with. When the handler acts
/// with a program-owned PDA, bind this to that PDA so only actions *your*
/// program scheduled — not a foreign program's — can drive the handler.
#[account(address = vault_authority.key())]
pub escrow_auth: UncheckedAccount<'info>,
/// CHECK: only the delegation program can sign for this PDA, so `signer`
/// proves the call arrived through the real post-commit path.
#[account(
signer,
address = ephemeral_rollups_sdk::pda::ephemeral_balance_pda_from_payer(
&escrow_auth.key(),
ACTION_ESCROW_INDEX,
),
)]
pub escrow: UncheckedAccount<'info>,
```
`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
Delegation, Commitment & Undelegation
Router overview and flow
Explore reference implementation on GitHub
Chat with the team and community
# Balance
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/balance
pages/ephemeral-spl-token/api-reference/openapi/balance.openapi.json GET /v1/spl/balance
Reads the owner's associated token account on the base RPC.
# Challenge
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/challenge
pages/ephemeral-spl-token/api-reference/openapi/challenge.openapi.json GET /v1/spl/challenge
Returns a challenge string that the wallet must sign. The signature is then submitted to `/v1/spl/login` in exchange for a bearer token used to read private data.
# Deposit SPL Tokens
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/deposit
pages/ephemeral-spl-token/api-reference/openapi/deposit.openapi.json POST /v1/spl/deposit
Wraps the SDK `delegateSpl(...)` flow. The API generates `shuttleId` server-side and pins `escrowIndex` to `0`.
# Initialize Mint
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/initialize-mint
pages/ephemeral-spl-token/api-reference/openapi/initialize-mint.openapi.json POST /v1/spl/initialize-mint
# Introduction
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/introduction
Ephemeral SPL Token API documentation
The on-chain Ephemeral SPL Token program
Explore the example private payments application and API flow
## Overview
The Ephemeral SPL Token API builds unsigned SPL token transactions for deposits, transfers, withdrawals, swaps, and mint initialization across Solana and MagicBlock ephemeral rollups. It also exposes balance queries, mint-initialization status, stealth pools, and a wallet challenge/login flow that issues bearer tokens for reading private data. The canonical public reference is available at [payments.magicblock.app/reference](https://payments.magicblock.app/reference).
### Meta
* [**Health**](/pages/ephemeral-spl-token/api-reference/health) - Check API health and availability
* [**Send Transaction**](/pages/ephemeral-spl-token/api-reference/transaction-send) - Submit a signed transaction to the base layer or ephemeral RPC
### Auth
* [**Challenge**](/pages/ephemeral-spl-token/api-reference/challenge) - Generate a challenge string for the wallet to sign
* [**Login**](/pages/ephemeral-spl-token/api-reference/login) - Exchange a signed challenge for a bearer token
### SPL
* [**Deposit SPL Tokens**](/pages/ephemeral-spl-token/api-reference/deposit) - Build an unsigned deposit transaction from Solana into an ephemeral rollup
* [**Transfer SPL Tokens**](/pages/ephemeral-spl-token/api-reference/transfer) - Build an unsigned public or private SPL transfer
* [**Withdraw SPL Tokens**](/pages/ephemeral-spl-token/api-reference/withdraw) - Build an unsigned withdrawal transaction back to Solana
* [**Undelegate Ephemeral ATA**](/pages/ephemeral-spl-token/api-reference/undelegate-ephemeral-ata) - Build an unsigned transaction that undelegates a wallet's eATA for a mint
* [**Initialize Mint**](/pages/ephemeral-spl-token/api-reference/initialize-mint) - Build an unsigned transaction that initializes a validator-scoped transfer queue for a mint
* [**Ensure Transfer Queue Crank**](/pages/ephemeral-spl-token/api-reference/transfer-queue-ensure-crank) - Verify a mint's transfer queue and force one crank attempt
* [**Balance**](/pages/ephemeral-spl-token/api-reference/balance) - Get the base-chain SPL token balance for an address
* [**Private Balance**](/pages/ephemeral-spl-token/api-reference/private-balance) - Get the ephemeral-rollup SPL token balance for an address (auth required)
* [**Is Mint Initialized**](/pages/ephemeral-spl-token/api-reference/is-mint-initialized) - Check whether a mint has a validator-scoped transfer queue on the ephemeral RPC
### Stealth Pools
* [**Create Stealth Pool**](/pages/ephemeral-spl-token/api-reference/stealth-pool) - Map a handle to destination keys for private transfers (auth required)
* [**Get Stealth Pool Status**](/pages/ephemeral-spl-token/api-reference/stealth-pool-status) - Check whether a handle's stealth pool exists
### Swap
* [**Swap Quote**](/pages/ephemeral-spl-token/api-reference/quote) - Get a swap quote between two SPL mints
* [**Swap**](/pages/ephemeral-spl-token/api-reference/swap) - Build an unsigned swap transaction (public pass-through or private with scheduled transfer)
### MCP
* [**MCP**](/pages/ephemeral-spl-token/api-reference/mcp) - Access the stateless Streamable HTTP MCP endpoint
```
┌────────────────────────────────────────────┐
│ 1. Deposit │
├────────────────────────────────────────────┤
│ • Build an unsigned deposit transaction │
│ • Solana base balance → ephemeral rollup │
└────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────┐
│ 2. Transfer / Swap │
├────────────────────────────────────────────┤
│ • Build SPL transfer or swap │
│ • base/ephemeral → base/ephemeral │
│ • public or private (delayed + split) │
└────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────┐
│ 3. Withdraw │
├────────────────────────────────────────────┤
│ • Build an unsigned withdrawal │
│ • ephemeral rollup → Solana base balance │
└────────────────────────────────────────────┘
```
## Auth Flow
Endpoints that read private data inside the Private Ephemeral Rollup require a bearer token:
1. `GET /v1/spl/challenge?pubkey=` returns a `challenge` string
2. The wallet signs the challenge
3. `POST /v1/spl/login` with `{ pubkey, challenge, signature }` returns a `token`
4. Pass `Authorization: Bearer ` on `/v1/spl/private-balance` (required) and on `/v1/spl/transfer` requests that need to connect to the Private Ephemeral Rollup (optional)
## Response Format
Successful transaction-building endpoints return an unsigned transaction payload:
```json theme={null}
{
"kind": "deposit",
"version": "legacy",
"transactionBase64": "base64-encoded-transaction",
"sendTo": "base",
"recentBlockhash": "blockhash",
"lastValidBlockHeight": 284512337,
"instructionCount": 3,
"requiredSigners": ["3rXKwQ1kpjBd5tdcco32qsvqUh1BnZjcYnS5kYrP7AYE"]
}
```
The expected client flow is:
1. Call the API
2. Decode `transactionBase64`
3. Optionally adjust the transaction if the client needs to
4. Sign with the required wallet(s)
5. Send to the RPC indicated by `sendTo` (`"base"` or `"ephemeral"`)
# Is Mint Initialized
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/is-mint-initialized
pages/ephemeral-spl-token/api-reference/openapi/is-mint-initialized.openapi.json GET /v1/spl/is-mint-initialized
# Login
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/login
pages/ephemeral-spl-token/api-reference/openapi/login.openapi.json POST /v1/spl/login
Verifies the wallet's signature over the challenge issued by `/v1/spl/challenge` and returns an authentication token. Pass the token as `Authorization: Bearer ` on `/v1/spl/private-balance` and on `/v1/spl/transfer` requests that need to connect to the Private Ephemeral Rollup.
# Private Balance
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/private-balance
pages/ephemeral-spl-token/api-reference/openapi/private-balance.openapi.json GET /v1/spl/private-balance
Reads the owner's associated token account on the ephemeral RPC. Requires an `Authorization: Bearer ` header obtained from `/v1/spl/login`.
# Create Stealth Pool
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/stealth-pool
pages/ephemeral-spl-token/api-reference/openapi/stealth-pool.openapi.json POST /v1/spl/stealth-pool
Build unsigned stealth-pool setup and ER update transactions that map a handle to 1-10 destination owner keys. Requires an `Authorization: Bearer ` header from the `/v1/spl/login` flow. Once initialized, the handle can be used as the `to` value of a private `/v1/spl/transfer`.
# Get Stealth Pool Status
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/stealth-pool-status
pages/ephemeral-spl-token/api-reference/openapi/stealth-pool.openapi.json GET /v1/spl/stealth-pool
Derive a stealth-pool PDA from an exact handle and report whether the base account exists. Does not return the destination keys.
# Send Transaction
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/transaction-send
pages/ephemeral-spl-token/api-reference/openapi/transaction-send.openapi.json POST /v1/transaction/send
Submit a signed, serialized transaction. Use the `sendTo` value returned by a build endpoint (deposit / transfer / withdraw / stealth-pool) to route the transaction to the base layer or the ephemeral RPC. Accepts an optional `Authorization: Bearer ` header for ephemeral submissions that require it.
# Transfer SPL Tokens
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/transfer
pages/ephemeral-spl-token/api-reference/openapi/transfer.openapi.json POST /v1/spl/transfer
Transfer SPL tokens publicly or privately through an ephemeral rollup. Accepts an optional `Authorization: Bearer ` header obtained from the `/v1/spl/login` flow when the request needs to read or write data inside the Private Ephemeral Rollup.
# Undelegate Ephemeral ATA
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/undelegate-ephemeral-ata
pages/ephemeral-spl-token/api-reference/openapi/undelegate-ephemeral-ata.openapi.json POST /v1/spl/undelegate-ephemeral-ata
Build an unsigned ephemeral-rollup transaction that undelegates a wallet's ephemeral ATA (eATA) for a mint, committing its balance back toward the base layer. Accepts an optional `Authorization: Bearer ` header.
# Withdraw SPL Tokens
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/withdraw
pages/ephemeral-spl-token/api-reference/openapi/withdraw.openapi.json POST /v1/spl/withdraw
Wraps the SDK `withdrawSpl(...)` flow. The API generates `shuttleId` server-side.
# Ephemeral SPL Tokens
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/overview
Move SPL tokens at Ephemeral Rollup speed with the Ephemeral SPL Token program — learn the eATA, Global Vault, delegation, custody, and private-payment models.
***
## Overview
**Ephemeral SPL Tokens** let you hold and move SPL tokens inside a [MagicBlock Ephemeral Rollup (ER)](/pages/ephemeral-rollups-ers/introduction/ephemeral-rollup)
with the same low latency as any other delegated state, then settle back to Solana's base layer. They
are the token primitive behind MagicBlock's **private payments** — but the primitive itself is general:
a transfer can be **public or private**.
Everything is powered by the on-chain **Ephemeral SPL Token program**
(`SPLxh1LVZzEkX99H6rqYizhytLWPZVV296zyYDPagv2`), which implements
[MIMD 0013](https://github.com/magicblock-labs/magicblock-validator/discussions/550).
On-chain program + hosted API. `SPLxh1LV…`
Delegate a token account, transfer on the ER, withdraw — end to end.
See how token custody fits with session keys, price oracles, cranks, and settlement.
***
## The model
A normal SPL token account lives on Solana's base layer. To move tokens at ER speed, the Ephemeral SPL
Token program introduces two account types.
### Ephemeral ATA (eATA)
An **ephemeral ATA** is a program-owned PDA derived from `[owner, mint]` that holds a single `u64`
balance. It is **not** a real SPL token account — it is a lightweight balance record that can be
delegated to the ER and mutated there at high speed.
### Global Vault
The real tokens are custodied in a per-mint **Global Vault** — a PDA derived from `[mint]` that owns
the SPL token account backing **every** eATA of that mint. Depositing moves real tokens into the vault
and credits your eATA's `u64`; withdrawing does the reverse.
An eATA tracks a balance; the Global Vault holds the actual tokens. One vault backs all eATAs for a
given mint.
***
## Delegation lifecycle
eATAs follow the standard Ephemeral Rollup lifecycle:
Real tokens move into the mint's Global Vault, your eATA is credited, and the eATA is delegated to
a MagicBlock validator so it can be mutated inside the ER.
Transfer between eATAs inside the rollup at low latency. Transfers can be **public** or
**private** (`visibility`).
The eATA is committed and undelegated back to the base layer.
Tokens move out of the Global Vault back to a standard base-layer SPL token account.
***
## Public vs. private
The primitive supports both:
* **Public** — a normal fast transfer between eATAs on the ER.
* **Private (Private Payments)** — the destination and settlement are shielded: private `visibility`,
encrypted/queued settlement, and **stealth handles** (a human-readable name such as
`alice@magicblock.id` that resolves to one or more destination keys). This is the use case behind
the [Private Payments guide](/pages/ephemeral-spl-token/private-payments).
***
## Two ways to integrate
Integrate directly in your own Anchor program and client with
`@magicblock-labs/ephemeral-rollups-sdk` — delegate a token account, transfer on the ER, withdraw.
Let the hosted API build unsigned transactions for deposit, transfer, withdraw, balances, swap, and
private payments.
# Private Payments with Ephemeral SPL Tokens
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/private-payments
Use Ephemeral SPL Tokens for private deposits, transfers, and withdrawals — private visibility, stealth handles, and queued settlement over the hosted API.
***
### Quick Access
Check out example:
SPL Tokens Anchor Implementation
Try the SPL Tokens demo
Try private payments
***
## Overview
**Private Payments** are the privacy use case built on top of [SPL tokens on
ER](/pages/ephemeral-spl-token/overview). The same
deposit / transfer / withdraw primitive runs with **private visibility**, so the amount, destination,
and timing of a payment are shielded rather than broadcast publicly.
New to the primitive? Read the [Ephemeral SPL Tokens
overview](/pages/ephemeral-spl-token/overview) first — this guide
assumes the eATA / Global Vault / delegation model.
The easiest way to build private payments is the hosted **Ephemeral SPL Token API**, which returns
unsigned transactions you sign and submit.
Endpoints for deposit, transfer, withdraw, balances, stealth pools, and auth.
The on-chain Ephemeral SPL Token program.
***
## Privacy model
Private payments rely on:
* **Private visibility** — transfers execute inside the Ephemeral Rollup with `visibility: "private"`,
so the transfer is not broadcast publicly.
* **Stealth handles** — send to a human-readable name (e.g. `alice@magicblock.id`) instead of a raw
public key. The handle resolves to one or more destination keys via a **stealth pool**, breaking the
direct sender → recipient link on-chain.
* **Queued settlement** — private transfers can settle through the program's transfer queue rather than
a direct, immediately-linkable movement.
Privacy here reduces **linkability**, not total observability. Amounts and timing may still be
inferable at the network level. Threat-model your assumptions.
***
## Authentication
Private reads and stealth-pool operations require a bearer token. Obtain one with the challenge/login
flow before calling protected endpoints:
`GET /v1/spl/challenge` returns a message for the user to sign.
`POST /v1/spl/login` exchanges the signed challenge for a bearer token.
Send `Authorization: Bearer ` on `GET /v1/spl/private-balance` and
`POST /v1/spl/stealth-pool`.
***
## Deposits
Move tokens into the mint's Global Vault and credit the depositor's ephemeral ATA.
* `POST /v1/spl/deposit` — builds the deposit transaction. Set `private: true` to keep the deposited
balance private.
The response is an unsigned transaction plus a `sendTo` field (`base` or `ephemeral`). Sign it and
submit via [`POST /v1/transaction/send`](/pages/ephemeral-spl-token/api-reference/transaction-send)
or your own RPC.
***
## Private transfers
* `POST /v1/spl/transfer` with `visibility: "private"`.
Two destination modes:
| Destination | `to` value | Requirements |
| -------------- | ---------------------------- | ------------------------------------------------------------------- |
| Direct | recipient public key | `visibility: "private"` |
| Stealth handle | an initialized handle string | `visibility: "private"`, `fromBalance: "base"`, `toBalance: "base"` |
To use a stealth handle, initialize it first:
* `POST /v1/spl/stealth-pool` — map a handle (≤255 UTF-8 bytes; **not** normalized, so
`Alice@…` ≠ `alice@…`) to 1–10 destination owner keys, optionally splitting payments across them.
* `GET /v1/spl/stealth-pool?handle=…` — check whether a handle's pool exists.
Handles are stored as their exact UTF-8 bytes. `GET` returns only whether the pool exists — never the
destination keys.
***
## Fees and gasless transfers
`visibility` and `gasless` are independent request fields: `visibility` controls how the transfer is
routed, while `gasless` controls who pays gas. The cost of a private transfer is the sum of a
**privacy fee**, which always applies, and the chosen **gas payment mode**.
Every private base → base transfer pays a **0.1% (10 bps) privacy fee**, charged in the token being
transferred — not in SOL. Gas can be paid in either of two modes:
| | Self-paid | Gasless (`gasless: true`) |
| ------------------- | ----------------------- | ---------------------------------- |
| SOL transaction fee | sender pays (needs SOL) | sponsor pays (sender needs no SOL) |
| Relay fee | none | flat 0.2 USDC/USDT to the sponsor |
| 0.1% privacy fee | yes, in tokens | yes, in tokens |
| Minimum amount | none | 0.5 USDC/USDT |
| Supported mints | any | mainnet USDC/USDT, devnet USDC |
With `gasless: true`, the configured sponsor becomes the transaction fee payer and co-signs, and the
API prepends a token-transfer instruction that reimburses the sponsor with a flat **0.2 USDC/USDT
relay fee** from the sender's balance. Because the relay fee is flat, gasless transfers require a
minimum of **0.5 USDC/USDT**; the minimum applies only to the gasless mode, not to private transfers
in general. Amounts below it can still be sent with `visibility: "private"` by omitting `gasless`.
A first private transfer may also include a one-time **\~0.00204 SOL** of rent to set up the ephemeral
token account. All token-denominated fees are reported in the transfer response as `fees.tokens`, and
SOL-denominated costs as `fees.lamports`.
A `gasless: true` request below the minimum, or with an unsupported mint, is rejected with a `400`
error (`INVALID_GASLESS_TRANSFER_AMOUNT` / `INVALID_GASLESS_TRANSFER_MINT`). The API never changes
the requested `visibility`: a transfer is only public when the request says so. Clients that handle
gasless errors by rebuilding the request should preserve `visibility: "private"` if privacy is
intended.
`gasless: true` is ignored when `from` is an off-curve PDA owner (gasless requires a wallet sender);
the transfer still executes with the requested visibility, with the sender as fee payer.
***
## Withdrawals
Move a balance back out of the Global Vault to a standard base-layer SPL token account.
* `POST /v1/spl/withdraw` — builds the withdrawal transaction.
Check balances any time:
* `GET /v1/spl/balance` — public balance.
* `GET /v1/spl/private-balance` — private balance (requires the bearer token).
***
## Developer notes
* **Privacy is a spectrum.** Ephemeral SPL tokens reduce linkability; they do not hide amounts, timing,
or protect against network-level analysis.
* **Handles aren't normalized.** Casing/whitespace matter — display and store handles consistently.
* **Match the flow.** Stealth-handle transfers require `visibility: "private"`, `fromBalance: "base"`,
`toBalance: "base"` (these are also the omitted-field defaults).
* **Sign then send.** Builder endpoints return unsigned transactions; submit them with
`POST /v1/transaction/send` and honor the returned `sendTo`.
***
## Next steps
The primitive behind private payments.
The on-chain/SDK integration path.
# Ephemeral SPL Token Quickstart
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/quickstart
Delegate an SPL token account to an Ephemeral Rollup, transfer on the ER, and withdraw back to the base layer — with the ephemeral-rollups-sdk.
***
**Building with an AI coding agent?** Install the MagicBlock Dev Skill to give your agent MagicBlock-specific patterns — delegation flows, Magic Actions, cranks, VRF, and more.
**Hit an error?** Ask your coding agent with the skill installed, not the docs assistant. The assistant only sees the docs, so it cannot debug your code.
Quick install for Claude Code:
```bash theme={null}
npx skills add https://github.com/magicblock-labs/magicblock-dev-skill
```
Using Cursor, Codex, Windsurf, Cline, or another agent? See the [AI Dev Skill](/pages/overview/additional-information/ai-dev-skill) page for all install targets.
### Quick Access
Check out example:
SPL Tokens Anchor Implementation
Try the SPL Tokens demo
Try private payments
Snippets target `@magicblock-labs/ephemeral-rollups-sdk` **v0.14.3** with the legacy-vault path. The
idempotent-shuttle path targets v0.15.3 — keep the same path across `delegateSpl` / `undelegateIx` /
`withdrawSpl` within one lifecycle.
***
## Step-By-Step Guide
Move SPL tokens through the full lifecycle against the Ephemeral SPL Token program
`SPLxh1LVZzEkX99H6rqYizhytLWPZVV296zyYDPagv2` — [delegate](/pages/ephemeral-rollups-ers/introduction/ephemeral-accounts), transact on the [ER](/pages/ephemeral-rollups-ers/introduction/ephemeral-rollup), then settle back to
Solana:
Delegate the token account}>
Delegate an owner's balance to a validator on the base layer. The first delegation for a mint
creates the shared Global Vault.
Transfer on the ER}>
Move tokens between eATAs inside the rollup — public or private.
Undelegate & commit}>
Undelegate on the ER and wait for the commit back to base.
Withdraw to base}>
Move the balance out of the Global Vault to a base-layer token account.
***
## Ephemeral SPL Token Example
The following software packages may be required, other versions may also be compatible:
| Software | Version | Installation Guide |
| ---------- | ------- | --------------------------------------------------------------- |
| **Solana** | 3.1.9 | [Install Solana](https://docs.anza.xyz/cli/install) |
| **Rust** | 1.89.0 | [Install Rust](https://www.rust-lang.org/tools/install) |
| **Anchor** | 1.0.2 | [Install Anchor](https://www.anchor-lang.com/docs/installation) |
| **Node** | 24.10.0 | [Install Node](https://nodejs.org/en/download/current) |
Install the SDK:
```bash theme={null}
yarn add @magicblock-labs/ephemeral-rollups-sdk@0.14.3
```
### Code Snippets
`delegateSpl` delegates an owner's balance to a validator. The first delegation for a mint creates
the shared Global Vault (`initVaultIfMissing: true`); later ones reuse it. Send on the **base
layer**, and fund the rent sponsor (`deriveRentPda()`) before delegating.
```typescript theme={null}
// Legacy vault flow — keep the same idempotent setting across
// delegateSpl / undelegateIx / withdrawSpl within one lifecycle.
const delegateOpts = { validator, idempotent: false as const, payer: admin.publicKey };
const ixs = await delegateSpl(owner.publicKey, mint.publicKey, amount, {
...delegateOpts,
initVaultIfMissing, // true for the first owner of this mint, false after
});
await provider.sendAndConfirm(
new anchor.web3.Transaction().add(...ixs),
[owner, admin],
{ commitment: "confirmed", skipPreflight: true },
);
```
[⬆️ Back to Top](#code-snippets)
`transferSpl` moves tokens between eATAs inside the rollup. Send on the **ephemeral** provider. Set
`visibility: "private"` for a private payment or `"public"` for a normal fast transfer.
```typescript theme={null}
// Delegation confirms on base before the ER clones the account —
// poll the ER view before transferring.
await waitForErTokenAccount(ata, expectedAmount);
const transferIxs = await transferSpl(
recipientA.publicKey,
recipientB.publicKey,
mint.publicKey,
2n,
{ visibility: "public", fromBalance: "ephemeral", toBalance: "ephemeral" },
);
await providerEphemeralRollup.sendAndConfirm(
new anchor.web3.Transaction().add(...transferIxs),
[recipientA],
{ commitment: "confirmed", skipPreflight: true },
);
```
[⬆️ Back to Top](#code-snippets)
Undelegate each owner on the ER (one per transaction), then wait for the commit back to base with
`GetCommitmentSignature` before withdrawing.
```typescript theme={null}
const sgn = await providerEphemeralRollup.sendAndConfirm(
new anchor.web3.Transaction().add(undelegateIx(owner.publicKey, mint.publicKey)),
[owner],
{ commitment: "confirmed", skipPreflight: true },
);
// Wait for the commit back to base before withdrawing, or the withdraw
// races the commit and fails with InvalidAccountOwner.
const commit = await GetCommitmentSignature(sgn, providerEphemeralRollup.connection);
await connection.confirmTransaction(commit, "confirmed");
```
[⬆️ Back to Top](#code-snippets)
`withdrawSpl` moves the balance out of the Global Vault back to the owner's base-layer ATA.
```typescript theme={null}
const withdrawIxs = await withdrawSpl(owner.publicKey, mint.publicKey, amount, {
idempotent: false,
});
await provider.sendAndConfirm(
new anchor.web3.Transaction().add(...withdrawIxs),
[owner],
{ commitment: "confirmed" },
);
```
[⬆️ Back to Top](#code-snippets)
To route the ER-side transfer through your own program instead of the SDK helper, add the
`#[ephemeral]` attribute — the instruction is a plain SPL Token CPI. See
[Smart Contract Integration](/pages/ephemeral-spl-token/smart-contract-integration) for custody
and PDA-signed transfers.
```rust theme={null}
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer as SplTransfer};
use ephemeral_rollups_sdk::anchor::ephemeral;
#[ephemeral]
#[program]
pub mod spl_tokens {
use super::*;
/// Transfer `amount` of SPL tokens from `from` to `to`.
pub fn transfer(ctx: Context, amount: u64) -> Result<()> {
require!(amount > 0, ErrorCode::InvalidAmount);
let cpi_accounts = SplTransfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.payer.to_account_info(),
};
let cpi_ctx = CpiContext::new(ctx.accounts.token_program.to_account_info(), cpi_accounts);
token::transfer(cpi_ctx, amount)?;
Ok(())
}
}
```
[⬆️ Back to Top](#code-snippets)
**Run it locally:** `yarn` → `yarn build` → `yarn setup` (boots the local base + ER cluster; leave
running) → `yarn test:local` in a second terminal.
***
## Solana Explorer
Get insights about your transactions and accounts on Solana:
Official Solana Explorer
Explore Solana Blockchain
## Solana RPC Providers
Send transactions and requests through existing RPC providers:
Free Public Nodes
Free Shared Nodes
Dedicated High-Performance Nodes
## Solana Validator Dashboard
Find real-time updates on Solana's validator infrastructure:
Get Validator Insights
Discover Validator Metrics
## Server Status
Subscribe to Solana's and MagicBlock's server status:
Subscribe to Solana Server Updates
Subscribe to MagicBlock Server Status
***
## MagicBlock Products
Execute real-time, zero-fee transactions securely on Solana.
Protect sensitive data with compliance — built on top of Ephemeral Rollups.
Move SPL tokens at rollup speed — public or private transfers, swaps, and private payments for trading and DeFi apps.
Combine real-time execution, session keys, token custody, price feeds, automation, and settlement.
Add provably fair onchain randomness to games, raffles, and real-time apps.
Access low-latency onchain price feeds for trading and DeFi.
***
# Smart Contract Integration
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/smart-contract-integration
Custody and move SPL tokens from your own Solana program on an Ephemeral Rollup — PDA-signed transfers, delegation, and commit/undelegate.
**Building with an AI coding agent?** Install the MagicBlock Dev Skill to give your agent MagicBlock-specific patterns — delegation flows, Magic Actions, cranks, VRF, and more.
**Hit an error?** Ask your coding agent with the skill installed, not the docs assistant. The assistant only sees the docs, so it cannot debug your code.
Quick install for Claude Code:
```bash theme={null}
npx skills add https://github.com/magicblock-labs/magicblock-dev-skill
```
Using Cursor, Codex, Windsurf, Cline, or another agent? See the [AI Dev Skill](/pages/overview/additional-information/ai-dev-skill) page for all install targets.
***
### Quick Access
eATA custody end to end
Base-layer post-commit payouts
Non-custody transfers
This guide is for on-chain programs — a DEX, AMM, prediction market, escrow, or game — that hold user
funds and move them at [Ephemeral Rollup](/pages/ephemeral-rollups-ers/introduction/ephemeral-rollup) speed. The core pattern: a program-owned account, delegated to
the ER, with your program signing token transfers as a PDA.
Moving tokens from a client instead of on-chain? See the
[Quickstart](/pages/ephemeral-spl-token/quickstart).
### Two custody models
| Model | You delegate… | Tokens move… | Use when |
| ---------------------------------- | -------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------ |
| **eATA custody** | a program-owned **ephemeral ATA (eATA)** | **on the ER**, PDA-signed | you settle or pay out inside the rollup (e.g. a market fill) |
| **State PDA + post-commit payout** | only a **state PDA**; real ATAs stay on base | **on the base layer**, as a post-commit action | funds stay on L1 and settle on commit |
The walkthrough below covers **eATA custody**, with the post-commit variant in the advanced snippets.
***
## Step-By-Step Guide
Give your program authority over an eATA, delegate it, move funds PDA-signed on the ER, then settle
back to Solana:
Make your program ER-aware}>
Add the `#[ephemeral]` attribute and import the SDK helpers.
Create the custody PDA and eATA}>
Own an eATA under the Ephemeral SPL Token program from your custody PDA.
Delegate the eATA}>
CPI into the Ephemeral SPL Token program to delegate it to the ER.
Move tokens PDA-signed}>
Sign the transfer as the custody PDA inside the rollup.
Commit & undelegate}>
Return the account to the base layer.
***
## Custody Example
The following software packages may be required, other versions may also be compatible:
| Software | Version | Installation Guide |
| ---------- | ------- | --------------------------------------------------------------- |
| **Solana** | 3.1.9 | [Install Solana](https://docs.anza.xyz/cli/install) |
| **Rust** | 1.89.0 | [Install Rust](https://www.rust-lang.org/tools/install) |
| **Anchor** | 1.0.2 | [Install Anchor](https://www.anchor-lang.com/docs/installation) |
| **Node** | 24.10.0 | [Install Node](https://nodejs.org/en/download/current) |
### Code Snippets
#### 1. ER-aware program
Wrap `#[program]` with `#[ephemeral]` and import the SDK helpers you need.
```rust theme={null}
use ephemeral_rollups_sdk::anchor::{commit, delegate, ephemeral};
use ephemeral_rollups_sdk::cpi::DelegateConfig;
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;
#[ephemeral]
#[program]
pub mod your_program {
use super::*;
// ...
}
```
[⬆️ Back to Top](#code-snippets)
#### 2. Create eATA
Your program owns a state PDA (for example, a `Pool`) that is the authority over the funds. Custody
on the ER uses an **ephemeral ATA (eATA)** owned by that PDA, derived from `[owner, mint]` under the
Ephemeral SPL Token program (`SPLxh1LVZzEkX99H6rqYizhytLWPZVV296zyYDPagv2`).
```rust theme={null}
// The eATA is owned by your custody PDA (e.g. a Pool), derived under the
// Ephemeral SPL Token program from [owner, mint].
let (eata, _bump) = Pubkey::find_program_address(
&[owner.as_ref(), mint.as_ref()],
&EPHEMERAL_SPL_TOKEN_PROGRAM_ID,
);
// At initialize, create the eATA (InitializeEphemeralAta) and the per-mint
// global vault that backs every eATA (InitializeGlobalVault), then fund it.
```
[⬆️ Back to Top](#code-snippets)
#### 3. Delegate eATA
Delegate the eATA with a CPI to the Ephemeral SPL Token program (`DelegateEphemeralAta`,
discriminator `4`).
Delegating a plain PDA-owned ATA directly to the ER is not supported — token custody on the ER
goes through an eATA. To delegate program *state* (not a token account), use the SDK's
`#[delegate]` macro instead.
```rust theme={null}
// Delegate the eATA with a CPI to the Ephemeral SPL Token program.
// An optional trailing validator pubkey routes to a specific validator.
let mut data = vec![4]; // DelegateEphemeralAta
if let Some(validator) = validator {
data.extend_from_slice(validator.as_ref());
}
let instruction = Instruction {
program_id: EPHEMERAL_SPL_TOKEN_PROGRAM_ID,
accounts: vec![
AccountMeta::new(payer.key(), true),
AccountMeta::new(ephemeral_ata.key(), false),
AccountMeta::new_readonly(EPHEMERAL_SPL_TOKEN_PROGRAM_ID, false),
AccountMeta::new(buffer.key(), false),
AccountMeta::new(record.key(), false),
AccountMeta::new(metadata.key(), false),
AccountMeta::new_readonly(delegation_program.key(), false),
AccountMeta::new_readonly(system_program.key(), false),
],
data,
};
invoke(&instruction, &account_infos)?;
```
[⬆️ Back to Top](#code-snippets)
#### 4. PDA-signed transfer
With the eATA delegated, move tokens inside the rollup with an SPL Token CPI signed by the custody
PDA via `CpiContext::new_with_signer`. The signer seeds are `[POOL_SEED, bump]` — the program
authorizes the payout itself, with no user signature.
```rust theme={null}
use anchor_spl::token::{self, Transfer as SplTransfer};
// The custody PDA signs the transfer — no user signature required.
let bump_seed = [pool_bump];
let signer_seeds: &[&[&[u8]]] = &[&[POOL_SEED, &bump_seed]];
let cpi_accounts = SplTransfer {
from,
to,
authority: pool, // the Pool PDA
};
let cpi_ctx = CpiContext::new_with_signer(token_program.to_account_info(), cpi_accounts, signer_seeds);
token::transfer(cpi_ctx, amount)?;
```
[⬆️ Back to Top](#code-snippets)
5. Commit & undelegate
Commit the ER state and return the account to the base layer with `MagicIntentBundleBuilder`.
```rust theme={null}
// The #[commit] attribute on the accounts context supplies
// magic_context and magic_program.
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.pool.to_account_info()])
.build_and_invoke()?;
```
[⬆️ Back to Top](#code-snippets)
***
### Advanced Code Snippets
If your program doesn't custody funds — it moves tokens between two already-delegated accounts on a
user's behalf — no PDA signer is needed. The authority is the user's `Signer`, with a plain
`CpiContext::new`.
```rust theme={null}
// No PDA signer — the authority is the user's Signer.
let cpi_accounts = SplTransfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.payer.to_account_info(),
};
token::transfer(CpiContext::new(ctx.accounts.token_program.to_account_info(), cpi_accounts), amount)?;
```
[⬆️ Back to Top](#advanced-code-snippets)
To keep real token accounts on the base layer and delegate only program state, schedule the payout
as a post-commit action that runs PDA-signed on the base layer right after a commit — using
`transfer_checked` (Token-2022 compatible) and committing with the action attached.
```rust theme={null}
// Keep real token accounts on base, delegate only program state, and pay out
// PDA-signed on the base layer right after the commit. Use the PDA AccountInfo
// as the payer and pass its seeds to build_and_invoke_signed.
MagicIntentBundleBuilder::new(
payer_pda.to_account_info(),
magic_context.to_account_info(),
magic_program.to_account_info(),
)
.magic_fee_vault(magic_fee_vault.to_account_info())
.commit(&[state_pda.to_account_info()])
.add_post_commit_actions([action])
.build_and_invoke_signed(&[payer_seeds])?;
```
The post-commit handler is a normal base-layer instruction — anyone can
call it directly, not only the delegation program. Because it signs the
transfer with your PDA, it **must authenticate the caller**; otherwise any
wallet can invoke it directly and move your program's tokens without
authorization. Require the injected `escrow` account as a `signer` and pin it
to `ephemeral_balance_pda_from_payer(escrow_auth, 255)`, and bind
`escrow_auth` to your paying PDA. See
[Authenticate the caller](/pages/ephemeral-rollups-ers/magic-actions/troubleshooting#security-authenticate-the-caller).
[⬆️ Back to Top](#advanced-code-snippets)
***
## Solana Explorer
Get insights about your transactions and accounts on Solana:
Official Solana Explorer
Explore Solana Blockchain
## Solana RPC Providers
Send transactions and requests through existing RPC providers:
Free Public Nodes
Free Shared Nodes
Dedicated High-Performance Nodes
## Solana Validator Dashboard
Find real-time updates on Solana's validator infrastructure:
Get Validator Insights
Discover Validator Metrics
## Server Status
Subscribe to Solana's and MagicBlock's server status:
Subscribe to Solana Server Updates
Subscribe to MagicBlock Server Status
***
# Why MagicBlock?
Source: https://docs.magicblock.gg/pages/get-started/introduction/why-magicblock
The high-performance engine for real-time applications on Solana
MagicBlock is an extension of the Solana network designed for high-performance
decentralized applications. It enhances Solana’s capabilities while preserving
its composability and integrity.
## Why MagicBlock?
While blockchain technology is revolutionizing decentralized applications, it still faces fundamental challenges in:
* **Latency** – Blockchain transaction speeds are too slow for real-time applications.
* **Cost** – Even "low-fee" blockchains can become expensive at scale.
* **Scalability** – Current architectures struggle to handle high-throughput applications.
* **Privacy** – Blockchains are by default public, meaning all data onchain can be read.
MagicBlock solves these issues with an **ephemeral rollup**, enabling developers to build dApps that require **10 ms state transitions**, **gasless transactions**, and **horizontal scaling**. Here's how:
#### Built on Solana's Ecosystem and Performance
MagicBlock operates as a specialized **Solana Virtual Machine (SVM) runtime**, seamlessly integrating with Solana’s base layer. Developers deploying programs on Solana are fully compatible with the ephemeral rollup down to the bytecode, benefiting from the SVM’s high performance and robust ecosystem.
#### Overcoming Limitations in Scaling and Cost
Traditional rollups and Solana’s current execution model have inherent constraints when handling resource-intensive applications. MagicBlock removes these limitations by enabling:
* **Ultra-Low Latency (10 ms Block Time)**: Solana's default 400 ms block time is too slow for real-time applications. MagicBlock allows developers to customize execution environments while maintaining full compatibility.
* **State Integrity Without Fragmentation**: Traditional rollups fragment application states across multiple environments, making interoperability difficult. MagicBlock prevents this by utilizing specialized RPC providers that **route and process transactions in parallel** between Solana’s base layer and ephemeral rollups.
* **Near-Zero Transaction Fees**: Even Solana’s low fees (\~\$0.01 per transaction) can be expensive at scale. MagicBlock drastically reduces costs, enabling minimal or **zero-fee transactions**.
* **Horizontal Scalability**: Standard rollups struggle to scale efficiently. MagicBlock enables **horizontal auto-scaling**, seamlessly spinning up multiple ephemeral rollups to process millions of transactions per second.
### Where do I start?
Learn how Ephemeral Rollups works
Learn how Magic Router works
Try out with Rust, Anchor, and Typescript
Discover new ideas from examples
Dive into Frameworks and SDKs
Build private, verifiable applications with TEEs
# AI Agents
Source: https://docs.magicblock.gg/pages/get-started/use-cases/ai
Unlock the next era of real-time on-chain AI
#### 🔴 Problems with AI
* **Centralized Control** – AI models are closed-source and biased.
* **Opaque Decision-Making** – Users cannot verify AI logic or data.
* **Monopoly on AI Access** – Only big tech controls AI resources.
#### ⛓️ Current Blockchain Limitations
* **Computational Cost** – On-chain AI execution is expensive.
* **Data Availability** – Limited high-speed on-chain data for AI.
* **Scalability** – AI models struggle with real-time blockchain inference.
#### ⚡ MagicBlock's Solution
* **Decentralized AI Models** – Open-source, auditable AI with trustless execution.
* **Real-Time On-Chain Data** – AI can interact with verifiable blockchain state.
* **Scalable AI Execution** – Optimized engine for fast, low-cost inference.
## Example: Ultra-Low Latency Decentralized AI Agent
Learn more about building and integrating Decentralized AI with MagicBlock!
# DePIN
Source: https://docs.magicblock.gg/pages/get-started/use-cases/depin
Unlock the next era of real-time decentralized physical infrastructure networks
#### 🔴 Problems with Infrastructure
* **Centralized Control & Single Points of Failure** – Networks, supply chains, and financial systems rely on centralized entities, making them vulnerable to outages, corruption, or censorship.
* **High Costs & Inefficiencies** – Multiple intermediaries slow down operations, increase costs, and introduce human errors.
* **Lack of Transparency & Trust** – Data manipulation and fraud thrive in closed systems, making it hard to verify authenticity.
#### ⛓️ Current Blockchain Limitations
* **Scalability Challenges** – Most blockchains struggle with high transaction loads, limiting their use for real-time infrastructure needs.
* **High Costs** – Rising gas fees make microtransactions and IoT integrations impractical.
* **Latency & Finality Delays** – Many blockchains require multiple confirmations, preventing instant execution in critical applications.
#### ⚡ MagicBlock’s Solution
* **Real-Time, Low-Cost Transactions** – Eliminates bottlenecks, enabling seamless on-chain infrastructure.
* **Scalable & Efficient** – Optimized for **high throughput** and **low fees**, making microtransactions viable.
* **Verifiable & Trustless** – Provides a tamper-proof, open ledger for **transparent** and **decentralized** infrastructure.
## Example: Ultra-Low Latency On-Chain Payments
Learn more about building and integrating on-chain payments with MagicBlock!
# Finance
Source: https://docs.magicblock.gg/pages/get-started/use-cases/finance
Unlock the next era of real-time decentralized finance
#### 🔴 Problems with Finance
* **Slow Settlement** – Transactions take days to finalize due to intermediaries.
* **Opaque Systems** – Users rely on centralized entities with limited visibility.
* **Limited Composability** – Financial platforms operate in silos, restricting innovation.
#### ⛓️ Current Blockchain Limitations
* **High Latency** – Transactions take seconds or minutes to confirm.
* **Scalability Issues** – Congestion leads to high gas fees and slow execution.
* **Off-Chain Dependencies** – Many DeFi platforms rely on centralized price oracles.
#### ⚡ MagicBlock's Solution
* **Real-Time Settlement** – Transactions finalize instantly with low latency.
* **Fully Transparent** – Programs provide auditable, tamper-proof execution.
* **Seamless Composability** – Financial applications interconnect permissionlessly.
## Example: Ultra-Low Latency DeFi
Learn more about building and integrating DeFi with MagicBlock!
Try out our live demo showcasing Pyth price data streams with MagicBlock
# Games
Source: https://docs.magicblock.gg/pages/get-started/use-cases/games
Unlock the next era of real-time on-chain gaming
#### 🔴 Problems with Gaming
* **Lack of True Ownership** – Game assets are controlled by companies.
* **Shut Down Risk** – Servers go offline, and players lose progress.
* **Closed Ecosystems** – No interoperability between game economies.
#### ⛓️ Current Blockchain Limitations
* **Slow Gameplay** – High latency prevents real-time interactions.
* **High Gas Costs** – Transactions are expensive for frequent in-game actions.
* **Limited Developer Tooling** – Complex blockchain development.
#### ⚡ MagicBlock's Solution
* **True Digital Ownership** – Players own and trade assets without restrictions.
* **Real-Time Gameplay** – Instant, gas-efficient transactions for smooth experiences.
* **Composable Worlds** – Game logic and economies can be extended by anyone.
## Example: MagicBlock Labs - Generals
### Abstract
In order to get a full birds eye view of all moving pieces involved with using MagicBlock, we provide a real-life example of a fully-fledged game accelerated by the MagicBlock Engine.
Learn more about building and integrating games with MagicBlock!
Streamline your workflow with Solana Unity SDK, SOAR, Session Keys, and more.
### Architecture
There are a few main components involved when building with MagicBlock:
* **Solana**, this will be `mainnet-beta` or `devnet` solana public chain's RPC
* This is where the smart contracts will be deployed and fetched from
* This is where the final and partial state of your game will be settled on
* **Ephemeral Rollups**, this is the MagicBlock validator RPC
* This is where the transactions for your game will be run on
* This is already deployed by MagicBlock and node operators (you can just use it as-is)
* **The Backend**, this is the source code for the smart contract of the game
* Those smart contracts will then be deployed on the regular solana's chain
* Source code is available: [HERE](https://github.com/magicblock-labs/solana-generals/tree/main/backend)
* **The Frontend**, this is the User Interface of the game
* The UI will be fetching state from both the chain and the ephemeral
* The UI will be sending transaction to both the chain and the ephemeral
* Source code is available: [HERE](https://github.com/magicblock-labs/solana-generals/tree/main/frontend)
### Important processes
Browsing through the codebase will help understanding the high-level logic for setting up the ephemeral games.
### When the user creates a new game
When the user opens the "Create" page, we run create a new game: [HERE](https://github.com/magicblock-labs/solana-generals/blob/main/frontend/src/states/gameCreate.ts)
1. We first create a new account on chain, like a regular solana smart contract
2. We then delegate this new account to our ephemeral validator
3. We can then send all our game transactions directly to the ephemeral's RPC after that
1. We generate the map (using a transaction sent to the ephemeral's RPC)
2. Wait for the players to join
3. Start the game when all player joined
#### When a player joins the game
When the user opens the main page, we fetch the list of existing games from the chain: [HERE](https://github.com/magicblock-labs/solana-generals/blob/main/frontend/src/states/gameList.ts)
1. When the user joined the game's page, we start listening to the game's state inside the ephemeral: [HERE](https://github.com/magicblock-labs/solana-generals/blob/main/frontend/src/states/gameListen.ts)
2. We update the UI to display the map every time the game changes inside of the ephemeral, you can find the code for the page logic: [HERE](https://github.com/magicblock-labs/solana-generals/blob/main/frontend/src/components/page/PagePlay.tsx)
3. When the user executes a move on the map, we send the command transaction through directly in the ephemeral: [HERE](https://github.com/magicblock-labs/solana-generals/blob/main/frontend/src/states/gameSystemCommand.ts)
4. This will update the state of the game inside of the ephemeral, notifying all players and updating the UI immediately
### Recap
Making a game using MagicBlock is very similar to making a game on Solana.
The only difference is the delegation process:
* Once you setup the accounts on Solana, you can `delegate` the accounts so that they can be used inside of MagicBlock Engine
* Once the accounts are delegated, you can send all transactions involving those accounts to the Ephemeral Rollups
* Those transactions running inside of the Ephemeral session run in real time and can be free
* Once you're done with the game's session, you can `undelegate` the accounts you need to use on Solana again
# Introduction
Source: https://docs.magicblock.gg/pages/get-started/use-cases/introduction
Discover Use Cases of Unstoppable Applications
We are entering an era where **centralized servers are no longer needed**—everything can be built **permanently on real-time, composable, decentralized blockchains**. The time is over with:
* ⏳ **Delays in transaction finality**
* 💰 **High fees for execution**.
* ⚖️ **Bottlenecks limiting adoption**
MagicBlock **accelerates** the benefits of **Solana’s speed and efficiency**, enabling **real-time zero-cost on-chain applications** that were previously impossible, while maintaining **composability** and **verifiability**.
With its innovative approach, MagicBlock unlocks **new use cases** in:
* **Finance & Payments** – Instant transactions with minimal fees.
* **AI & Automation** – On-chain machine learning and data processing.
* **Gaming & Metaverse** – Seamless, real-time interactions with full transparency.
* **DePIN** – Enabling zero-cost coordination of **real-world assets**, from **wireless networks** to **energy grids** and **sensor data**.
With **fast, scalable, and secure on-chain execution**, industries can **redefine what’s possible**—and MagicBlock is leading the way.
🚀 **Let’s build the future—real-time, verifiable, and unstoppable!**
## Explore and learn from use cases
Build real-time DeFi apps!
Build real-time games!
Build fast responsive AI agents!
Build real-time payment!
Build real-time zero-cost infrastructure!
Build private, verifiable applications!
# Payments
Source: https://docs.magicblock.gg/pages/get-started/use-cases/payments
Unlock the next era of real-time on-chain payments
#### 🔴 Problems with Payments
* **High Fees & Delays** – Cross-border payments are expensive and slow.
* **Censorship & Restrictions** – Payment networks can block transactions.
* **Lack of Programmability** – No automation for conditional payments.
#### ⛓️ Current Blockchain Limitations
* **Transaction Bottlenecks** – Network congestion causes delays.
* **High Costs** – Fees can spike unpredictably.
* **Merchant Adoption** – Crypto payments lack seamless merchant integration.
#### ⚡ MagicBlock's Solution
* **Instant, Low-Cost Transfers** – No middlemen, real-time finality.
* **Censorship-Resistant** – Payments cannot be blocked or reversed.
* **Programmable Money** – Programs automate payments, escrow, and subscriptions.
## Example: Ultra-Low Latency On-Chain Payments
Learn more about building and integrating on-chain payments with MagicBlock!
# Privacy
Source: https://docs.magicblock.gg/pages/get-started/use-cases/privacy
Unlock private, verifiable computation
#### 🔴 Problems with Privacy
* **Data Exposure** – Sensitive information is on the public ledger.
* **Centralized Trust** – Users must trust third parties with their private data.
* **Lack of Verifiability** – No way to prove computations were performed correctly without revealing data.
#### ⛓️ Current Blockchain Limitations
* **Public Execution** – All transactions and smart contract state are visible on-chain.
* **MEV Exploitation** – Front-running and sandwich attacks exploit transaction visibility.
* **Compliance Barriers** – Regulatory requirements often conflict with public transparency.
#### ⚡ MagicBlock's Solution
* **Trusted Execution Environments (TEEs)** on Intel TDX – Secure, isolated computing environments that protect data during processing.
* **Private State Execution** – Run computations on sensitive data without exposing it to the network.
* **Verifiable Privacy** – Cryptographic proofs ensure correct execution while maintaining confidentiality.
## Explore TEE Docs
What PER is and why TEEs
Permission groups and access
Add permissions via CPI
Attestation, challenge, and access
# AI Dev Skill
Source: https://docs.magicblock.gg/pages/overview/additional-information/ai-dev-skill
MagicBlock Ephemeral Rollups development skill for AI coding agents, with MagicBlock-specific patterns for delegation, fee economics, Magic Actions, cranks, VRF, lamports top-up, the Ephemeral SPL Token lifecycle, private payments with swaps, and dual-connection architecture.
View the repository, installation steps, and source files for the skill.
## Quick install
```bash theme={null}
npx skills add https://github.com/magicblock-labs/magicblock-dev-skill
```
## What It Is
The MagicBlock Dev Skill is an AI development skill that packages MagicBlock-specific patterns into a reusable workflow that activates when you ask for MagicBlock or Ephemeral Rollups help. Instead of re-explaining the same integration details in every prompt, the skill gives your coding agent structured guidance for building on MagicBlock.
It is designed for teams working on:
* MagicBlock Ephemeral Rollups integration
* Delegating and undelegating Solana accounts
* Dual-connection Solana + MagicBlock architectures
* High-performance, low-latency transaction flows
* Cranks for recurring automated transactions
* VRF for provable randomness
* Magic Actions — base-layer instructions chained atomically to an ER commit
* Topping up a delegated account's lamports via `lamportsDelegatedTransferIx`
* Delegation deposits, refunds, commit limits, and fees paid through `magic_fee_vault`
* Ephemeral SPL Token lifecycle — delegate, transfer, undelegate, and withdraw eATAs across the eATA + Global Vault model, via the SDK or the direct [Ephemeral SPL Token API](/pages/ephemeral-spl-token/api-reference/introduction)
* Private payments — deposits, transfers, withdrawals, and swaps via the Payments API, including the challenge/login bearer-token flow for private reads
* Gaming and real-time app development on Solana
* Anchor and TypeScript-based integrations
## Installation
### Quick install
```bash theme={null}
npx skills add https://github.com/magicblock-labs/magicblock-dev-skill
```
### Manual install
```bash theme={null}
git clone https://github.com/magicblock-labs/magicblock-dev-skill
cd magicblock-dev-skill
./install.sh
```
By default, `./install.sh` installs the skill to both personal skill directories:
* `~/.claude/skills/magicblock`
* `${CODEX_HOME:-~/.codex}/skills/magicblock`
### Targeting specific agents
Global / per-user targets:
```bash theme={null}
./install.sh --claude
./install.sh --codex
```
Project-scoped targets (always install into the current directory):
```bash theme={null}
./install.sh --cursor # .cursor/rules/magicblock.mdc
./install.sh --windsurf # .windsurf/rules/magicblock.md
./install.sh --cline # .clinerules/magicblock.md
./install.sh --continue # .continue/rules/magicblock.md
./install.sh --agents-md # ./AGENTS.md
```
Combined:
```bash theme={null}
./install.sh --all # everything for the current project
./install.sh --project # Claude + Codex into .claude/.codex inside the project
./install.sh --path /custom/path/magicblock
```
The single-file targets for Cursor, Windsurf, Cline, Continue, and `AGENTS.md` are generated from `dist/` artifacts. `install.sh` runs `./build.sh` automatically if `dist/` is missing.
### Building dist artifacts manually
```bash theme={null}
./build.sh
```
Produces:
* `dist/AGENTS.md` - full flattened skill (`SKILL.md` plus all references)
* `dist/system-prompt.md` - trimmed `SKILL.md` plus reference URLs for chat-only platforms
* `dist/magicblock.cursor.mdc` - Cursor-formatted rule with `.mdc` frontmatter
* `dist/magicblock.zip` - zipped `skill/` folder for Claude.ai upload
## Usage
The skill activates automatically when you ask about MagicBlock or Ephemeral Rollups.
* In Claude Code, you can also invoke it directly with `/magicblock`.
* In Codex, mention it explicitly by name, for example: `use the magicblock skill`.
* In Cursor / Windsurf / Cline / Continue, the rule's description triggers contextually when you mention MagicBlock topics.
* For chat-only platforms, load `dist/system-prompt.md` once as the system prompt, custom instructions, or project context.
Examples:
```text theme={null}
Add delegation hooks to my player account
Change my roll_dice function to use VRF
Set up a crank that updates game state every 100ms
Add a Magic Action that updates my onchain leaderboard after every commit
Top up my delegated fee payer with lamports
Build a private USDC transfer flow using the Payments API
Help me integrate MagicBlock into my Anchor program
```
## What the Skill Adds
The skill goes beyond a simple prompt template. Its main entrypoint and supporting references guide the agent toward MagicBlock-specific implementation details such as:
* When to use the base layer connection vs. the ephemeral rollup connection
* How to structure delegation, commit, and undelegation flows correctly with `MagicIntentBundleBuilder` (SDK 0.11+)
* Common Anchor patterns for `#[ephemeral]`, `#[delegate]`, and `#[commit]`
* Magic Actions: scheduling base-layer instructions inside an ER transaction via `MagicIntentBundleBuilder.add_post_commit_actions(...)` so they execute atomically once the commit is sealed back
* Topping up delegated accounts with `lamportsDelegatedTransferIx` (single-use lamports PDA, submitted on base layer, credited on the ER)
* Understanding what the delegation deposit pays for, what gets refunded, why simple commits stop after 10, and why `magic_fee_vault` starts charging the delegated payer on commit 26
* Ephemeral SPL Token integration — the delegate → transfer → undelegate → withdraw lifecycle, the two integration models (SDK helpers vs. the lower-level `ephemeral-spl-api` program surface), and lifecycle gotchas like `initVaultIfMissing`, one undelegate per transaction, and waiting for commits before withdrawal
* Private Ephemeral Rollups (PER) patterns — delegating the permission account alongside the permissioned account so member updates execute on the ER
* Private Payments API workflows including the challenge → login → bearer-token flow for reading private balances and the public/private swap modes
* VRF and crank setup for real-time apps and games
* Environment variables, versions, and dependencies for MagicBlock development
For the full skill, source files, and installation instructions, visit the [MagicBlock Dev Skill repository](https://github.com/magicblock-labs/magicblock-dev-skill).
# Pricing
Source: https://docs.magicblock.gg/pages/overview/additional-information/pricing
MagicBlock’s pricing is modeled after decentralized cloud infrastructure. The goal is to keep computing costs **predictable for developers**, while still providing **flexibility for enterprises** that need priority access or dedicated resources.
***
## Product Pricing
**Public nodes** make it simple to start building. ER transactions cost 0 in the current release. When an account is undelegated, MagicBlock takes the session and commit charges from the deposit funded during delegation. Longer sessions can also use a delegated fee payer.
| Fee type |
Amount (SOL) |
Description |
| Base fee |
0 |
Per ER transaction in the current release |
| Session fee |
0.0003 |
Taken from the delegation deposit when the account is undelegated |
| Commit fee |
0.0001 |
For each commit after the first; see the detailed guide for limits and refunds |
**Dedicated nodes** are ideal for enterprises and high-scale teams. They provide maximum reliability, predictable costs, and MEV protection with your own dedicated infrastructure.
[Learn more about ER →](/pages/ephemeral-rollups-ers/how-to-guide/quickstart)
[Learn how deposits, commit limits, fees, and refunds work →](/pages/ephemeral-rollups-ers/introduction/fees-and-commit-economics)
### ER Cost Simulator: 30-Days
This is a simple estimate. It treats every commit you enter as a paid commit. It does not include
free thresholds, deposit limits, Base Actions, callbacks, or refundable Ephemeral Account storage.
Use the detailed guide when budgeting for a real account.
Private Ephemeral Rollup supports custom private computation defined by your smart contract.
Custom PER logic uses standard ER pricing. [See Ephemeral Rollup pricing →](#product-pricing)
The VRF service provides **provably fair randomness on-chain**. Fees cover proof generation + posting on-chain.
> ⚠️ Note: Costs do not include the transaction to request randomness. On ER transactions are free, on Solana transactions may vary based on your priority fees.
| VRF type |
Amount (SOL) |
Description |
| ER (\<50 ms) |
Free |
Per randomness request |
| Solana (\<500 ms) |
0.0008 |
Per randomness request |
| Solana (1/2 seconds) |
0.0005 |
Per randomness request |
### VRF Cost Simulator: 30-Days
[Learn more about Solana VRF →](/pages/verifiable-randomness-functions-vrfs/introduction/solana-vrf)
Private Payment API make it simple to send private stablecoin transfers on Solana Mainnet and/or on an a Private ER.
| Payment type |
Fixed fee (SOL) |
Volume fee |
| Solana Mainnet |
0.002 |
0.1% |
Explore the Private Payments API endpoints
***
## Customer Support
For support to **run your own nodes**, reach out to:
📧 [development@magicblock.xyz](mailto:development@magicblock.xyz)
Execute real-time, zero-fee transactions securely on Solana.
Protect sensitive data with compliance — built on top of Ephemeral Rollups.
Move SPL tokens at rollup speed — public or private transfers, swaps, and private payments for trading and DeFi apps.
Combine real-time execution, session keys, token custody, price feeds, automation, and settlement.
Add provably fair onchain randomness to games, raffles, and real-time apps.
Access low-latency onchain price feeds for trading and DeFi.
# Request For Products
Source: https://docs.magicblock.gg/pages/overview/additional-information/request-for-products
Go beyond tutorials by working on real-world problems and community challenges. The Request for Products (RFPs) showcase open ideas and challenges waiting for contributors like you.
Build on community ideas and solve meaningful challenges.
# Security & Audits
Source: https://docs.magicblock.gg/pages/overview/additional-information/security-and-audits
At MagicBlock, security is a top priority. All our core programs have undergone professional audits to ensure robustness and safety for our users.
***
## [Delegation Program](/pages/ephemeral-rollups-ers/introduction/why)
* **GitHub:** [https://github.com/magicblock-labs/delegation-program](https://github.com/magicblock-labs/delegation-program)
* **Program Id:** `DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh`
* **Audit Firm:** Halborn
* **Audit Report:** [View audit report](https://github.com/magicblock-labs/delegation-program/tree/429f86fd56f5e8956cf132da0063b971346f6c67/security_audits)
* **Details:** Comprehensive security review of the delegation program, covering account delegation, transaction safety, and program logic.
***
## [Solana VRF Program](/pages/verifiable-randomness-functions-vrfs/introduction/solana-vrf)
* **GitHub:** [https://github.com/magicblock-labs/solana-vrf](https://github.com/magicblock-labs/solana-vrf)
* **Program Id:** `Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz`
* **Audit Firm:** Zenith
* **Audit Report:** [View audit report](https://github.com/magicblock-labs/solana-vrf/blob/main/security_audits/2025-08-06%20VRF%20Program%20Audit%20Report%20by%20Zenith.pdf)
* **Details:** Detailed audit of the Verifiable Randomness Function (VRF) program, ensuring secure randomness generation and safe integration with your program.
***
## [Permission Program](/pages/private-ephemeral-rollups-pers/introduction/onchain-privacy)
* **GitHub:** Currently Private
* **Program Id:** `BTWAqWNBmF2TboMh3fxMJfgR16xGHYD7Kgr2dPwbRPBi`
* **Audit:** TBC
***
> These audits reflect our commitment to building secure, reliable programs for the MagicBlock ecosystem.
# Systems Status
Source: https://docs.magicblock.gg/pages/overview/additional-information/system-status
# Whitepaper
Source: https://docs.magicblock.gg/pages/overview/additional-information/whitepaper
Ephemeral Rollups Are All You Need (https://arxiv.org/abs/2311.02650)
**Authors:** Gabriele Picco, Andrea Fortugno
We propose a framework that leverages the **Solana Virtual Machine (SVM)** to scale fully on-chain applications without state fragmentation or compromised trust assumptions.
To enhance scalability and resource optimization, we introduce the concept of **Ephemeral Rollups (ERs)**:
* Dedicated runtimes customizable for higher operational speed.
* Configurable ticking mechanisms.
* Provable sessions and gasless transactions.
* Achieves scalability without compromising composability.
***
> This whitepaper outlines the foundations of MagicBlock’s architecture and the innovative solutions we propose for fully on-chain ecosystems.
# Products
Source: https://docs.magicblock.gg/pages/overview/products
MagicBlock is your backend for the new financial internet. Upgrade your Solana program and build composable applications with real-time UX, on-chain privacy, and verifiable randomness.
Execute real-time, zero-fee transactions securely on Solana.
Protect sensitive data with compliance — built on top of Ephemeral Rollups.
Move SPL tokens at rollup speed — public or private transfers, swaps, and private payments for trading and DeFi apps.
Combine real-time execution, session keys, token custody, price feeds, automation, and settlement.
Add provably fair onchain randomness to games, raffles, and real-time apps.
Access low-latency onchain price feeds for trading and DeFi.
# Private Payments
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/introduction
Private, compliant onchain payments on MagicBlock — powered by Ephemeral SPL Tokens.
***
## Private Payments
**Private Payments** let you add private, compliant onchain transfers to your app — shielded amounts,
destinations, and timing, with stealth handles and queued settlement.
Private Payments are powered by **Ephemeral SPL Tokens**: the same fast token primitive used for public
transfers, run with private `visibility`. All of the documentation — concepts, the SDK quickstart, the
private-payments guide, and the full REST/MCP API — lives in the dedicated **Ephemeral SPL Token**
section.
Private transfers, stealth handles, and queued settlement — end to end.
The token primitive behind private payments.
Deposit, transfer, withdraw, balances, stealth pools, swap, and auth.
Integrate ephemeral SPL tokens in your own program and client.
Looking for the full API? Head to the [**Ephemeral SPL
Token**](/pages/ephemeral-spl-token/api-reference/introduction) tab — it's the single source of truth
for every endpoint and guide.
# Access Control
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/how-to-guide/access-control
Learn how to manage fine-grained access control and member permissions in Private Ephemeral Rollups.
**Building with an AI coding agent?** Install the MagicBlock Dev Skill to give your agent MagicBlock-specific patterns — delegation flows, Magic Actions, cranks, VRF, and more.
**Hit an error?** Ask your coding agent with the skill installed, not the docs assistant. The assistant only sees the docs, so it cannot debug your code.
Quick install for Claude Code:
```bash theme={null}
npx skills add https://github.com/magicblock-labs/magicblock-dev-skill
```
Using Cursor, Codex, Windsurf, Cline, or another agent? See the [AI Dev Skill](/pages/overview/additional-information/ai-dev-skill) page for all install targets.
***
On-chain Permission Management (Coming soon)
SDK for Private Ephemeral Rollups
***
## Overview
Private Ephemeral Rollups are [Ephemeral Rollups](/pages/ephemeral-rollups-ers/introduction/why) that enable fine-grained permission over permissioned accounts in a [Trusted Execution Environment](/pages/private-ephemeral-rollups-pers/introduction/onchain-privacy) with [compliance](/pages/private-ephemeral-rollups-pers/introduction/compliance-framework) at its heart. Each permission account maintains a list of members with specific flags that determine what actions they can perform.
### Key Concepts
* **Permission Account**: A PDA that stores access control rules for a specific account
* **Members**: Addresses granted specific permissions via flags
* **Flags**: Bitmasks that define what a member can do (authority, view logs, view balances, etc.)
* **Public Permissions**: When members are set to `None`, the permissioned account becomes temporarily visible
***
## Member Flags
Member flags define fine-grained permissions for each member. Flags can be combined using bitwise OR to grant multiple permissions.
**Flag Descriptions:**
* **AUTHORITY**: Allows a member to update and delegate permission settings, add/remove other members, and update member flags.
* **TX\_LOGS**: Allows a member to view transaction execution logs.
* **TX\_BALANCES**: Allows a member to view account balance changes.
* **TX\_MESSAGE**: Allows a member to view transaction message data.
* **ACCOUNT\_SIGNATURES**: Allows a member to view account signatures
```rust theme={null}
use ephemeral_rollups_sdk::access_control::structs::{
Member,
AUTHORITY_FLAG,
TX_LOGS_FLAG,
TX_BALANCES_FLAG,
TX_MESSAGE_FLAG,
ACCOUNT_SIGNATURES_FLAG,
};
// Set flags by combining them with bitwise OR
let flags = AUTHORITY_FLAG | TX_LOGS_FLAG;
// Create a member with combined flags
let mut member = Member {
flags,
pubkey: user_pubkey,
};
// Check if member has a specific flag using bitwise AND
let is_authority = (member.flags & AUTHORITY_FLAG) != 0;
let can_see_logs = (member.flags & TX_LOGS_FLAG) != 0;
// Use helper methods to set/remove flags
member.set_flags(TX_BALANCES_FLAG); // Add a flag
member.remove_flags(TX_LOGS_FLAG); // Remove a flag
```
```rust theme={null}
use ephemeral_rollups_pinocchio::types::{Member, MemberFlags};
use pinocchio::Address;
// Create and set flags using individual methods
let mut flags = MemberFlags::new();
flags.set(MemberFlags::AUTHORITY);
flags.set(MemberFlags::TX_LOGS);
flags.set(MemberFlags::TX_BALANCES);
// Create a member with flags
let member = Member {
flags,
pubkey: user_address,
};
// Remove a flag
flags.remove(MemberFlags::TX_LOGS);
// Create flags from individual boolean values
let flags = MemberFlags::from_acl_flags(
true, // authority
true, // tx_logs
false, // tx_balances
true, // tx_message
false, // account_signatures
);
// Convert flags to byte value
let flag_byte = flags.to_acl_flag_byte();
// Create flags from byte value
let flags = MemberFlags::from_acl_flag_byte(flag_byte);
```
```typescript theme={null}
import { PublicKey } from "@solana/web3.js";
import {
AUTHORITY_FLAG,
TX_LOGS_FLAG,
TX_BALANCES_FLAG,
TX_MESSAGE_FLAG,
ACCOUNT_SIGNATURES_FLAG,
type Member,
} from "@magicblock-labs/ephemeral-rollups-sdk";
// Set flags by combining them with bitwise OR
const flags = AUTHORITY_FLAG | TX_LOGS_FLAG;
// Create a member with combined flags
const member: Member = {
flags,
pubkey: new PublicKey(userAddress),
};
// Check if a flag is present using bitwise AND
const isAuthority = (member.flags & AUTHORITY_FLAG) !== 0;
const canSeeLogs = (member.flags & TX_LOGS_FLAG) !== 0;
const canSeeBalances = (member.flags & TX_BALANCES_FLAG) !== 0;
// Add a flag to existing flags
const updatedFlags = member.flags | TX_BALANCES_FLAG;
// Remove a flag from existing flags
const removedFlags = member.flags & ~TX_LOGS_FLAG;
```
```typescript theme={null}
import {
AUTHORITY_FLAG,
TX_LOGS_FLAG,
TX_BALANCES_FLAG,
TX_MESSAGE_FLAG,
ACCOUNT_SIGNATURES_FLAG,
isAuthority,
canSeeTxLogs,
canSeeTxBalances,
canSeeTxMessages,
canSeeAccountSignatures,
type Member,
} from "@magicblock-labs/ephemeral-rollups-sdk";
// Set flags by combining them with bitwise OR
const flags = AUTHORITY_FLAG | TX_LOGS_FLAG | TX_BALANCES_FLAG;
// Create a member with combined flags
const member: Member = {
flags,
pubkey: userAddress,
};
// Use helper functions to check specific permissions
const canModifyPermission = isAuthority(member, userAddress);
const canViewLogs = canSeeTxLogs(member, userAddress);
const canViewBalances = canSeeTxBalances(member, userAddress);
const canViewMessages = canSeeTxMessages(member, userAddress);
const canViewSignatures = canSeeAccountSignatures(member, userAddress);
// Add a flag to existing member
const updatedFlags = member.flags | TX_MESSAGE_FLAG;
// Remove a flag from existing member
const removedFlags = member.flags & ~TX_LOGS_FLAG;
```
***
## Ephemeral Permission
`EphemeralPermission` accounts live entirely on the Ephemeral Rollup and are
paid for by the delegated PDA — no base-layer permission account to create,
delegate, or commit-and-undelegate. Three CPI ops cover the full lifecycle:
**Create**, **Update**, **Close** — all PDA-signed by the data account on
the ER, via MagicBlock's Permission Program `ACLseoPoyC3cBqoUtkbjZ4aDrkurZW86v19pXz2XQnp1`.
**Prerequisite — delegate the data PDA.** Only the data account is delegated to
the TEE validator (on the base layer, via MagicBlock's Delegation Program
`DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh`). Once delegated, the PDA signs
all three permission ops on the ER using its program seeds and pays the
ephemeral permission rent — so it must be pre-funded at `initialize` time. See
[Quickstart](/pages/private-ephemeral-rollups-pers/how-to-guide/quickstart#2-delegate-and-create-permission)
for the end-to-end flow.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Create Ephemeral Permission}>
Initialize a new `EphemeralPermission` account on the ER with initial
members and the privacy flag. Idempotent — skip if it already exists.
Update Ephemeral Permission}>
Toggle the privacy flag, or add / remove / re-flag members. Updates take
effect immediately on the ER.
Close Ephemeral Permission}>
Close the `EphemeralPermission` account on the ER and refund the rent to
the data PDA when the permission is no longer needed.
***
## Ephemeral Permission Operations
Initialize a new `EphemeralPermission` account on the ER via
`CreateEphemeralPermissionCpi`. Payer = the delegated data PDA, which
signs with its program seeds and covers the rent from the lamports
pre-funded at `initialize` time.
```rust theme={null}
use ephemeral_rollups_sdk::access_control::{
instructions::CreateEphemeralPermissionCpi,
structs::{EphemeralMembersArgs, Member},
};
// Counter PDA pays for its own permission rent (it carries lamports onto the ER
// after delegation and signs as PDA via seeds).
let signers = [
COUNTER_SEED,
ctx.accounts.counter.authority.as_ref(),
&[ctx.bumps.counter],
];
CreateEphemeralPermissionCpi {
payer: ctx.accounts.counter.to_account_info(), // pays ephemeral rent
permissioned_account: ctx.accounts.counter.to_account_info(), // what the permission gates
permission: ctx.accounts.permission.to_account_info(),
vault: ctx.accounts.ephemeral_vault.to_account_info(),
magic_program: ctx.accounts.magic_program.to_account_info(),
permission_program: ctx.accounts.permission_program.to_account_info(),
args: EphemeralMembersArgs {
is_private: false, // start public — flip via UpdateEphemeralPermission
members: vec![],
},
}
.invoke_signed(&[&signers])?;
```
```rust theme={null}
use ephemeral_rollups_sdk::access_control::{
instructions::CreateEphemeralPermissionCpi,
structs::{EphemeralMembersArgs, Member},
};
// `permissioned_account` (here a counter PDA) signs as PDA via seeds; pass the
// same seeds you used for `find_program_address` to derive it.
let seeds: &[&[u8]] = &[
COUNTER_SEED,
permissioned_account_authority.as_ref(),
&[bump],
];
CreateEphemeralPermissionCpi {
payer: &counter_account_info, // pays ephemeral rent
permissioned_account: &counter_account_info, // what the permission gates
permission: &permission_account_info,
vault: &ephemeral_vault_account_info,
magic_program: &magic_program_account_info,
permission_program: &permission_program_account_info,
args: EphemeralMembersArgs {
is_private: false, // start public — flip via UpdateEphemeralPermission
members: vec![],
},
}
.invoke_signed(&[seeds])?;
```
```rust theme={null}
use ephemeral_rollups_pinocchio::acl::{
CreateEphemeralPermission, EphemeralMembersArgs, Member,
};
use pinocchio::cpi::{Seed, Signer};
// Buffer size: discriminator (8) + EphemeralMembersArgs body.
// 64 bytes covers up to 1 member with slack for future Update calls.
const PERMISSION_CPI_BUF: usize = 64;
// PDA-signed CPI — the counter PDA pays rent and authorizes the permission.
let bump_seed = [bump];
let seeds_array: [Seed; 3] = [
Seed::from(b"counter"),
Seed::from(authority.address().as_ref()),
Seed::from(&bump_seed),
];
let signer = Signer::from(&seeds_array);
let members: [Member; 0] = []; // start public; toggle via Update
CreateEphemeralPermission {
payer: counter_account,
permissioned_account: counter_account,
permission,
vault,
magic_program,
permission_program,
args: EphemeralMembersArgs {
is_private: false,
members: &members,
},
}
.invoke_signed::(&[signer])?;
```
```typescript theme={null}
import {
MAGIC_PROGRAM_ID,
PERMISSION_PROGRAM_ID,
EPHEMERAL_VAULT_ID,
} from "@magicblock-labs/ephemeral-rollups-sdk";
import { pipe, createTransactionMessage, appendTransactionMessageInstructions } from "@solana/kit";
// EphemeralPermissions are created on the ER by the delegated PDA (via the
// user-program's wrapper instruction). Submit to the ER connection, not base.
const initIx = await counterProgram.methods
.initPermission()
.accountsPartial({
authority: tempKeypair.address,
counter: counterPda,
permission: permissionPda,
permissionProgram: PERMISSION_PROGRAM_ID,
ephemeralVault: EPHEMERAL_VAULT_ID,
magicProgram: MAGIC_PROGRAM_ID,
})
.instruction();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions([initIx], tx),
);
const sig = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[tempKeypair],
{ commitment: "confirmed" },
);
console.log("init_permission tx:", sig);
```
```typescript theme={null}
import {
MAGIC_PROGRAM_ID,
PERMISSION_PROGRAM_ID,
EPHEMERAL_VAULT_ID,
} from "@magicblock-labs/ephemeral-rollups-sdk";
import { Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
// EphemeralPermissions are created on the ER by the delegated PDA (via the
// user-program's wrapper instruction). Submit to the ER connection, not base.
const initIx = await counterProgram.methods
.initPermission()
.accountsPartial({
authority: tempKeypair.publicKey,
counter: counterPda,
permission: permissionPda,
permissionProgram: PERMISSION_PROGRAM_ID,
ephemeralVault: EPHEMERAL_VAULT_ID,
magicProgram: MAGIC_PROGRAM_ID,
})
.instruction();
const tx = new Transaction().add(initIx);
const sig = await sendAndConfirmTransaction(ephemeralConnection, tx, [tempKeypair]);
console.log("init_permission tx:", sig);
```
**Use Cases:**
* Bootstrap access control for a newly delegated PDA on the ER
* Start public (`is_private: false`, empty members) and tighten later via Update
[⬆️ Back to Top](#ephemeral-permission)
Flip the privacy flag and rewrite the member list via
`UpdateEphemeralPermissionCpi`. Rebuild the full member list every call
(including the authority) so the data PDA can never lock itself out.
```rust theme={null}
use ephemeral_rollups_sdk::access_control::{
instructions::UpdateEphemeralPermissionCpi,
structs::{
EphemeralMembersArgs, Member,
TX_LOGS_FLAG, TX_MESSAGE_FLAG, TX_BALANCES_FLAG,
},
};
let signers = [
COUNTER_SEED,
ctx.accounts.counter.authority.as_ref(),
&[ctx.bumps.counter],
];
// When private, only listed members can read ER state via the TEE.
// Empty member list + is_private=false = fully public.
let members = if is_private {
vec![Member {
flags: TX_LOGS_FLAG | TX_MESSAGE_FLAG | TX_BALANCES_FLAG,
pubkey: ctx.accounts.counter.authority,
}]
} else {
vec![]
};
UpdateEphemeralPermissionCpi {
payer: ctx.accounts.counter.to_account_info(),
permissioned_account: ctx.accounts.counter.to_account_info(),
permission: ctx.accounts.permission.to_account_info(),
vault: ctx.accounts.ephemeral_vault.to_account_info(),
magic_program: ctx.accounts.magic_program.to_account_info(),
permission_program: ctx.accounts.permission_program.to_account_info(),
authority: ctx.accounts.counter.to_account_info(),
authority_is_signer: false, // PDA signs via the seeds above
args: EphemeralMembersArgs { is_private, members },
}
.invoke_signed(&[&signers])?;
```
```rust theme={null}
use ephemeral_rollups_sdk::access_control::{
instructions::UpdateEphemeralPermissionCpi,
structs::{
EphemeralMembersArgs, Member,
TX_LOGS_FLAG, TX_MESSAGE_FLAG, TX_BALANCES_FLAG,
},
};
let seeds: &[&[u8]] = &[
COUNTER_SEED,
permissioned_account_authority.as_ref(),
&[bump],
];
// When private, only listed members can read ER state via the TEE.
let members = if is_private {
vec![Member {
flags: TX_LOGS_FLAG | TX_MESSAGE_FLAG | TX_BALANCES_FLAG,
pubkey: permissioned_account_authority,
}]
} else {
vec![]
};
UpdateEphemeralPermissionCpi {
payer: &counter_account_info,
permissioned_account: &counter_account_info,
permission: &permission_account_info,
vault: &ephemeral_vault_account_info,
magic_program: &magic_program_account_info,
permission_program: &permission_program_account_info,
authority: &counter_account_info,
authority_is_signer: false, // PDA signs via the seeds above
args: EphemeralMembersArgs { is_private, members },
}
.invoke_signed(&[seeds])?;
```
```rust theme={null}
use ephemeral_rollups_pinocchio::acl::{
EphemeralMembersArgs, Member, MemberFlags, UpdateEphemeralPermission,
};
use pinocchio::cpi::{Seed, Signer};
const PERMISSION_CPI_BUF: usize = 64;
let bump_seed = [bump];
let seeds_array: [Seed; 3] = [
Seed::from(b"counter"),
Seed::from(authority.address().as_ref()),
Seed::from(&bump_seed),
];
let signer = Signer::from(&seeds_array);
// Read the on-chain Counter to grab `authority` — the sole "private" member.
let counter_authority = {
let data = counter_account.try_borrow()?;
Counter::load(&data)?.authority
};
let single_member = [Member {
flags: MemberFlags::from_acl_flag_byte(
MemberFlags::TX_LOGS | MemberFlags::TX_MESSAGE | MemberFlags::TX_BALANCES,
),
pubkey: counter_authority,
}];
let members: &[Member] = if is_private { &single_member } else { &[] };
UpdateEphemeralPermission {
payer: counter_account,
permissioned_account: counter_account,
permission,
vault,
magic_program,
permission_program,
authority: counter_account,
authority_is_signer: false, // PDA signs via the seeds above
args: EphemeralMembersArgs { is_private, members },
}
.invoke_signed::(&[signer])?;
```
```typescript theme={null}
import {
MAGIC_PROGRAM_ID,
PERMISSION_PROGRAM_ID,
EPHEMERAL_VAULT_ID,
} from "@magicblock-labs/ephemeral-rollups-sdk";
import { pipe, createTransactionMessage, appendTransactionMessageInstructions } from "@solana/kit";
// Toggle the `is_private` flag. Idempotent — the program rebuilds the member
// list every call so the authority never locks itself out.
const updateIx = await counterProgram.methods
.setPrivacy(isPrivate)
.accountsPartial({
authority: tempKeypair.address,
counter: counterPda,
permission: permissionPda,
permissionProgram: PERMISSION_PROGRAM_ID,
ephemeralVault: EPHEMERAL_VAULT_ID,
magicProgram: MAGIC_PROGRAM_ID,
})
.instruction();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions([updateIx], tx),
);
const sig = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[tempKeypair],
{ commitment: "confirmed" },
);
console.log("set_privacy tx:", sig);
```
```typescript theme={null}
import {
MAGIC_PROGRAM_ID,
PERMISSION_PROGRAM_ID,
EPHEMERAL_VAULT_ID,
} from "@magicblock-labs/ephemeral-rollups-sdk";
import { Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
// Toggle the `is_private` flag. Idempotent — the program rebuilds the member
// list every call so the authority never locks itself out.
const updateIx = await counterProgram.methods
.setPrivacy(isPrivate)
.accountsPartial({
authority: tempKeypair.publicKey,
counter: counterPda,
permission: permissionPda,
permissionProgram: PERMISSION_PROGRAM_ID,
ephemeralVault: EPHEMERAL_VAULT_ID,
magicProgram: MAGIC_PROGRAM_ID,
})
.instruction();
const tx = new Transaction().add(updateIx);
const sig = await sendAndConfirmTransaction(ephemeralConnection, tx, [tempKeypair]);
console.log("set_privacy tx:", sig);
```
**Use Cases:**
* Toggle `is_private` on demand (e.g. private play, public reveal)
* Add new viewers with `TX_LOGS | TX_MESSAGE | TX_BALANCES` flags
* Revoke a member by omitting them from the next call's member list
[⬆️ Back to Top](#ephemeral-permission)
Close the `EphemeralPermission` account on the ER via
`CloseEphemeralPermissionCpi`. Rent is refunded to the data PDA (the
original payer). Optional — only call when the permission is no longer
needed.
```rust theme={null}
use ephemeral_rollups_sdk::access_control::instructions::CloseEphemeralPermissionCpi;
let signers = [
COUNTER_SEED,
ctx.accounts.counter.authority.as_ref(),
&[ctx.bumps.counter],
];
// Refunds the permission's rent to `payer` (the counter PDA).
CloseEphemeralPermissionCpi {
payer: ctx.accounts.counter.to_account_info(),
permissioned_account: ctx.accounts.counter.to_account_info(),
permission: ctx.accounts.permission.to_account_info(),
vault: ctx.accounts.ephemeral_vault.to_account_info(),
magic_program: ctx.accounts.magic_program.to_account_info(),
permission_program: ctx.accounts.permission_program.to_account_info(),
authority: ctx.accounts.counter.to_account_info(),
authority_is_signer: false,
}
.invoke_signed(&[&signers])?;
```
```rust theme={null}
use ephemeral_rollups_sdk::access_control::instructions::CloseEphemeralPermissionCpi;
let seeds: &[&[u8]] = &[
COUNTER_SEED,
permissioned_account_authority.as_ref(),
&[bump],
];
// Refunds the permission's rent to `payer` (the counter PDA).
CloseEphemeralPermissionCpi {
payer: &counter_account_info,
permissioned_account: &counter_account_info,
permission: &permission_account_info,
vault: &ephemeral_vault_account_info,
magic_program: &magic_program_account_info,
permission_program: &permission_program_account_info,
authority: &counter_account_info,
authority_is_signer: false,
}
.invoke_signed(&[seeds])?;
```
```rust theme={null}
use ephemeral_rollups_pinocchio::acl::CloseEphemeralPermission;
use pinocchio::cpi::{Seed, Signer};
let bump_seed = [bump];
let seeds_array: [Seed; 3] = [
Seed::from(b"counter"),
Seed::from(authority.address().as_ref()),
Seed::from(&bump_seed),
];
let signer = Signer::from(&seeds_array);
// Refunds the permission's rent to `payer` (the counter PDA).
CloseEphemeralPermission {
payer: counter_account,
permissioned_account: counter_account,
permission,
vault,
magic_program,
permission_program,
authority: counter_account,
authority_is_signer: false,
}
.invoke_signed(&[signer])?;
```
```typescript theme={null}
import {
MAGIC_PROGRAM_ID,
PERMISSION_PROGRAM_ID,
EPHEMERAL_VAULT_ID,
} from "@magicblock-labs/ephemeral-rollups-sdk";
import { pipe, createTransactionMessage, appendTransactionMessageInstructions } from "@solana/kit";
// Refunds the permission's rent to the counter PDA.
const closeIx = await counterProgram.methods
.closePermission()
.accountsPartial({
authority: tempKeypair.address,
counter: counterPda,
permission: permissionPda,
permissionProgram: PERMISSION_PROGRAM_ID,
ephemeralVault: EPHEMERAL_VAULT_ID,
magicProgram: MAGIC_PROGRAM_ID,
})
.instruction();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions([closeIx], tx),
);
const sig = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[tempKeypair],
{ commitment: "confirmed" },
);
console.log("close_permission tx:", sig);
```
```typescript theme={null}
import {
MAGIC_PROGRAM_ID,
PERMISSION_PROGRAM_ID,
EPHEMERAL_VAULT_ID,
} from "@magicblock-labs/ephemeral-rollups-sdk";
import { Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
// Refunds the permission's rent to the counter PDA.
const closeIx = await counterProgram.methods
.closePermission()
.accountsPartial({
authority: tempKeypair.publicKey,
counter: counterPda,
permission: permissionPda,
permissionProgram: PERMISSION_PROGRAM_ID,
ephemeralVault: EPHEMERAL_VAULT_ID,
magicProgram: MAGIC_PROGRAM_ID,
})
.instruction();
const tx = new Transaction().add(closeIx);
const sig = await sendAndConfirmTransaction(ephemeralConnection, tx, [tempKeypair]);
console.log("close_permission tx:", sig);
```
**Use Cases:**
* Reclaim ephemeral rent before undelegating the data PDA
* Tear down access control once private execution is finished
[⬆️ Back to Top](#ephemeral-permission)
Reference implementations:
[`private-counter/anchor`](https://github.com/magicblock-labs/magicblock-engine-examples/tree/main/private-counter/anchor)
(Anchor)
and
[`private-counter/pinocchio`](https://github.com/magicblock-labs/magicblock-engine-examples/tree/main/private-counter/pinocchio).
***
## Best Practices
1. **Authority Management**: Always assign AUTHORITY\_FLAG to at least one trusted member
2. **Least Privilege**: Grant only necessary flags to each member
3. **Real-time Updates**: Permissions can be updated in real-time on Private Ephemeral Rollup without undelegating, allowing dynamic access control adjustments
4. **Cleanup**: Undelegate and close unused permission accounts to free SOL
***
## Security Considerations
* **Signer Validation**: Only members with AUTHORITY\_FLAG or program with permissioned account can authorize changes
* **Public Accounts**: Setting members to `None` makes the account publicly visible
* **Default Authority**: By default, the owner of the permissioned account is added as permission authority to members of permission account.
* **Empty Member List**: If members field is set to empty list, the permissioned account is fully restricted and private. Only the owner of permissioned account can modify the permission.
* **Access Auditing**: Use member flags to audit and control access
***
Fine-grained Access Control
Privacy Mechanisms and Concepts
Authorization Framework
Compliance Standards and Guidelines
# Local Development
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/how-to-guide/local-development
Run and test your Private Ephemeral Rollup programs locally, with the Query Filtering Service emulating the TEE privacy layer — no hardware attestation required.
***
### Quick Access
Explore private program and test scripts for Anchor, Native Rust, and Pinocchio:
Anchor implementation with permissions.
Pinocchio implementation with magic permission accounts.
***
## Why a Local PER Setup Is Different
In production a **Private Ephemeral Rollup** runs the Ephemeral Rollup inside a
**Trusted Execution Environment (TEE)** on Intel TDX. Clients don't reach the ER
directly — they connect through a **token-gated TEE endpoint** that enforces
privacy and compliance (IP geofencing, OFAC screening, and per-account
[permission filtering](/pages/private-ephemeral-rollups-pers/how-to-guide/access-control))
at ingress, before any transaction is accepted or executed.
You can't run real TDX hardware locally. Instead, the TEE ingress is emulated by
a dedicated process — the **Query Filtering Service (QFS)** — which replicates
the same token-auth and permission-filtering logic **without** hardware
attestation, and fronts an ordinary local ephemeral validator.
That is the only structural difference from a plain
[Ephemeral Rollup local setup](/pages/ephemeral-rollups-ers/how-to-guide/local-development):
**you run the QFS in front of the ER and point your client at the QFS instead of
the ER directly.**
## Topology
Four processes, each fronting the next:
```mermaid theme={null}
graph LR
User((Client))
QFS["Query Filtering Service
:6699 / :6700"]
ER["Ephemeral Rollup
:7799 / :7800"]
SN["Base Solana
:8899 / :8900"]
User <--> QFS
QFS <--> ER
ER <--> SN
```
| Process | RPC / WS | Role |
| ------------------------- | ----------- | --------------------------------------------------------------------------------------------------------- |
| `mb-test-validator` | 8899 / 8900 | Base layer. Wraps `solana-test-validator` and pre-clones the MagicBlock delegation + permission programs. |
| `ephemeral-validator` | 7799 / 7800 | The Ephemeral Rollup itself. |
| `query-filtering-service` | 6699 / 6700 | **PER-specific.** Emulates the TEE ingress — token auth + permission filtering. |
| `vrf-oracle` (optional) | — | Only needed if the program under test uses VRF. |
Point your client at the **QFS (6699)** to test privacy features. Point it at
the **ER (7799)** directly when you don't need the privacy layer.
## Quickstart with mb-stack
`mb-stack` is a single CLI that wraps `mb-test-validator`, `ephemeral-validator`,
and `query-filtering-service`, and handles the startup ordering between them for
you, in a single command. It ships in the same package as `ephemeral-validator`.
```bash theme={null}
npm install -g @magicblock-labs/ephemeral-validator@latest
```
```bash theme={null}
mb-stack --reset
```
This starts `mb-test-validator` (8899 / 8900), `ephemeral-validator`
(7799 / 7800), and `query-filtering-service` (6699 / 6700) together, on the
same default ports used throughout this guide.
Once the stack is up, deploy against the base layer as usual:
```bash theme={null}
anchor build && anchor deploy --provider.cluster localnet
```
```bash theme={null}
cargo build-sbf
solana config set --url localhost
solana program deploy YOUR_PROGRAM_PATH
```
## Per-Service Setup
`mb-stack`, `ephemeral-validator`, `query-filtering-service`, and `mb-test-validator` ship together:
```bash theme={null}
npm install -g @magicblock-labs/ephemeral-validator@latest
```
```bash theme={null}
mb-test-validator --reset
```
```bash theme={null}
anchor build && anchor deploy --provider.cluster localnet
```
```bash theme={null}
cargo build-sbf
solana config set --url localhost
solana program deploy YOUR_PROGRAM_PATH
```
Alternatively, inject the program when the base validator boots so it is
on-chain at its declared address from slot 0:
```bash theme={null}
mb-test-validator --reset \
--upgradeable-program
```
```bash theme={null}
ephemeral-validator \
--lifecycle ephemeral \
--remotes http://127.0.0.1:8899 \
--remotes ws://127.0.0.1:8900 \
--listen 127.0.0.1:7799 \
--reset
```
This is the process that makes it a **private** rollup. It points upstream at
the ER and exposes the client-facing endpoint:
```bash theme={null}
RUST_LOG=info query-filtering-service \
--listen-addr 127.0.0.1:6699 \
--listen-addr-ws 127.0.0.1:6700 \
--ephemeral-url http://127.0.0.1:7799 \
--ephemeral-url-ws ws://127.0.0.1:7800 \
--token-expiry-days 180 \
--add-cors-headers
```
`--add-cors-headers` is required if you test from a browser app. Wait for
port `6699` to accept a connection before firing any request.
If your program requests randomness, run one oracle against the base layer and
one against the ER:
```bash theme={null}
# Base-layer VRF requests
VRF_ORACLE_SKIP_PREFLIGHT=true RPC_URL=http://localhost:8899 \
WEBSOCKET_URL=ws://localhost:8900 RUST_LOG=info vrf-oracle &
# ER VRF requests
VRF_ORACLE_SKIP_PREFLIGHT=true RPC_URL=http://localhost:7799 \
WEBSOCKET_URL=ws://localhost:7800 RUST_LOG=info vrf-oracle &
```
## Connect Your Client Through the QFS
Your client treats the **QFS endpoint as the TEE endpoint**: point
`TEE_PROVIDER_ENDPOINT` at `http://localhost:6699` and reuse the same
[authorization flow](/pages/private-ephemeral-rollups-pers/how-to-guide/quickstart#4-authorize)
from the quickstart — fetch a token by signing a challenge, then open the
connection with `?token=...` attached. When a PDA is set to private via its
`EphemeralPermission`, the QFS blocks any wallet not in the member list — exactly
as the TEE would on devnet.
```ts theme={null}
import { getAuthToken } from "@magicblock-labs/ephemeral-rollups-sdk";
import nacl from "tweetnacl";
// Locally, TEE_PROVIDER_ENDPOINT = http://localhost:6699 (the QFS).
const teeUrl =
process.env.TEE_PROVIDER_ENDPOINT || "https://devnet-tee.magicblock.app";
const teeWsUrl =
process.env.TEE_WS_ENDPOINT || "wss://devnet-tee.magicblock.app";
const token = await getAuthToken(
teeUrl,
payer.publicKey,
(message: Uint8Array) =>
Promise.resolve(nacl.sign.detached(message, payer.secretKey)),
);
const erProvider = new anchor.AnchorProvider(
new anchor.web3.Connection(`${teeUrl}?token=${token.token}`, {
wsEndpoint: `${teeWsUrl}?token=${token.token}`,
commitment: "confirmed",
}),
anchor.Wallet.local(),
);
```
`verifyTeeRpcIntegrity` checks a real TDX attestation and is meant for the
devnet/mainnet TEE endpoints. The local QFS has no hardware attestation, so
skip that check when running fully local.
## Delegate to the Local Validator Identity
When delegating your PDA in a local test, delegate to the **localnet ER
identity** — not a devnet/mainnet one — so commits and undelegations settle
correctly:
```
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
```
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
## Endpoint Environment Variables
Centralize the endpoints so tests hit the local cluster instead of silently
falling back to devnet. The PER-relevant variables:
```bash theme={null}
export PROVIDER_ENDPOINT=http://localhost:8899 # base layer
export WS_ENDPOINT=ws://localhost:8900
export EPHEMERAL_PROVIDER_ENDPOINT=http://localhost:7799 # ER direct
export EPHEMERAL_WS_ENDPOINT=ws://localhost:7800
export QFS_ENDPOINT=http://localhost:6699 # QFS
export QFS_WS_ENDPOINT=ws://localhost:6700
export TEE_PROVIDER_ENDPOINT=$QFS_ENDPOINT # private tests read this
export TEE_WS_ENDPOINT=$QFS_WS_ENDPOINT
export VALIDATOR=mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
```
***
# Quickstart
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/how-to-guide/quickstart
Enable privacy in any Solana program state account through MagicBlock's ER and Trusted Execution Environment on Intel TDX.
***
**Building with an AI coding agent?** Install the MagicBlock Dev Skill to give your agent MagicBlock-specific patterns — delegation flows, Magic Actions, cranks, VRF, and more.
**Hit an error?** Ask your coding agent with the skill installed, not the docs assistant. The assistant only sees the docs, so it cannot debug your code.
Quick install for Claude Code:
```bash theme={null}
npx skills add https://github.com/magicblock-labs/magicblock-dev-skill
```
Using Cursor, Codex, Windsurf, Cline, or another agent? See the [AI Dev Skill](/pages/overview/additional-information/ai-dev-skill) page for all install targets.
### Quick Access
Check out example:
Private Counter Anchor Implementation
Try the Private Counter
MagicBlock's Private Ephemeral Rollup enforces compliance based on node-level
IP geofencing, OFAC-sanction list and restricted jurisdictions at ingress,
before any transaction is accepted or executed. [Find out
more](/pages/private-ephemeral-rollups-pers/introduction/compliance-framework)
***
## Step-By-Step Guide
Build your program, delegate state to the TEE validator, and create an `EphemeralPermission` account directly on the ER via MagicBlock's Permission Program `ACLseoPoyC3cBqoUtkbjZ4aDrkurZW86v19pXz2XQnp1` and Delegation Program `DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh`:
Write your program}>
Write your Solana program as you normally.
Delegate and create permission
}
>
`delegate` delegates the counter to the TEE validator on the base layer.
`init_permission` then runs on the ER — the delegated PDA signs as PDA and
pays its own ephemeral permission rent (pre-funded at `initialize` time).
`set_privacy` flips the public/private flag on demand. No base-layer
permission account to create, delegate, or commit-and-undelegate. [See
access control details](/pages/private-ephemeral-rollups-pers/how-to-guide/access-control).
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Deploy your program on Solana}>
Deploy your Solana program using Anchor CLI.
Implement authorization in your client}>
Sign user message to retrieve authorization token from TEE endpoint.
Execute transactions and test privacy}>
Request for authorization token and send confidential transactions.
***
## Private Counter Example
The following software packages may be required, other versions may also be compatible:
| Software | Version | Installation Guide |
| ---------- | ------- | --------------------------------------------------------------- |
| **Solana** | 3.1.9 | [Install Solana](https://docs.anza.xyz/cli/install) |
| **Rust** | 1.89.0 | [Install Rust](https://www.rust-lang.org/tools/install) |
| **Anchor** | 1.0.2 | [Install Anchor](https://www.anchor-lang.com/docs/installation) |
| **Node** | 24.10.0 | [Install Node](https://nodejs.org/en/download/current) |
The EphemeralPermission flow shown below requires `ephemeral-rollups-sdk`
v0.14+ (introduces `CreateEphemeralPermissionCpi` /
`UpdateEphemeralPermissionCpi` / `CloseEphemeralPermissionCpi`). For older
SDK and Anchor versions, see
[legacy examples](https://github.com/magicblock-labs/magicblock-engine-examples/tree/main/00-LEGACY_EXAMPLES).
### Code Snippets
A simple counter program with `initialize` and `increment` instructions, identical in shape to the public counter — privacy is added in the next steps:
```rust theme={null}
#[ephemeral]
#[program]
pub mod private_counter {
use super::*;
/// Initialize the counter.
pub fn initialize(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = 0;
Ok(())
}
/// Increment the counter.
pub fn increment(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
}
/// ... Other instructions for delegation, permission, and privacy
}
pub const COUNTER_SEED: &[u8] = b"counter";
/// Context for initializing counter
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init_if_needed, payer = user, space = 8 + 8, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
/// Context for incrementing counter
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
}
/// Counter struct
#[account]
pub struct Counter {
pub count: u64,
}
/// Other context and accounts for delegation and privacy ...
```
[⬆️ Back to Top](#code-snippets)
The full privacy lifecycle is split across two layers: the **base layer** delegates the counter to a TEE validator; the **ER** then creates / updates / closes its own `EphemeralPermission` account, signed by the delegated PDA itself.
* `initialize` pre-funds the counter PDA with rent for the ephemeral permission, so step 3+ never need a separate lamports-top-up.
* `delegate` delegates only the counter to the TEE validator.
* `init_permission` runs on the ER — the delegated PDA signs a [`CreateEphemeralPermissionCpi`](https://github.com/magicblock-labs/ephemeral-rollups-sdk) using its program seeds and pays the rent. Idempotent.
* `set_privacy(is_private)` toggles privacy on the ER via [`UpdateEphemeralPermissionCpi`](https://github.com/magicblock-labs/ephemeral-rollups-sdk). When private, only the counter's `authority` is in the member list with `TX_LOGS_FLAG | TX_MESSAGE_FLAG | TX_BALANCES_FLAG` — every other wallet is blocked at the TEE ingress.
* `close_permission` refunds the rent back to the PDA when the permission is no longer needed (optional).
* `undelegate` commits and undelegates the counter via `MagicIntentBundleBuilder`.
See [access control](/pages/private-ephemeral-rollups-pers/how-to-guide/access-control) for the full lifecycle and the per-language snippet variants.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
```rust theme={null}
use anchor_lang::system_program::{transfer, Transfer};
use ephemeral_rollups_sdk::{
access_control::{
instructions::{
CloseEphemeralPermissionCpi, CreateEphemeralPermissionCpi,
UpdateEphemeralPermissionCpi,
},
structs::{
EphemeralMembersArgs, EphemeralPermission, Member,
TX_BALANCES_FLAG, TX_LOGS_FLAG, TX_MESSAGE_FLAG,
},
},
anchor::{commit, delegate, ephemeral},
cpi::DelegateConfig,
ephem::MagicIntentBundleBuilder,
};
#[ephemeral] // Adds undelegation instruction for the ER validator
#[program]
pub mod private_counter {
use super::*;
/// Initialize on the base layer. Pre-funds the counter PDA with enough
/// lamports to cover the ephemeral permission rent that will be paid on
/// the ER (rent = ~32 lamports/byte × (size + 60); use
/// `EphemeralPermission::size_of(N)` for the exact byte count).
pub fn initialize(ctx: Context) -> Result<()> {
transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
Transfer {
from: ctx.accounts.authority.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
},
),
ephemeral_rollups_sdk::ephemeral_accounts::rent(
EphemeralPermission::size_of(1) as u32,
),
)?;
let counter = &mut ctx.accounts.counter;
counter.count = 0;
counter.authority = ctx.accounts.authority.key();
Ok(())
}
/// Delegate the counter to the (TEE) ER. No permission CPI here —
/// the EphemeralPermission is created directly on the ER via
/// `init_permission` (next instruction).
pub fn delegate(ctx: Context) -> Result<()> {
if ctx.accounts.counter.owner != &ephemeral_rollups_sdk::id() {
let validator = ctx.accounts.validator.as_ref();
ctx.accounts.delegate_counter(
&ctx.accounts.authority,
&[COUNTER_SEED, ctx.accounts.authority.key().as_ref()],
DelegateConfig {
validator: validator.map(|v| v.key()),
..Default::default()
},
)?;
}
Ok(())
}
/// Create the ephemeral permission directly on the ER. Payer = the
/// counter PDA (delegated), which carries its base-layer lamports onto
/// the ER and signs via its program seeds. Idempotent: skip if the
/// permission account already exists. Starts public; flip with
/// `set_privacy`.
pub fn init_permission(ctx: Context) -> Result<()> {
if ctx.accounts.permission.lamports() > 0 {
return Ok(());
}
let signers = [
COUNTER_SEED,
ctx.accounts.counter.authority.as_ref(),
&[ctx.bumps.counter],
];
CreateEphemeralPermissionCpi {
payer: ctx.accounts.counter.to_account_info(),
permissioned_account: ctx.accounts.counter.to_account_info(),
permission: ctx.accounts.permission.to_account_info(),
vault: ctx.accounts.ephemeral_vault.to_account_info(),
magic_program: ctx.accounts.magic_program.to_account_info(),
permission_program: ctx.accounts.permission_program.to_account_info(),
args: EphemeralMembersArgs {
is_private: false,
members: vec![],
},
}
.invoke_signed(&[&signers])?;
Ok(())
}
/// Toggle the privacy flag on the ER. When private, only the counter's
/// `authority` is allowed to read state via the TEE (logs, messages,
/// balances). The authority is the only member; the member list is
/// rebuilt every call so the authority can never lock itself out.
pub fn set_privacy(ctx: Context, is_private: bool) -> Result<()> {
let signers = [
COUNTER_SEED,
ctx.accounts.counter.authority.as_ref(),
&[ctx.bumps.counter],
];
let members = if is_private {
vec![Member {
flags: TX_LOGS_FLAG | TX_MESSAGE_FLAG | TX_BALANCES_FLAG,
pubkey: ctx.accounts.counter.authority,
}]
} else {
vec![]
};
UpdateEphemeralPermissionCpi {
payer: ctx.accounts.counter.to_account_info(),
permissioned_account: ctx.accounts.counter.to_account_info(),
permission: ctx.accounts.permission.to_account_info(),
vault: ctx.accounts.ephemeral_vault.to_account_info(),
magic_program: ctx.accounts.magic_program.to_account_info(),
permission_program: ctx.accounts.permission_program.to_account_info(),
authority: ctx.accounts.counter.to_account_info(),
authority_is_signer: false, // PDA signs via the seeds above
args: EphemeralMembersArgs { is_private, members },
}
.invoke_signed(&[&signers])?;
Ok(())
}
/// Close the ephemeral permission account on the ER, refunding rent to
/// the counter PDA (the payer that originally deposited it).
pub fn close_permission(ctx: Context) -> Result<()> {
let signers = [
COUNTER_SEED,
ctx.accounts.counter.authority.as_ref(),
&[ctx.bumps.counter],
];
CloseEphemeralPermissionCpi {
payer: ctx.accounts.counter.to_account_info(),
permissioned_account: ctx.accounts.counter.to_account_info(),
permission: ctx.accounts.permission.to_account_info(),
vault: ctx.accounts.ephemeral_vault.to_account_info(),
magic_program: ctx.accounts.magic_program.to_account_info(),
permission_program: ctx.accounts.permission_program.to_account_info(),
authority: ctx.accounts.counter.to_account_info(),
authority_is_signer: false,
}
.invoke_signed(&[&signers])?;
Ok(())
}
/// Commit + undelegate the counter when private execution is done. No
/// separate permission undelegation step — ephemeral permissions are
/// confined to the ER and were already cleaned up via `close_permission`
/// (if called).
pub fn undelegate(ctx: Context) -> 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(())
}
}
```
[⬆️ Back to Top](#code-snippets)
Now you’re program is upgraded and ready! Build and deploy to the desired cluster:
```bash theme={null}
anchor build && anchor deploy
```
[⬆️ Back to Top](#code-snippets)
Set up interaction with ER RPC in TEE:
1. Verify integrity of TEE RPC via `https://pccs.phala.network/tdx/certification/v4`
2. Request an authorization token for user to interact with TEE endpoint
```typescript Web3.js theme={null}
import {
verifyTeeRpcIntegrity,
getAuthToken,
} from "@magicblock-labs/ephemeral-rollups-sdk";
// Verify the integrity of the TEE RPC
const isVerified = await verifyTeeRpcIntegrity(EPHEMERAL_RPC_URL);
// Get an auth token before making requests to the TEE
const token = await getAuthToken(
EPHEMERAL_RPC_URL,
wallet.publicKey,
(message: Uint8Array) =>
Promise.resolve(nacl.sign.detached(message, wallet.secretKey)),
);
```
[⬆️ Back to Top](#code-snippets)
Test your program with the Private Ephemeral Rollup connection:
`https://devnet-tee.magicblock.app?token=${token}`
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
### Quick Access
Check out example:
Private Counter Anchor Implementation
Try the Private Counter
[⬆️ Back to Top](#code-snippets)
***
### Advanced Code Snippets
These ER building blocks work the same way inside a Private Ephemeral Rollup.
When resizing a delegated PDA:
* PDA must have enough lamports to remain rent-exempt for the new account size.
* If additional lamports are needed, the **payer account must be delegated** to provide the difference.
* PDA must be owned by the program, and the transaction must include any signer(s) required for transferring lamports.
* Use `system_instruction::allocate`
```rust theme={null}
#[account]
pub struct Counter {
pub count: u64,
pub extra_data: Vec,
}
#[derive(Accounts)]
pub struct ResizeCounter<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
// Resize the counter (e.g., to store more extra_data)
pub fn resize_counter(ctx: Context, new_size: usize) -> Result<()> {
let account_to_resize = &mut ctx.accounts.counter.to_account_info();
let payer = &mut ctx.accounts.payer.to_account_info();
// Calculate rent-exemption for the new size
let rent = Rent::get()?;
let min_balance = rent.minimum_balance(new_size);
// Top up lamports if needed
let current_lamports = **account_to_resize.lamports.borrow();
if current_lamports < min_balance {
let to_transfer = min_balance - current_lamports;
**payer.try_borrow_mut_lamports()? -= to_transfer;
**account_to_resize.try_borrow_mut_lamports()? += to_transfer;
}
// Resize account
account_to_resize.resize(new_size)?;
Ok(())
}
```
[⬆️ Back to Top](#advanced-code-snippets)
Initialize connection with Magic Router before you send transactions dynamically.
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Choose your preferred SDK to initialize, send and confirm transactions:
* `ephemeral-rollups-kit` for `@solana/kit`
* `ephemeral-rollups-sdk` for `@solana/web.js`
```typescript Kit theme={null}
import { Connection } from "@magicblock-labs/ephemeral-rollups-kit";
// Initialize connection
const connection = await Connection.create(
"https://devnet-router.magicblock.app",
"wss://devnet-router.magicblock.app"
);
// ... create transaction
// Send and confirm transaction
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
import { sendAndConfirmTransaction } from "@solana/web3.js";
import { ConnectionMagicRouter } from "@magicblock-labs/ephemeral-rollups-sdk";
// Initialize connection
const connection = new ConnectionMagicRouter(
"https://devnet-router.magicblock.app/",
{ wsEndpoint: "wss://devnet-router.magicblock.app/" }
);
// ... create transaction
// Send and confirm transaction
const txHash = await sendAndConfirmTransaction(connection, tx, [payer], {
skipPreflight: true,
commitment: "confirmed",
});
```
[Learn more about Magic Router](/pages/ephemeral-rollups-ers/introduction/magic-router)
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
Explore reference implementation on GitHub
Attach one or more instructions that run automatically on the Solana base layer immediately after an Ephemeral Rollup
(ER) commit.
[Learn more about Magic Action](/pages/ephemeral-rollups-ers/magic-actions/overview)
### 1) Create action instruction
The instruction `update_leaderboard` runs on the base layer immediately after the commit lands. The `#[action]` attribute on its accounts context marks it as callable from a post-commit action.
`#[action]` makes the instruction **callable from** a post-commit action — it
does not make it callable **only** that way. The handler is an ordinary
base-layer instruction, so anyone can invoke it directly with a wallet.
Address, `seeds`, and `owner` constraints only pin *which* accounts are passed;
they do not authenticate *who* called it. Any handler that moves value or
changes authoritative state must verify the injected `escrow` signer — see
[Authenticate the caller](/pages/ephemeral-rollups-ers/magic-actions/troubleshooting#security-authenticate-the-caller).
```rust theme={null}
// program instruction
pub fn update_leaderboard(ctx: Context) -> Result<()> {
let leaderboard = &mut ctx.accounts.leaderboard;
let counter_info = &mut ctx.accounts.counter.to_account_info();
let mut data: &[u8] = &counter_info.try_borrow_data()?;
let counter = Counter::try_deserialize(&mut data)?;
if counter.count > leaderboard.high_score {
leaderboard.high_score = counter.count;
}
msg!(
"Leaderboard updated! High score: {}",
leaderboard.high_score
);
Ok(())
}
// instruction context
#[action]
#[derive(Accounts)]
pub struct UpdateLeaderboard<'info> {
#[account(mut, seeds = [LEADERBOARD_SEED], bump)]
pub leaderboard: Account<'info, Leaderboard>,
/// CHECK: PDA owner depends on: 1) Delegated: Delegation Program; 2) Undelegated: Your program ID
pub counter: UncheckedAccount<'info>,
}
```
### 2) Build the commit instruction with the action
The commit instruction `commit_and_update_leaderboard` runs on the ER. It uses `MagicIntentBundleBuilder` to schedule both the commit and the post-commit action onto `magic_context` — both are applied together when the ER transaction is sealed back to the base layer.
```rust theme={null}
// commit action instruction on ER
pub fn commit_and_update_leaderboard(ctx: Context) -> Result<()> {
// Build the post-commit action that updates the leaderboard on base layer
let instruction_data =
anchor_lang::InstructionData::data(&crate::instruction::UpdateLeaderboard {});
let action_args = ActionArgs::new(instruction_data);
let action_accounts = vec![
ShortAccountMeta {
pubkey: ctx.accounts.leaderboard.key(),
is_writable: true,
},
ShortAccountMeta {
pubkey: ctx.accounts.counter.key(),
is_writable: false,
},
];
let action = CallHandler {
destination_program: crate::ID,
accounts: action_accounts,
args: action_args,
// Signer that pays transaction fees for the action from its escrow PDA
escrow_authority: ctx.accounts.payer.to_account_info(),
compute_units: 200_000,
};
// Schedule commit + post-commit action on magic_context
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()])
.add_post_commit_actions([action])
.build_and_invoke()?;
Ok(())
}
// commit action context on ER
#[commit]
#[derive(Accounts)]
pub struct CommitAndUpdateLeaderboard<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
/// CHECK: Leaderboard PDA - not mut here, writable set in handler
#[account(seeds = [LEADERBOARD_SEED], bump)]
pub leaderboard: UncheckedAccount<'info>,
/// CHECK: Your program ID
pub program_id: AccountInfo<'info>,
}
```
### Execute multiple actions
You can commit multiple accounts and chain several actions in one call. Actions execute sequentially in the order they're passed to `add_post_commit_actions`.
```rust theme={null}
// Chain several actions — they execute sequentially on base layer after the commit lands.
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(),
// ... additional committed accounts
])
.add_post_commit_actions([action_1, action_2, action_3])
.build_and_invoke()?;
```
### Undelegate with actions
Actions can also be chained onto an undelegation — the counter commits, undelegates, and the actions run, all atomically in one ER transaction.
```rust theme={null}
// Commit, undelegate, AND execute actions — all atomically on base layer after the ER transaction seals.
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()])
.add_post_commit_actions([action])
.build_and_invoke()?;
```
[⬆️ Back to Top](#advanced-code-snippets)
Top up a delegated account's lamports on the ER side. The transaction is submitted on the **base layer** and uses the Ephemeral SPL Token program to shuttle lamports to the destination's delegated balance via a single-use lamports PDA.
Common use case: keeping a delegated fee payer funded for a long session. Without a fee payer, an account stops after 10 commits. With a fee payer and `magic_fee_vault`, it can keep committing, and the payer starts paying live commit fees on commit 26.
Notes:
* Generate a fresh 32-byte salt per top-up via `crypto.getRandomValues` — re-using a salt collides with an existing PDA.
* Submit to the base-layer RPC, not the ER.
* The destination must already be delegated.
```typescript theme={null}
import {
Connection,
Keypair,
PublicKey,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
lamportsDelegatedTransferIx,
deriveLamportsPda,
} from "@magicblock-labs/ephemeral-rollups-sdk";
/**
* Top up a delegated account with lamports.
*
* The transaction is submitted on the BASE LAYER. The Ephemeral SPL Token
* program creates a single-use lamports PDA, funds it from the payer, and
* delegates it so the ER credits the destination's delegated balance.
*/
async function topUpDelegatedAccount(
connection: Connection, // base-layer connection
payer: Keypair,
destination: PublicKey, // delegated account to top up
amountLamports: bigint,
) {
// Generate a fresh 32-byte salt per top-up.
// Re-using a salt collides with an existing lamports PDA and the call fails.
const salt = crypto.getRandomValues(new Uint8Array(32));
const [lamportsPda] = deriveLamportsPda(payer.publicKey, destination, salt);
const ix = await lamportsDelegatedTransferIx(
payer.publicKey,
destination,
amountLamports,
salt,
);
const tx = new Transaction().add(ix);
tx.feePayer = payer.publicKey;
// CRITICAL: send to the base-layer RPC, not the ER.
const sig = await sendAndConfirmTransaction(connection, tx, [payer], {
commitment: "confirmed",
skipPreflight: true,
});
return { sig, lamportsPda };
}
```
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
On-Curve Delegation
Required signers for delegating an on-curve account:
1. On-curve account to be delegated
2. Fee payer
Required instructions for delegating on-curve accounts:
1. Assign System Account to Delegation Program
2. Delegate to Delegation Program
```typescript Kit theme={null}
// Create assign instruction
// The on-curve account must sign this instruction to change its owner
const accountSigner = await cryptoKeyPairToTransactionSigner(userKeypair);
const delegationProgramAddress = address(DELEGATION_PROGRAM_ID.toString());
const assignInstruction = getAssignInstruction({
account: accountSigner,
programAddress: delegationProgramAddress,
});
// Create delegate instruction
const delegateInstruction = await createDelegateInstruction({
payer: feePayerAddress,
delegatedAccount: userAddress,
ownerProgram: ownerProgramAddress,
validator: validatorAddress,
});
// Prepare transaction
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(feePayerAddress, tx),
(tx) =>
appendTransactionMessageInstructions(
[assignInstruction, delegateInstruction],
tx
)
);
// Send and confirm transaction (fee payer need to sign, on-curve account cannot be signer since delegated)
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair, feePayerKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// Create assign instruction
const assignInstruction = SystemProgram.assign({
accountPubkey: userPubkey,
programId: DELEGATION_PROGRAM_ID,
});
// Create delegate instruction
const delegateInstruction = createDelegateInstruction({
payer: feePayerKeypair.publicKey,
delegatedAccount: userPubkey,
ownerProgram: ownerProgram,
validator: validator,
});
// Create and send transaction (fee payer need to sign, on-curve account cannot be signer since delegated)
const tx = new Transaction().add(assignInstruction, delegateInstruction);
tx.feePayer = feePayerKeypair.publicKey;
const txSignature = await sendAndConfirmTransaction(
connectionBaseLayer,
tx,
[userKeypair, feePayerKeypair],
{
skipPreflight: true,
}
);
```
Direct commit and undelegate through Magic Program only.
```typescript Kit theme={null}
// Create commit and undelegate instruction
const commitAndUndelegateInstruction = createCommitAndUndelegateInstruction(
userAddress,
[userAddress]
);
// Prepare transaction
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(feePayerAddress, tx),
(tx) =>
appendTransactionMessageInstructions([commitAndUndelegateInstruction], tx)
);
// Send and confirm transaction on ephemeral connection
const txHash = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair, feePayerKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// Create commit and undelegate instruction
const commitAndUndelegateInstruction = createCommitAndUndelegateInstruction(
userPubkey,
[userPubkey]
);
// Send and confirm transaction on ephemeral connection
const tx = new Transaction().add(commitAndUndelegateInstruction);
tx.feePayer = feePayerKeypair.publicKey;
const txSignature = await sendAndConfirmTransaction(
ephemeralConnection,
tx,
[userKeypair, feePayerKeypair],
{
skipPreflight: true,
}
);
```
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
Explore reference implementation on GitHub
Attach instructions to a delegation that the ER validator runs automatically
inside the rollup, right after the account is delegated — no extra transaction:
* Build the action(s) as standard `Instruction`s and convert them to the
compact payload with `.cleartext()` (public) — encrypted actions are built
off-chain by a client holding the validator key.
* The base-layer delegation program stores the payload in the delegation
record; the ER validator executes it once the account lands in the rollup.
* CPI with `delegate_account_with_actions` instead of the plain `delegate_pda`
helper; the `#[delegate]` macro still provides the buffer/record/metadata
accounts.
```rust theme={null}
/// Reuse the same accounts context as a normal delegation
#[delegate]
#[derive(Accounts)]
pub struct DelegateInput<'info> {
pub payer: Signer<'info>,
/// CHECK: The pda to delegate
#[account(mut, del)]
pub pda: AccountInfo<'info>,
}
```
```rust theme={null}
use anchor_lang::solana_program::instruction::{AccountMeta, Instruction};
use anchor_lang::InstructionData;
use ephemeral_rollups_sdk::cpi::{
delegate_account_with_actions, DelegateAccounts, DelegateConfig,
};
use ephemeral_rollups_sdk::dlp_api::compact::ClearText;
/// Delegate the account AND attach a post-delegation action. The action is stored
/// in the delegation record on the base layer and executed automatically by the ER
/// validator inside the rollup, right after the account is delegated — no extra
/// transaction. Here the action is a self-CPI back into `increment`.
pub fn delegate_with_actions(ctx: Context) -> Result<()> {
let counter_key = ctx.accounts.pda.key();
// The instruction the ER validator runs post-delegation, inside the rollup.
let increment_action = Instruction {
program_id: crate::ID,
accounts: vec![AccountMeta::new(counter_key, false)],
data: crate::instruction::Increment {}.data(),
};
// Convert to the compact, cleartext post-delegation actions payload.
// (Use `cleartext` for public actions; encrypted actions are built off-chain
// by a client that holds the validator key.)
let actions = vec![increment_action].cleartext();
let payer = ctx.accounts.payer.to_account_info();
let pda = ctx.accounts.pda.to_account_info();
let delegate_accounts = DelegateAccounts {
payer: &payer,
pda: &pda,
owner_program: &ctx.accounts.owner_program,
buffer: &ctx.accounts.buffer_pda,
delegation_record: &ctx.accounts.delegation_record_pda,
delegation_metadata: &ctx.accounts.delegation_metadata_pda,
delegation_program: &ctx.accounts.delegation_program,
system_program: &ctx.accounts.system_program,
};
delegate_account_with_actions(
delegate_accounts,
&[COUNTER_SEED],
DelegateConfig {
// Optionally set a specific validator from the first remaining account
validator: ctx.remaining_accounts.first().map(|acc| acc.key()),
..Default::default()
},
actions,
// No extra signers are required by the increment action.
&[],
)?;
Ok(())
}
```
[⬆️ Back to Top](#advanced-code-snippets)
***
Fine-grained Access Control
Privacy Mechanisms and Concepts
Authorization Framework
Compliance Standards and Guidelines
***
## Solana Explorer
Get insights about your transactions and accounts on Solana:
Official Solana Explorer
Explore Solana Blockchain
## Solana RPC Providers
Send transactions and requests through existing RPC providers:
Free Public Nodes
Free Shared Nodes
Dedicated High-Performance Nodes
## Solana Validator Dashboard
Find real-time updates on Solana's validator infrastructure:
Get Validator Insights
Discover Validator Metrics
## Server Status
Subscribe to Solana's and MagicBlock's server status:
Subscribe to Solana Server Updates
Subscribe to MagicBlock Server Status
***
## MagicBlock Products
Execute real-time, zero-fee transactions securely on Solana.
Protect sensitive data with compliance — built on top of Ephemeral Rollups.
Move SPL tokens at rollup speed — public or private transfers, swaps, and private payments for trading and DeFi apps.
Combine real-time execution, session keys, token custody, price feeds, automation, and settlement.
Add provably fair onchain randomness to games, raffles, and real-time apps.
Access low-latency onchain price feeds for trading and DeFi.
***
# Authorization
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/introduction/authorization
Customize authorized access through onchain restrictions on account level for user groups.
***
### Authorization Model
Private Ephemeral Rollups use a Permission Program to manage fine-grained privacy controls for accounts and account groups. This runs on Solana L1 and can be updated on the fly.
* **Permission Groups**: Define groups with arbitrary membership and IDs via CPI. A group aggregates users and the accounts governed by its permissions.
* **Permissions**: Add permissions to groups. Today a permission implies read access for the delegated account; read/write splits may be added in the future.
* **Access**: Client access to permissioned ER state requires authenticating ownership of a specified public key. Successful authentication yields a token used to query the ER.
Private Ephemeral Rollup (devnet) endpoint:
`https://devnet-tee.magicblock.app?token= {authToken}`. Replace `{authToken}` with your authorization token obtained
from the TEE RPC to send requests.
This abstraction into groups lets you modify the permissions for many users/accounts atomically in a single transaction.
***
Fine-grained Access Control
Privacy Mechanisms and Concepts
Authorization Framework
Compliance Standards and Guidelines
# Compliance Framework
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/introduction/compliance-framework
MagicBlock enables confidential execution without compromising performance, compliance, or control.
### Overview
MagicBlock Private Ephemeral Rollups (PERs) enable confidential execution while enforcing account-level state access and regulatory-compliant controls. We believe in a form of privacy that is respectful of users' rights and lawful. Private ERs are not open anonymity rails, but rather private environments with enforced boundaries that can be customized for specific use cases.
### Built for Institutional Requirements: Performance, Compliance, and Control
Businesses and institutions do not choose between performance, compliance, and control; they require all three. Systems that sacrifice performance for compliance are unusable at scale, while systems that optimize solely for speed or privacy without enforceable controls are incompatible with institutional mandates or the day-to-day operations of lawful businesses.
MagicBlock's Trusted Execution Environment (TEE) architecture is explicitly designed to satisfy these requirements simultaneously.
* **Performance**: Private ERs deliver low-latency, high-throughput execution suitable for real-time applications, market-sensitive workflows, and on-chain systems that cannot tolerate delayed or probabilistic settlement.
* **Compliance**: Jurisdictional enforcement, real-time AML and sanctions screening, and clear, upfront licensing ensure private execution operates within clearly defined legal and regulatory boundaries.
* **Control**: Access to private execution is conditional, configurable, and enforceable at the on-chain program level. Institutions retain control over who can connect and under what constraints assets may enter or exit.
This approach allows institutions to unlock the benefits of confidential execution while preserving the guarantees they require to operate responsibly at scale.
### Compliance Safeguards
* **Jurisdiction & Network Access Controls**\
Private ER access is enforced at the infrastructure layer through node-level IP geofencing. Connections originating from OFAC-sanctioned or otherwise restricted jurisdictions are blocked at ingress, before any transaction is accepted or executed. These controls ensure jurisdictional restrictions are enforced deterministically and upstream of execution.
* **Real-Time AML & Sanctions Screening**\
All relevant interaction points with Private ERs are subject to continuous, real-time AML and sanctions screening via Range. This includes sanctions list verification, exposure and counterparty risk assessment, and behavioral risk signals. Transactions that fail screening are rejected or halted prior to execution or settlement, preventing tainted flows from entering or exiting the private environment.
* **EULA & Licensed Deployments**\
Private ER instances are operated under explicit licensing and policy constraints defined by MagicBlock Labs and, where applicable, its partners. Different licenses can be applied to different instances to ensure the open-source software we provide explicitly forbids illicit use cases or misuse of the technology for unwarranted transactions.
MagicBlock Private ERs deliver confidential execution within clearly enforced legal and regulatory boundaries.
***
Fine-grained Access Control
Privacy Mechanisms and Concepts
Authorization Framework
Compliance Standards and Guidelines
# Onchain Privacy
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/introduction/onchain-privacy
The high-performance engine for real-time applications that require privacy and compliance on Solana
***
### MagicBlock Private Ephemeral Rollup (PER)
MagicBlock is bringing high-performance, general-purpose Trusted Execution Environments (TEEs) to Solana. We call this the Private Ephemeral Rollup (PER).
By using the security guarantees of Intel Trust Domain Extension (TDX) architecture combined with MagicBlock’s Ephemeral Rollup (ER) technology, we’re making it possible to run sensitive logic inside of a hardware-secured environment, while inheriting the composability and speed of Solana.
For the first time, builders can design applications that are simultaneously:
* **Confidential**: state is protected from all unauthorized parties
* **Scalable**: running inside an ER that can execute blocks at high throughput and low latency
* **Composable**: still able to interoperate with other Solana programs
* **Compliant**: easy-to-enforce compliance thanks to a fine-grained access control layer
This enables use cases like confidential transfers, sealed-bid auctions, and secure identity flows on Solana.
### Trusted Execution Environments on Intel TDX
A trusted execution environment acts as a vault inside a CPU. Generally, when you run a program (e.g., a validator), the operating system can see and influence everything: the code, the state, and the memory. A TEE creates a hardware-secured space that prevents interference, even by the machine it’s running on.
We use a TEE to protect the state of an Ephemeral Rollup. Normally, when you execute a smart contract onchain, every step of the process is visible: the program being called, the accounts, and the resulting state changes. Using the TEE, programs can selectively shield ER state.
* Every account is public by default, like in Solana
* Programs can explicitly define access rules for their accounts
This means transfers, program calls, and interactions can avoid being broadcast publicly when desired.
### Why TEE
There are multiple privacy-preserving approaches used in blockchains. Each has different tradeoffs.
| Solution | What is it? | Pros | Cons |
| ------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
| Trusted Execution Environments | Hardware-secured execution within a CPU | Near-native performance, run normal code | Trust assumption in vendor hardware |
| Fully Homomorphic Encryption | Compute directly on encrypted data | Data never decrypted | Extremely slow, specialized tooling, difficult key management, bugs and vulnerabilities |
| Zero Knowledge Proofs | Prove something without revealing inputs | Efficient verification; strong for identity/compliance/correctness | Proving is heavy, bugs and vulnerabilities |
| Multi-party Computation | Split a secret across multiple parties to compute jointly | Strong cryptographic guarantees | Coordination overhead, high latency, specialized tooling, bugs and vulnerabilities |
* **ZK**: efficient verification, not optimized for general low-latency computation
* **MPC**: shared trust, higher latency and coordination
* **FHE**: powerful in theory, not ready for general usage today
* **TEE**: practical confidentiality with real-time performance and familiar developer UX on Solana
### Application Unlocks
* **Confidential Transfers**: move assets privately without exposing balances or counterparties
* **Sealed-Bid Auctions**: keep bids hidden until settlement for fair price discovery
* **Private Games**: support games where revealing state undermines gameplay
* **Enterprise and Compliance Flows**: run sensitive operations with blockchain guarantees while keeping data private
* **Identity-Based Access**: verify group membership without revealing unnecessary account history
Private Ephemeral Rollup (devnet) endpoint:
`https://devnet-tee.magicblock.app?token= {authToken}`. Replace `{authToken}` with your authorization token obtained
from the TEE RPC to send requests.
***
Fine-grained Access Control
Privacy Mechanisms and Concepts
Authorization Framework
Compliance Standards and Guidelines
# Prediction Markets and Trading
Source: https://docs.magicblock.gg/pages/solutions/prediction-markets
Architecture guide for prediction markets, leveraged trading, and settlement using Ephemeral Rollups, session keys, SPL token custody, pricing oracles, cranks, and Magic Actions.
Build the latency-sensitive trading loop on an Ephemeral Rollup (ER), then commit the state or settle
funds on Solana when the result must become durable. This guide maps each protocol requirement to the
MagicBlock feature that solves it.
The linked examples demonstrate integration patterns, not a production risk engine. A leveraged
protocol still needs audited pricing, liquidity, solvency, liquidation, pause, and recovery rules.
## Choose the components
| Protocol requirement | MagicBlock feature | Start here |
| ------------------------------------------------ | ------------------------- | ------------------------------------------------------------------------------- |
| Frequent orders, bets, or position updates | Ephemeral Rollups | [ER quickstart](/pages/ephemeral-rollups-ers/how-to-guide/quickstart) |
| Restricted position or strategy visibility | Private Ephemeral Rollups | [PER quickstart](/pages/private-ephemeral-rollups-pers/how-to-guide/quickstart) |
| Repeated actions without wallet prompts | Session Keys | [Session Keys overview](/pages/tools/session-keys/introduction) |
| Collateral, custody, pool liquidity, and payouts | Ephemeral SPL Token | [Token model](/pages/ephemeral-spl-token/overview) |
| Prices and liquidation inputs | Pricing Oracle | [Oracle overview](/pages/tools/oracle/introduction) |
| Expiry, liquidation, and settlement checks | Cranks | [Cranks overview](/pages/tools/crank/introduction) |
| Base-layer payout after an ER commit | Magic Actions | [Magic Actions overview](/pages/ephemeral-rollups-ers/magic-actions/overview) |
## Reference architecture
Create market configuration, authorities, collateral mint, and base-layer custody accounts. Keep
administrative controls and durable settlement records on Solana.
Delegate position, order, or bet accounts to the ER. Use a PER when the contents or allowed
participants must be restricted inside a TEE-backed validator.
Scope a session key to the protocol instructions, duration, and limits the user needs. Do not
grant the session unrestricted control of the user's assets. SPL Token does not understand
session tokens, so token movement may also require a one-time approval to a constrained program
authority, as shown in the binary prediction example.
Deposit collateral into the Global Vault and use program-controlled eATAs when funds must move at
ER speed. Read the configured oracle account inside each price-sensitive instruction.
Schedule cranks for expiry, liquidation, or periodic settlement logic. Make every scheduled
instruction safe to retry and validate the current market state before changing it.
Commit final state while remaining delegated for continued trading, or commit and undelegate when
the market closes. Use a Magic Action when the commit must trigger a base-layer payout or another
Solana instruction.
## Choose a custody model
| Model | Best for | Trade-off |
| ------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------- |
| **Program-controlled eATA custody** | Stakes, collateral, and payouts that move repeatedly on the ER | Requires the Ephemeral SPL Token lifecycle and explicit withdrawal handling |
| **Base-layer custody + post-commit payout** | Funds that should remain in normal ATAs until settlement | Token movement waits for commit, but base-layer custody stays authoritative |
The [Smart Contract Integration guide](/pages/ephemeral-spl-token/smart-contract-integration)
implements both patterns and links the binary-prediction reference.
## Prepare for production
Before moving from a reference example to a production protocol, define:
* oracle freshness, confidence, and failure behavior;
* collateral, exposure, and liquidation rules when the product requires them;
* maximum position, market exposure, and utilization limits;
* liquidity and payout reserves under worst-case moves;
* fees, rounding, and integer overflow boundaries;
* crank failure, retry, and manual recovery paths;
* commit frequency and the authoritative state during settlement;
* emergency pause, close-only, and controlled unwind behavior.
Use PER only when restricted visibility is a protocol requirement. Privacy does not replace solvency,
oracle validation, or authorization checks.
## Start with working examples
Session-authorized bets, real-time prices, eATA pool custody, and expiry-based settlement.
A smaller example focused on freshness checks, feed validation, and slippage protection.
Hidden bids on a Private Ephemeral Rollup with two-layer SPL escrow and on-chain winner selection.
Build the binary prediction example first. Add crank-driven settlement next, then choose ER custody or
a post-commit base-layer payout based on where your protocol needs funds to remain authoritative.
# Implementation
Source: https://docs.magicblock.gg/pages/tools/crank/implementation
Learn how to implement cranks in your Solana program using MagicBlock Ephemeral Rollups
## Overview
This guide demonstrates how to implement cranks using **MagicBlock's Ephemeral Rollups (ER)** with the Anchor framework. The implementation follows this flow:
1. Initialize counter on Solana base layer
2. Delegate counter account to Ephemeral Rollup for faster execution
3. Schedule a crank task that automatically increments the counter
4. Execute the crank automatically at specified intervals
5. Undelegate the account back to Solana base layer when done
## Execution Flow
```
1. User calls initialize() on Solana base layer
└─> Creates Counter PDA with count = 0
2. User calls delegate() on Solana base layer
└─> Moves Counter account to Ephemeral Rollup
3. User calls schedule_increment() on Ephemeral Rollup
└─> CPI to MagicBlock program
└─> Schedules task with:
- task_id: 1
- interval: 100ms
- iterations: 3
- instruction: increment()
4. MagicBlock automatically executes increment() 3 times:
└─> Execution 1: count = 1 (at T+0ms)
└─> Execution 2: count = 2 (at T+100ms)
└─> Execution 3: count = 3 (at T+200ms)
5. User calls undelegate() on Ephemeral Rollup
└─> Commits changes and moves Counter back to Solana base layer
```
## Core Components
### Scheduled Function
Let's take for example that you wanted to schedule the following simple increment instruction for a counter.
```rust theme={null}
pub fn increment(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
if counter.count > 1000 {
counter.count = 0;
}
msg!("PDA {} count: {}", counter.key(), counter.count);
Ok(())
}
```
### Schedule Increment Function
The core crank scheduling logic:
```rust theme={null}
pub fn schedule_increment(ctx: Context, args: ScheduleIncrementArgs) -> Result<()> {
let increment_ix = Instruction {
program_id: crate::ID,
accounts: vec![AccountMeta::new(ctx.accounts.counter.key(), false)],
// Defining the instruction to call.
data: anchor_lang::InstructionData::data(&crate::instruction::Increment {}),
};
let ix_data = bincode::serialize(&MagicBlockInstruction::ScheduleTask(
ScheduleTaskArgs {
task_id: args.task_id,
execution_interval_millis: args.execution_interval_millis,
iterations: args.iterations,
instructions: vec![increment_ix],
},
))
.map_err(|err| {
msg!("ERROR: failed to serialize args {:?}", err);
ProgramError::InvalidArgument
})?;
let schedule_ix = Instruction::new_with_bytes(
MAGIC_PROGRAM_ID,
&ix_data,
vec![
AccountMeta::new(ctx.accounts.payer.key(), true),
AccountMeta::new(ctx.accounts.counter.key(), false),
],
);
invoke_signed(
&schedule_ix,
&[
ctx.accounts.payer.to_account_info(),
ctx.accounts.counter.to_account_info(),
],
&[],
)?;
Ok(())
}
```
**Key Points:**
* Creates an instruction to increment the counter
* Serializes it into a `ScheduleTask` instruction for MagicBlock
* Uses CPI (Cross-Program Invocation) to call the MagicBlock program
* The MagicBlock program handles the scheduling and execution
### Schedule Arguments
```rust theme={null}
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct ScheduleIncrementArgs {
pub task_id: u64, // Unique identifier for the task
pub execution_interval_millis: u64, // Time between executions in milliseconds
pub iterations: u64, // Number of times to execute
}
```
### ScheduleIncrement Context
```rust theme={null}
#[derive(Accounts)]
pub struct ScheduleIncrement<'info> {
/// CHECK: used for CPI
#[account()]
pub magic_program: AccountInfo<'info>,
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: Passed to CPI - using AccountInfo to avoid Anchor re-serializing stale data after CPI
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: AccountInfo<'info>,
/// CHECK: used for CPI
pub program: AccountInfo<'info>,
}
```
**Important**: Uses `AccountInfo` instead of `Account` to avoid Anchor re-serializing stale data after CPI calls.
# Introduction
Source: https://docs.magicblock.gg/pages/tools/crank/introduction
Automated, time-based execution of on-chain instructions
## What Are Cranks?
Cranks (scheduled tasks) enable **automated, time-based execution** of on-chain instructions without requiring manual user intervention. In traditional blockchain systems, every action requires a user to sign and submit a transaction, which limits automation capabilities.
With MagicBlock's Ephemeral Rollups, you can schedule tasks that execute automatically at predetermined intervals, bringing powerful automation capabilities to your Solana programs.
## Key Benefits
* **Automated Execution**: Programs can execute instructions at predetermined intervals without user interaction
* **Cost Efficiency**: Reduces the need for off-chain cron jobs or monitoring services
* **Decentralization**: Execution happens on-chain, maintaining trustless guarantees
* **Reliability**: Scheduled tasks execute reliably within the blockchain's consensus mechanism
## Use Cases
* **Periodic State Updates**: Update prices, clear expired data, or refresh game state
* **Automated Workflows**: Recurring payments, vesting schedules, or subscription renewals
* **Game Mechanics**: Periodic rewards, time-based events, or automated game progression
* **DeFi Operations**: Rebalancing portfolios, liquidation checks, or yield compounding
Learn how to implement cranks in your program
Check out our GitHub repository
See where cranks fit into expiry checks, liquidation checks, and settlement.
## How It Works
Cranks leverage MagicBlock's Ephemeral Rollups to provide scheduled execution:
1. **Initialize** your program state on Solana base layer
2. **Delegate** accounts to Ephemeral Rollup for faster execution
3. **Schedule** a crank task that automatically executes instructions
4. **Execute** automatically at specified intervals
5. **Undelegate** accounts back to Solana base layer when done
The scheduling happens through Cross-Program Invocation (CPI) to MagicBlock's scheduling program, which handles the actual execution timing and reliability.
Cranks execute within the Ephemeral Rollup's consensus mechanism, ensuring reliable and trustless execution. For detailed implementation steps, see the Implementation guide.
# Health
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/health
pages/ephemeral-spl-token/api-reference/openapi/health.openapi.json GET /health
# MCP
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/mcp
pages/ephemeral-spl-token/api-reference/openapi/mcp.openapi.json POST /mcp
# Swap Quote
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/quote
pages/ephemeral-spl-token/api-reference/openapi/quote.openapi.json GET /v1/swap/quote
Returns a swap quote between two SPL mints. The quote response can be passed as-is into `POST /v1/swap/swap` to build the swap transaction.
# Swap
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/swap
pages/ephemeral-spl-token/api-reference/openapi/swap.openapi.json POST /v1/swap/swap
Build an unsigned swap transaction from a quote.
**Visibility modes:**
- **`visibility: "public"`** (default) — pure pass-through to the Jupiter/Metis upstream. The returned transaction is whatever the upstream produces.
- **`visibility: "private"`** — the server forces Jupiter's output into a program-owned stash ATA (deterministically derived from `(userPublicKey, quoteResponse.outputMint)`), prepends an idempotent ATA-create, and appends a `schedule_private_transfer` instruction that registers a one-shot Hydra crank. When the crank fires, it self-CPIs into the on-chain private-transfer flow to deliver the swapped tokens to `destination` with the requested delay/split policy. The returned transaction is a v0 `VersionedTransaction` that is still unsigned — the client signs and submits.
When `visibility = "private"`, the fields `destination`, `minDelayMs`, `maxDelayMs`, and `split` are **required**. `clientRefId` and `validator` are optional. Explicitly setting `destinationTokenAccount` to anything other than the server-derived stash ATA returns `400`.
# Ensure Transfer Queue Crank
Source: https://docs.magicblock.gg/pages/ephemeral-spl-token/api-reference/transfer-queue-ensure-crank
pages/ephemeral-spl-token/api-reference/openapi/transfer-queue-ensure-crank.openapi.json POST /v1/spl/transfer-queue/ensure-crank
After setup confirmation, verify that the validator-scoped transfer queue exists for a mint and force one crank attempt. Used to make sure queued private transfers make progress.
# Binary Prediction
Source: https://docs.magicblock.gg/pages/templates/binary-prediction
Build an up/down prediction flow with session-authorized bets, SPL token custody, real-time oracle prices, and Ephemeral Rollup settlement.
The Binary Prediction example lets a user stake SPL tokens on an up/down price move. It reads a
MagicBlock pricing-oracle feed on the ER, snapshots the entry price, and settles after the bet expires.
This is an integration example, not a production risk engine. The pool acts as the counterparty
without hedging its exposure and uses deliberately simple liquidity accounting.
Try the live binary prediction application.
Build, test, and run the Anchor program and client.
Place this example in a complete prediction-market or trading architecture.
Learn how the example combines Ephemeral Rollups, SPL tokens, session keys, and live prices.
## What the example demonstrates
* **Session-authorized execution:** place bets without repeated wallet prompts after setup.
* **SPL-token custody:** stake and pool liquidity move through delegated eATAs.
* **Real-time pricing:** opening and settlement prices come from the same configured oracle feed.
* **Expiry settlement:** wins pay the configured multiplier, ties refund, and losses remain in the pool.
## Recommended next steps
Understand eATA custody and post-commit payout alternatives.
Validate feed identity, freshness, and price data in your program.
Scope repeated trading actions and review the security boundaries.
Extend manual settlement with scheduled expiry checks.
# Counter
Source: https://docs.magicblock.gg/pages/templates/counter
Try the live application
Explore the source code and implementation
***
## Overview
Track and update counters on-chain with low latency using Ephemeral Rollups. This simple example demonstrates the core concepts of delegating accounts to ephemeral rollups and achieving high-frequency state updates.
***
## Related Products
Learn how Ephemeral Rollups work
Get started with Ephemeral Rollups
Build with Anchor framework
Understand transaction routing
# Gachapon
Source: https://docs.magicblock.gg/pages/templates/gachapon
Explore the source code and implementation
***
## Overview
Mint Metaplex NFTs through a gachapon-style machine powered by MagicBlock VRF. This template requests verifiable randomness to select the prize outcome, then mints the matching NFT so each pull is unpredictable, auditable, and suitable for games or collectible drops.
***
## Related Products
Learn why verifiable randomness matters
Get started with verifiable randomness
Learn VRF implementation best practices
Explore Metaplex NFT standards and tooling
# Onchain Dice
Source: https://docs.magicblock.gg/pages/templates/onchain-dice
Try the live application
Explore the source code and implementation
***
## Overview
Roll dice with provable on-chain randomness using VRFs. This demo shows how to implement fair and verifiable dice rolling mechanics.
***
## Related Products
Learn why verifiable randomness matters
Get started with verifiable randomness
Understand VRF security guarantees
Common questions about VRFs
# Oracle-Priced Purchase
Source: https://docs.magicblock.gg/pages/templates/oracle-priced-purchase
Learn oracle account validation, freshness checks, price conversion, and slippage protection with a compact Anchor example.
The Oracle-Priced Purchase example sells a USD-priced item for SOL using the MagicBlock real-time
pricing-oracle account format. It is a focused starting point for any protocol instruction whose
amount depends on a live market price.
Build and test the Anchor example against local oracle fixtures.
Apply the pricing pattern to positions, collateral, liquidation, and settlement.
## What the example demonstrates
* binding program configuration to an expected oracle feed ID;
* rejecting prices that are not fully verified or are older than 60 seconds;
* converting a USD-denominated amount into lamports;
* enforcing a buyer-provided maximum to protect against price movement.
Understand MagicBlock's low-latency pricing feeds.
Read and validate pricing accounts in your program.
# Private Payments
Source: https://docs.magicblock.gg/pages/templates/private-payments
Try the live application
Explore the source code and implementation
Fastest and simplest way to build private payments
Learn more about building private payments
***
## Overview
Send and receive payments privately on-chain using Private Ephemeral Rollups. This demo showcases how to implement confidential transactions while maintaining the security and composability of blockchain technology.
***
## Related Products
Learn how Private ERs enable confidential on-chain computations
Understand authorization mechanisms in Private ERs
Get started with building private applications
Compliance Standards and Guidelines
# Random Character Generator
Source: https://docs.magicblock.gg/pages/templates/random-character-generator
Try the live application
Explore the source code and implementation
Learn more about verifiable randomness
***
## Overview
Generate unique characters using verifiable randomness on-chain. This demo showcases how to leverage VRFs (Verifiable Random Functions) to create provably fair and unpredictable character attributes for gaming and NFT applications.
***
## Related Products
Learn why verifiable randomness matters
Get started with verifiable randomness
Learn VRF implementation best practices
Understand how VRF works under the hood
# Real-Time Price Feed
Source: https://docs.magicblock.gg/pages/templates/real-time-price-feed
Try the live application
Explore the source code and implementation
Learn more about real-time price feeds
***
## Overview
Access live market prices via Pyth oracles on Ephemeral Rollups. This template demonstrates how to integrate real-time price data into your on-chain applications with minimal latency.
***
## Related Products
Learn how Ephemeral Rollups enable real-time operations
Understand how to integrate price oracles
Learn how to implement oracle-based applications
Get started with Ephemeral Rollups
# Rock Paper Scissors
Source: https://docs.magicblock.gg/pages/templates/rock-paper-scissors
Try the live application
Explore the source code and implementation
***
## Overview
Play confidential Rock Paper Scissors using Private Ephemeral Rollups. Each move lives in a private TEE account that neither the opponent nor the RPC can read — the winner is revealed automatically the instant the last choice lands, and either player can start a rematch that reuses the same accounts with no new rent.
The example includes a web app with a solo mode against a robot and a two-player mode where a friend joins via link or QR code.
***
## Related Products
Learn how Private ERs enable confidential on-chain computations
Understand authorization mechanisms in Private ERs
Get started with building private applications
Set fine-grained permissions on ephemeral accounts
# Sealed-Bid Auction
Source: https://docs.magicblock.gg/pages/templates/sealed-bid-auction
Run a private first-price sealed-bid auction with hidden bids on a Private Ephemeral Rollup, SPL token escrow on both layers, and on-chain winner selection.
The Sealed-Bid Auction example sells a fixed SPL token lot to the highest of a set of hidden,
fully collateralized bids. The seller's lot stays escrowed in a base-layer token account owned by the
auction PDA, bidders place private bids on a Private Ephemeral Rollup (PER), and after the deadline
the program scans the bid accounts on-chain, pays the seller, refunds the losers, and releases the
lot to the winner on the base layer.
This is an integration example, not a production auction house. It implements a single first-price
auction per lot with deliberately simple deadline and refund handling.
Build, test, and run the Anchor program and client.
Understand the PER privacy model behind the hidden bid state.
## What the example demonstrates
* **Hidden bid state on a PER:** each bid lives in a private, auction-sponsored PDA that only gains
PER access at creation, so amounts and bidders stay hidden until settlement.
* **Two-layer SPL escrow:** the seller's lot is parked in a base-layer auction ATA while bid
collateral moves through a delegated auction-owned escrow on the ER.
* **On-chain winner selection:** `end_auction` count-checks and scans exactly the accepted bid PDAs
on the ER — no off-chain matching or reveal phase.
* **Cleanup-gated undelegation:** the auction PDA can only undelegate after every bid account is
settled and closed, and the base-layer `finalize` then transfers the lot to the winner.
## Recommended next steps
Set up a Private Ephemeral Rollup and its access control.
Custody and move SPL tokens from your own program on the ER.
Grant and revoke PER permissions on program accounts.
Compose custody, privacy, and settlement into a full protocol.
# VRF Rewards
Source: https://docs.magicblock.gg/pages/templates/vrf-rewards
Mint random rewards on an Ephemeral Rollup with a delegated reward account, verifiable randomness from the VRF oracle, and commit-back settlement to Solana.
The VRF Rewards example mints random rewards for a delegated account on an Ephemeral Rollup. The
program requests randomness on the ER, the VRF oracle fulfills it by invoking the program's callback
with verified random bytes, the program derives the reward from those bytes, and the state commits
back to the base layer.
Try the live rewards dashboard.
Build, test, and run the Anchor program and client.
How verifiable randomness works on MagicBlock.
## What the example demonstrates
* **VRF on a delegated account:** the reward account lives on the ER, so randomness requests and
reward minting run at rollup speed.
* **Oracle callback pattern:** the program exposes a callback instruction that only the VRF oracle
can fulfill, receiving verified random bytes on-chain.
* **Randomness-derived state:** reward selection happens deterministically from the verified bytes —
no off-chain draw to trust.
* **Commit-back settlement:** the final reward state is committed from the ER back to Solana, ready
for base-layer payouts.
## Recommended next steps
Request and consume randomness in your own program.
The delegation and commit lifecycle behind the example.
Pay rewards out in SPL tokens — ER custody or post-commit payouts.
Trigger base-layer follow-ups atomically when the commit lands.
# Introduction
Source: https://docs.magicblock.gg/pages/tools/introduction
Utilize existing SDKs and Frameworks to accelerate your development process
* [**Wallets & Onramp**](/pages/tools/wallets-and-onramp/overview) – Integrate with existing **wallet and onramp** providers
* [**Session Keys**](/pages/tools/session-keys/introduction) – Implement **tiered access management** for Solana programs
* [**Solana Unity SDK**](/pages/tools/solana-unity-sdk/overview) – Integrate Solana into **Unity-based games** effortlessly.
* [**SOAR**](/pages/tools/open-source-programs/SOAR) – Enable **on-chain achievements, leaderboards, and rewards**.
Onboard users with wallets frictionlessly
Integrate with Session Keys
Create games with the Solana Unity SDK
Create on-chain achievement, leaderboard and rewards with SOAR
Looking for **Price Oracle** or **Cranks**? The Price Oracle now has its own
product section, and Cranks live under **Ephemeral Rollup → Guides**.
# SOAR
Source: https://docs.magicblock.gg/pages/tools/open-source-programs/SOAR
Solana On-Chain Achievement & Ranking
SOAR is a program that provides a seamless solution for managing leaderboards, achievements, players' profiles and automatic rewards distribution on the Solana blockchain. Currently supporting invocation from a TypeScript client, the integration in Solana.Unity-SDK will be coming soon.
## Contents
* [Getting started](#quick-start)
* [Classes](#classes)
* [SoarProgram](#soarprogram)
* [GameClient](#gameclient)
* [InstructionBuilder](#instructionbuilder)
## Quick start
### Create a new game
```typescript theme={null}
import { SoarProgram, GameType, Genre } from "@magicblock-labs/soar-sdk";
// Create a Soar client using the '@solana/web3.js' active Connection and a defaultPayer
const client = SoarProgram.getFromConnection(connection, defaultPayer);
let game = Keypair.generate();
let title = "Game1";
let description = "Description";
let genre = Genre.Action;
let gameType = GameType.Web;
let nftMeta = Keypair.generate().publicKey;
let _auths = auths.map((keypair) => keypair.publicKey);
// Retrieve the bundled transaction.
let { newGame, transaction } = await client.initializeNewGame(
game.publicKey,
title,
description,
genre,
gameType,
nftMeta,
_auths
);
// Send and confirm the transaction with the game keypair as signer.
await web3.sendAndConfirmTransaction(connection, transaction);
```
### Create a leaderboard
```typescript theme={null}
const transactionIx = await client.addNewGameLeaderBoard(
newGame,
authWallet.publicKey,
"my leaderboard", // description
leaderboardNft, // nft associated with the leaderboard
100,
true // isAscending
);
await web3.sendAndConfirmTransaction(connection, transactionIx.transaction, [
authWallet,
]);
```
### Submit a score
```typescript theme={null}
const score = 10;
const playerAddress = new web3.PublicKey("..."); // The player publicKey
const authWallet = web3.Keypair.fromSecretKey(bs58.decode("")); // AUTH_WALLET_PRIVATE_KEY
const leaderboardPda = new web3.PublicKey(""); // LEADERBOARD_PDA
const transactionIx = await client.submitScoreToLeaderBoard(
playerAddress,
authWallet.publicKey,
leaderboardPda,
new BN(score)
);
await web3.sendAndConfirmTransaction(connection, transactionIx.transaction, [
authWallet,
]);
```
## Classes
### SoarProgram
The `SoarProgram` class gives client access to every instruction in the on-chain SOAR program.
It also gives utility functions for deriving PDAs:
```typescript theme={null}
const user = Keypair.generate().publicKey;
const playerAddress = client.utils.derivePlayerAddress(user)[0];
```
fetching an account:
```typescript theme={null}
const account = await client.fetchLeaderBoardAccount(address);
```
and fetching multiple accounts:
```typescript theme={null}
const accounts = await client.fetchAllLeaderboardAccounts([]);
```
### GameClient
The `GameClient` provides a more specific set of functions tailored to a single Game account.
```typescript theme={null}
import { GameClient } from "@magicblock-labs/soar-sdk";
```
Get an instance representing an existing on-chain Game account:
```typescript theme={null}
const soar = SoarProgram.getFromConnection(connection, defaultPayer);
const gameClient = new GameClient(soar, address);
```
Register a new game:
```typescript theme={null}
const soar = SoarProgram.getFromConnection(connection, defaultPayer);
const game = new GameClient.register(soar, ...);
```
```typescript theme={null}
// Create a new leaderboard:
await game.addLeaderboard(....);
// Access the game's state.
await game.init();
// Refresh the game's state.
await game.refresh();
// Get the most recently-created achievement for a game
const achievement = game.recentAchievementAddress();
```
## InstructionBuilder
```typescript theme={null}
import { InstructionBuilder } from "@magicblock-labs/soar-sdk";
```
The InstructionBuilder provides a set of methods for conveniently bundling transactions.
```typescript theme={null}
const transaction = await this.builder
.andInitializePlayer({ username, nftMeta }, user)
.andRegisterPlayerEntry(/*...*/)
.andSubmitScoreToLeaderboard(/*...*/)
.and(/*some other transaction*/)
.then((builder) => builder.build());
```
# Implementation
Source: https://docs.magicblock.gg/pages/tools/oracle/implementation
Derive the Pyth Lazer price feed account and decode its bytes
## Deriving the Price Feed Account
We derive the PDA of the price account using the following seeds:
* **"price feed"** (buffer)
* **"pyth-lazer"** (buffer)
* **feedID** (buffer) — the asset you want. Find the supported feeds [here](https://github.com/magicblock-labs/real-time-pricing-oracle/blob/main/pyth_lazer_list.json).
* **price program id** — `PriCems5tHihc6UDXDjzjeawomAwBduWMGAi8ZUjppd`
```ts theme={null}
// seeds: ["price_feed", "pyth-lazer", feed_id_as_string]
function deriveFeedAddress (feedId: string) {
const [addr] = web3.PublicKey.findProgramAddressSync(
[Buffer.from('price_feed'), Buffer.from('pyth-lazer'), Buffer.from(feedId)],
PROGRAM ID
);
return addr
}
```
## Parsing the Account
The on‑chain account stores a header and fields for price data. We read raw bytes and decode.
* **Price offset**: 73 bytes from start
* **Type**: signed 64‑bit integer (`i64`)
* **Apply exponent**: scale the raw price using the account's exponent
```ts theme={null}
const addr = deriveFeedAddress(feed.id);
const PRICE_OFFSET = 73;
const dv = new DataView(ai.data.buffer, ai.data.byteOffset, ai.data.byteLength);
const raw = dv.getBigUint64(PRICE_OFFSET, true);
const price = Number(raw) * Math.pow(10, feed.exponent);
```
Real‑time price stream
Learn how to access our oracles onchain
Check out our Github Repo
# Introduction
Source: https://docs.magicblock.gg/pages/tools/oracle/introduction
Real-time onchain data with MagicBlock Ephemeral Rollups
## Oracles on MagicBlock
MagicBlock provides low‑latency, high‑throughput oracle data by ingesting Pyth Lazer feeds and updating Ephemeral Rollup accounts at 50–200 ms intervals (asset‑dependent).
Real‑time price stream
Learn how to access our oracles onchain
Check out our Github Repo
This Oracle example uses Pyth Lazer, but we can create Oracles for any arbitrary data source.
## What Are Onchain Oracles?
Onchain oracles deliver verifiable off‑chain data that programs can trust. Use cases range from asset prices to event outcomes.
On Solana, oracles typically keep accounts updated on‑chain. Programs read these accounts directly—no external API calls at execution time. We use Pyth, a widely adopted cross‑chain oracle network.
## Why Oracles Matter
* **Finance**: liquidations, funding, TWAPs — inaccurate quotes cause loss and risk
* **Games**: settle sports results; sync in‑game state with real‑world events
* **Composability**: reliable, on‑chain data enables secure program composition
Accuracy and latency directly impact correctness, safety, and UX.
Place real-time price feeds in a complete trading flow with custody, sessions, automation, and settlement.
## Oracles on MagicBlock
MagicBlock follows the standard oracle pattern—writing data into composable on‑chain accounts—while updating at 50–200 ms (asset‑dependent) versus \~400 ms on Solana slots. This latency profile is well‑suited for liquidations, copy‑trading, and other time‑sensitive flows.
### The two pieces of an oracle
* **Data Source**: The upstream truth. We can ingest arbitrary on/off‑chain feeds to surface assets Pyth doesn’t cover (e.g., new PumpFun or Raydium R‑tokens) into Ephemeral Rollups.
* **Chain Pusher**: Processes the source feed and writes updates on‑chain. MagicBlock’s chain pusher will be open‑sourced.
### Flow
1. Receive Pyth Lazer updates at fixed intervals (50 ms or 200 ms by asset).
2. Push updates to predefined on‑chain accounts.
3. Programs read the relevant account directly.
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Solana Devnet: [https://api.devnet.solana.com](https://api.devnet.solana.com)
ER Devnet (Asia): [https://devnet-as.magicblock.app](https://devnet-as.magicblock.app)
ER Devnet (EU): [https://devnet-eu.magicblock.app](https://devnet-eu.magicblock.app)
ER Devnet (US): [https://devnet-us.magicblock.app](https://devnet-us.magicblock.app)
TEE Devnet: [https://devnet-tee.magicblock.app/](https://devnet-tee.magicblock.app/)
Find out more details
here
.
This page is an overview. For byte‑level details and code examples, see the Implementation.
Code snippets intentionally use placeholders to keep the focus on the flow.
# How do Session Keys work?
Source: https://docs.magicblock.gg/pages/tools/session-keys/how-do-session-keys-work
What are Session Keys?
Session Keys are meant to be used as secondary signers in your program, especially for frequent interactions like liking a post or moving a piece in a game of chess where constant popups can get in the way of smooth user experience. **They are not burner wallets**.
Session Keys work in tandem with our on chain program to validate the token and it's scope.
## **Session Keys have two components**
1. An **Ephemeral Keypair**, intended to be used as a **secondary signer** in the target program.
2. A **Session Token**, a PDA containing information about **expiry and scope** of the keypair.
## **How do they work?**
* Ephemeral Keys are stored on the client side, to invoke transactions.
* The transactions invoked by these ephemeral keys are validated in the target program for their validity, expiry and scope.
* Every transaction needs to present both the ephemeral signer and the session token
* This is the general idea behind *account abstraction*, where instead of just an externally owned key there is also smart contract that enhances security.
# Installation
Source: https://docs.magicblock.gg/pages/tools/session-keys/installation
Step-by-step guide on setting up and integrating the session wallet management system into your dApp
Session Keys are a part of the Gum React-SDK, so you need to install the package using either yarn or npm:
```bash Yarn theme={null}
yarn add @magicblock-labs/gum-react-sdk
```
```bash Npm theme={null}
npm i @magicblock-labs/gum-react-sdk
```
With the package installed, you can now begin integrating the session wallet management system into your dApp for enhanced security and user experience.
# Integrating Sessions in your Program
Source: https://docs.magicblock.gg/pages/tools/session-keys/integrating-sessions-in-your-program
Integrate and manage sessions in your Solana Programs
This guide demonstrates how to integrate Session Keys into your Solana Anchor programs, using a simple counter program as an example.
## Installation
First, add the session-keys crate to your Cargo.toml:
```toml theme={null}
[dependencies]
session-keys = { version = "3.1.1", features = ["no-entrypoint"] }
```
Or use the cargo command:
```bash theme={null}
cargo add session-keys --features no-entrypoint
```
## Usage
1. Importing session-keys:
```rust theme={null}
use session_keys::{SessionError, SessionToken, session_auth_or, Session};
```
This line imports the necessary components from the session-keys crate.
2. Deriving the Session trait:
```rust theme={null}
#[derive(Accounts, Session)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [ COUNTER_SEED, counter.authority.key().as_ref() ],
bump
)]
pub counter: Account<'info, Counter>,
...
}
```
The `Session` trait is derived on the `Increment` struct, enabling session functionality.
3. Defining the session token account:
```rust theme={null}
#[session(
signer = signer,
authority = counter.authority.key()
)]
pub session_token: Option>,
```
This defines an optional `SessionToken` account, specifying the signer and authority for the session.
* session\_token.authority: account which created the session token
* counter.authority.key(): account which created the counter
The authority condition checks if the session token is created by the same user as the counter.
4. Using the `session_auth_or` macro:
```rust theme={null}
#[session_auth_or(
ctx.accounts.counter.authority.key() == ctx.accounts.signer.key(),
SessionError::InvalidToken
)]
pub fn increment(ctx: Context) -> Result<()> {
...
}
```
This macro is applied to the `increment` function.
It checks for a valid session token, or if not present, verifies that the signer is the counter's authority.
## Full Example
Here's a complete example of a counter program using session keys.
Each user gets his own counter account, so we can show how authentication is done with session keys.
```rust theme={null}
use anchor_lang::prelude::*;
use session_keys::{SessionError, SessionToken, session_auth_or, Session};
declare_id!("...");
const COUNTER_SEED: &[u8] = b"counter";
#[program]
pub mod counter_session {
use super::*;
pub fn initialize(ctx: Context) -> Result<()> {
let counter: &mut Counter = &mut ctx.accounts.counter;
counter.count = 0;
counter.authority = *ctx.accounts.owner.key;
Ok(())
}
#[session_auth_or(
ctx.accounts.counter.authority.key() == ctx.accounts.signer.key(),
SessionError::InvalidToken
)]
pub fn increment(ctx: Context) -> Result<()> {
let counter: &mut Counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = owner,
space = Counter::INIT_SPACE + 8 ,
seeds = [ COUNTER_SEED, owner.key().as_ref() ], bump
)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub owner: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts, Session)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [ COUNTER_SEED, counter.authority.key().as_ref() ],
bump
)]
pub counter: Account<'info, Counter>,
#[session(
signer = signer,
authority = counter.authority.key()
)]
pub session_token: Option>,
#[account(mut)]
pub signer: Signer<'info>,
}
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub authority: Pubkey,
pub count: u64,
}
```
## Tests
Here's an example of how to test the counter program with session keys:
```typescript theme={null}
import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { CounterSession } from "../target/types/counter_session";
import { createSessionToken } from "@session-keys/anchor";
import { expect } from "chai";
describe("counter_session", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.CounterSession as Program;
let counterPDA: anchor.web3.PublicKey;
let sessionToken: anchor.web3.PublicKey;
it("Initializes the counter", async () => {
const [pda] = await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from("counter"), provider.wallet.publicKey.toBuffer()],
program.programId
);
counterPDA = pda;
await program.methods
.initialize()
.accounts({
counter: counterPDA,
owner: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const counterAccount = await program.account.counter.fetch(counterPDA);
expect(counterAccount.count).to.equal(0);
expect(counterAccount.authority.toString()).to.equal(provider.wallet.publicKey.toString());
});
it("Increments the counter without session", async () => {
await program.methods
.increment()
.accounts({
counter: counterPDA,
signer: provider.wallet.publicKey,
})
.rpc();
const counterAccount = await program.account.counter.fetch(counterPDA);
expect(counterAccount.count).to.equal(1);
});
it("Increments the counter with session token", async () => {
await program.methods
.increment()
.accounts({
counter: counterPDA,
sessionToken: sessionToken,
signer: provider.wallet.publicKey,
})
.rpc();
const counterAccount = await program.account.counter.fetch(counterPDA);
expect(counterAccount.count).to.equal(2);
});
it("fails to increment with wrong session token owner", async () => {
const user = anchor.web3.Keypair.generate();
await topUp(user);
let counterPDA = await createCounterPDA(user.publicKey);
await createCounter(user);
const secondUser = anchor.web3.Keypair.generate();
await topUp(secondUser);
const { sessionSigner, sessionToken } = await createSessionSigner(secondUser);
try {
await increment_with_session(counterPDA, sessionSigner, sessionToken);
assert(false, "Expected to fail");
} catch (err) {}
const counterData = await program.account.counter.fetch(counterPDA);
assert(counterData.count.eq(new anchor.BN(0)));
});
});
```
This test suite demonstrates initializing the counter, incrementing it without a session, creating a session token, and then incrementing with the session token.
The last test is important, as it ensures that only the owner of the counter can increment it.
## Testing locally
To test it locally with `solana-test-validator`, you need to start it with the session keys program and account.
1. Make sure your Solana CLI points to DEVNET:
```
solana config set --url https://api.devnet.solana.com
```
2. Dump Session Keys program to local file:
```
solana program dump KeyspM2ssCJbqUhQ4k7sveSiY4WjnYsrXkC8oDbwde5 ./session-keys.so
```
3. Start solana-test-validator with session keys program and account:
> `-r` - reset the ledger to genesis
> `-ud` - URL for Solana's JSON RPC or moniker (-ud = DEVNET)
> `--clone` - Copy an account from the cluster
> `--bpf-program` - add a SBF program to the genesis configuration
```
solana-test-validator -ud --clone KeyspM2ssCJbqUhQ4k7sveSiY4WjnYsrXkC8oDbwde5 -r --bpf-program KeyspM2ssCJbqUhQ4k7sveSiY4WjnYsrXkC8oDbwde5 ./session-keys.so
```
# Introduction
Source: https://docs.magicblock.gg/pages/tools/session-keys/introduction
What are Session Keys?
Session Keys are ephemeral keys with fine-grained instruction scoping for tiered access in your Solana Programs.
Session keys are a giant leap for improving UX for users as they take away the need for repeated wallet popups while a user performs actions in-game like purchases or on-chain interactions. The expiry and access are stored at the contract level, which reduces the exposure of session keys to potential security vulnerabilities, though it does not eliminate risks from browser compromise, malicious extensions, or application bugs. The Session Keys parameters can be duration, the maximum amount of tokens spent, amount of transactions or any other function specific to your use case.
You can also provide a layered security model which allows tiered access to a session key making sure a user’s assets are always secure and can’t be accessed by the session keys.
This type of layered security is a standard model in web2 applications and provides a stronger defence against attacks and helps ensure the security of your users' asset. This is now possible in web3 with the use of Session Keys at the contract level.
#### Example use cases for Session Keys
* An uninterrupted gaming experience for an on-chain game.
* A seamless experience for in-app NFTs purchase
* A layered security model for your game/dApp
Use scoped sessions for repeated orders, bets, and position updates without repeated wallet prompts.
The original Session key program was developed by *[Gum](https://gum.fun/)*.
The newly deployed program is 'KeyspM2ssCJbqUhQ4k7sveSiY4WjnYsrXkC8oDbwde5'
# Security
Source: https://docs.magicblock.gg/pages/tools/session-keys/security
Key management and security model
## **Key Management**
Key Management is an extremely important aspect of the security. One thing to note here however is that how securely one can manage the keys is platform dependent, for example a mobile app with access to local keystore/keychain is a lot more secure compared to a web browser.\
\
Our current client side key management is only on the web. Given the constraints, we do take adequate security measures on the browser.
\
The ephemeral keypair is encrypted and safely stored in the user's browser using IndexedDB, an in-browser database. When a user initiates an action, such as signing and sending a transaction, the session token signs the transaction using the temporary key pair. The smart contract can then validate the transaction, confirming that the user's wallet authorized the session token.\\
1. Generate a random keypair using `web3.Keypair.generate()`
2. Generate a random encryption key
3. Using the encryption key encrypt the generated keypair.
4. Store the encrypted key in IndexedDB
## Security Model
1\. The session keys are like your JWT Token adapted to web3
2\. These keys have an expiry and a scope
3\. Once the key has expired they can't be reused in the target program. A new session token is to be generated.
4\. They are also designed to be revokable, so that in the worst case if something wrong happens the attack surface is limited to the ephemeral keypair and the assets contained in them. i.e **0.01 SOL**
### Note on IndexedDB
Web Browser is an extremely adversarial environment, no amount of security is enough there, right from cookies to session to extension's sandbox.
Attackers could always inject arbitrary code via XSS or malicious extension. This is why users are discouraged from storing serious funds in a browser wallet, they are only for day to day expenses and it is really important to establish the distinction that **Session Keys are not burner wallets.**
However, majority of web3 today is on web browsers and that's how users primarily interact with other dApps. Given the constraints of today, we have to **design around them** and **harden** them via other means.
1. Drastically reduce the scope of what's possible with an ephemeral signer, they are highly context and use case specific.
2. This is similar to the approach to JWT in a typical client server architecture in web2.
3. For example the session or JWT tokens on Facebook, twitter or even banking website could be vulnerable to the same issue. The way they address is this by **limiting the scope** of what **you can do with a token**, have **intelligent systems** in place to **revoke** them and further introduce 2FA for suspicious activity.
4. We follow a similar model to limit the scope, we are also working on adding intelligent revocation systems which can revoke a compromised token as soon as we witness a malicious transaction like out of scope usage etc. **The absolute worst case scenario in terms of loss of funds is the 0.01 SOL topped up to pay for the gas fee.**
5. Also\*\*, developers can work around this today by pairing it with a gasless relay like octane and setting `topUp to false`\*\*. However, we don't have a seamless way to do it directly from our SDK yet, although it is on our roadmap.
# Session Provider & Context
Source: https://docs.magicblock.gg/pages/tools/session-keys/session-provider-and-context
Understand the usage of the SessionWalletProvider and the context it provides, enabling easy access to the session wallet functionalities across your application components?
`SessionWalletProvider` is a higher-order component that wraps around your app components to provide the `sessionWallet` context throughout the application.
Here's an example of how to use the `SessionWalletProvider`:
1. Create a new file named `components/SessionProvider.tsx`
```typescript theme={null}
// components/SessionProvider.tsx
// The SessionProvider component initializes the SessionKeyManager and provides it to its children via context.
// Wrap any component that needs access to the SessionKeyManager with this provider.
import {
SessionWalletProvider,
useSessionKeyManager,
} from "@magicblock-labs/gum-react-sdk";
import {
AnchorWallet,
useAnchorWallet,
useConnection,
} from "@solana/wallet-adapter-react";
interface SessionProviderProps {
children: React.ReactNode;
}
const SessionProvider: React.FC = ({ children }) => {
const { connection } = useConnection();
const anchorWallet = useAnchorWallet() as AnchorWallet;
const cluster = "devnet"; // or "mainnet-beta", "testnet", "localnet"
//here the useSessionKeyManager takes in 3 properties
const sessionWallet = useSessionKeyManager(anchorWallet, connection, cluster);
return (
{children}
);
};
export default SessionProvider;
```
2. In your `_app.tsx` file, wrap the SessionProvider around the entire app to ensure it's accessible within every component:
```typescript theme={null}
// pages/_app.tsx
import SessionProvider from "@/components/SessionProvider";
import { WalletContextProvider } from "@/contexts/WalletContextProvider";
import "@/styles/globals.css";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import {
PhantomWalletAdapter,
SolflareWalletAdapter,
} from "@solana/wallet-adapter-wallets";
import { clusterApiUrl } from "@solana/web3.js";
import type { AppProps } from "next/app";
import React, { useMemo } from "react";
import dotenv from "dotenv";
// Use require instead of import since order matters
require("@solana/wallet-adapter-react-ui/styles.css");
dotenv.config();
export default function App({ Component, pageProps }: AppProps) {
const network = WalletAdapterNetwork.Devnet;
const endpoint =
process.env.NEXT_PUBLIC_SOLANA_ENDPOINT || clusterApiUrl(network);
const wallets = useMemo(
() => [
new PhantomWalletAdapter({ network }),
new SolflareWalletAdapter({ network }),
],
[network]
);
return (
);
}
```
***Note: Ensure that all your Solana wallet adapter contexts are the parent of the SessionProvider.***
3. With the SessionWalletProvider set up, you can now use the `useSessionWallet` hook in your components:
### Using `useSessionWallet` in components
`useSessionWallet` is a custom hook that provides access to the session wallet context value. Use this hook in any component wrapped by the SessionWalletProvider.
To properly understand what the `useSessionWallet` hook provides, we need to examine the `SessionWalletInterface`. This is what `useSessionKeyManager` returns, providing the methods needed to utilize session keys as well as transaction signing and sending capabilities.
```typescript theme={null}
interface SessionWalletInterface {
publicKey: PublicKey | null; // Public key associated with the session wallet
ownerPublicKey: PublicKey | null; // The Publickey of the session token authority(The creator of the session token)
isLoading: boolean; // Indicates whether the session wallet is loading
error: string | null; // An error message, if any
sessionToken: string | null; // Session token for the current session
signTransaction:
| ((
transaction: T,
connection?: Connection,
sendOptions?: SendTransactionOptions
) => Promise)
| undefined; // Sign a single transaction
signAllTransactions:
| ((
transactions: T[],
connection?: Connection,
sendOptions?: SendTransactionOptions
) => Promise)
| undefined; // Sign multiple transactions
signMessage: ((message: Uint8Array) => Promise) | undefined; // Sign a message
sendTransaction:
| ((
transaction: T,
connection?: Connection,
options?: SendTransactionOptions
) => Promise)
| undefined; // Send a signed transaction
signAndSendTransaction:
| ((
transactions: T | T[],
connection?: Connection,
options?: SendTransactionOptions
) => Promise)
| undefined; // Sign and send transactions
createSession: (
targetProgram: PublicKey, // Target Solana program
topUpLamports?: number, // Top up session wallet with lamports or not
validUntil?: number, // Duration of session token before expiration
sessionCreatedCallback?: (sessionInfo: {
sessionToken: string;
publicKey: string;
}) => void
) => Promise; // Create a new session
revokeSession: () => Promise; // Revoke the current session
getSessionToken: () => Promise; // Retrieve the current session token
}
```
This will help us understand how to use the sessionWallet in our code for creating session tokens as well as revoking them when no longer needed.
```typescript theme={null}
import { useSessionWallet } from "@magicblock-labs/gum-react-sdk";
function YourComponent() {
const sessionWallet = useSessionWallet();
//create session token
const session = await sessionWallet.createSession(
// pass needed params here
);
//Revoke Session Wallet
const result = await sessionWallet.revokeSession();
//Access the session signer Publickey
sessionWallet.publicKey,
//To access the session token
sessionWallet.sessionToken
return (
// Your component JSX
);
}
```
# Usage Example
Source: https://docs.magicblock.gg/pages/tools/session-keys/usage-examples
Learn how to interact with your dApp using the useSessionKeyManager and SessionWalletProvider through various practical examples
Now that you have set up the hooks and provider, let's look at some examples of using the provided methods.
### Creating a Session
To create a session, call the `createSession` method from the `sessionWallet`. This method accepts three parameters:
1. `targetProgramPublicKey`: A `PublicKey` instance representing the target program you want to interact with.
2. `topUp`: A boolean value, set to `true` if you want to top up a session keypair with `0.01 SOL` initially and \`false\` if you don't want to top up the session keypair.
3. `expiryInMinutes`: An optional parameter, representing the session's expiry time in minutes. The default value is 60 minutes.
```typescript theme={null}
const handleCreateSession = async () => {
const targetProgramPublicKey = new PublicKey(
"your_target_program_public_key"
);
const topUp = true;
const expiryInMinutes = 60;
const session = await sessionWallet.createSession(
targetProgramPublicKey,
topUp,
expiryInMinutes
);
// you can also specify the amount you want to topUp the session wallet.
//This will top up the amount specified, and when revoked, send back to the authority
const session = await sessionWallet.createSession(
targetProgramPublicKey,
topUp ? 10000000 : 0, // 0.01 SOL
expiryInMinutes
);
if (session) {
console.log("Session created:", session);
} else {
console.error("Failed to create session");
}
};
```
By calling `createSession`, a new ephemeral keypair is generated and stored on the client-side. The session token is then created and stored alongside the keypair. This enables the user to securely sign transactions using the generated keypair without revealing their actual wallet's private key.
### Signing and Sending a Transaction
To sign and send a transaction, use the `signAndSendTransaction` method. This method first signs the transaction using the ephemeral key pair created during the session. Then, it sends the signed transaction to the Solana network.
```typescript theme={null}
const handleSendTransaction = async () => {
const transaction = new Transaction();
// Add instructions to the transaction
const txids = await sessionWallet.signAndSendTransaction(transaction);
if (txids && txids.length > 0) {
console.log("Transaction sent:", txids);
} else {
console.error("Failed to send transaction");
}
};
```
The `signAndSendTransaction` method provides an extra layer of security by ensuring that the actual wallet's private key is not exposed. The ephemeral key pair stored on the client-side is used to sign the transaction, thus keeping the user's main wallet secure.
### Revoking a Session
To revoke a session, call the `revokeSession` method from the `sessionWallet`. This method performs three actions:
1. It removes the ephemeral key pair and the session token from the client-side storage.
2. It revokes the session from the contract.
3. Returns the lamports to the authority and closes the session token pda
```typescript theme={null}
const handleRevokeSession = async () => {
await sessionWallet.revokeSession();
console.log("Session revoked");
};
```
Revoking a session ensures that the ephemeral key pair is no longer valid and usable.
These examples should help you get started with implementing session management and wallet functionality in your app.
***Please refer to the*** [***session-keys example***](https://github.com/magicblock-labs/magicblock-engine-examples/tree/main/session-keys) ***to see how the Session Token implementation is done.***
Try it live at [counter-session-keys-example.magicblock.app](https://counter-session-keys-example.magicblock.app/) — a counter demo that runs on both the base layer and the Ephemeral Rollup using session keys.
# Use session key manager
Source: https://docs.magicblock.gg/pages/tools/session-keys/use-sessionkey-manager
A custom React hook that manages the creation and revocation of session tokens, and provides essential methods for signing and sending transactions in a secure and user-friendly way
`useSessionKeyManager` is a custom hook that takes an `AnchorWallet`, `Connection`, and `Cluster` as arguments and returns a `SessionWalletInterface`. This hook manages the session keys, tokens, and provides methods for signing and sending transactions.
The `SessionWalletInterface` consists of the following properties and methods:
```typescript theme={null}
interface SessionWalletInterface {
publicKey: PublicKey | null; // Public key associated with the session wallet
ownerPublicKey: PublicKey | null; // The Publickey of the session token authority(The creator of the session token)
isLoading: boolean; // Indicates whether the session wallet is loading
error: string | null; // An error message, if any
sessionToken: string | null; // Session token for the current session
signTransaction:
| ((
transaction: T,
connection?: Connection,
sendOptions?: SendTransactionOptions
) => Promise)
| undefined; // Sign a single transaction
signAllTransactions:
| ((
transactions: T[],
connection?: Connection,
sendOptions?: SendTransactionOptions
) => Promise)
| undefined; // Sign multiple transactions
signMessage: ((message: Uint8Array) => Promise) | undefined; // Sign a message
sendTransaction:
| ((
transaction: T,
connection?: Connection,
options?: SendTransactionOptions
) => Promise)
| undefined; // Send a signed transaction
signAndSendTransaction:
| ((
transactions: T | T[],
connection?: Connection,
options?: SendTransactionOptions
) => Promise)
| undefined; // Sign and send transactions
createSession: (
targetProgram: PublicKey, // Target Solana program
topUpLamports?: number, // Top up session wallet with lamports or not
validUntil?: number, // Duration of session token before expiration
sessionCreatedCallback?: (sessionInfo: {
sessionToken: string;
publicKey: string;
}) => void
) => Promise; // Create a new session
revokeSession: () => Promise; // Revoke the current session
getSessionToken: () => Promise; // Retrieve the current session token
}
```
Here's an example of how to use the `useSessionKeyManager`:
```typescript theme={null}
import { useAnchorWallet, useConnection } from '@solana/wallet-adapter-react';
import { useSessionKeyManager } from '@magicblock-labs/gum-react-sdk';
function YourComponent() {
const wallet = useAnchorWallet();
const connection = useConnection();
const cluster = "devnet"; // or "mainnet-beta", "testnet", "localnet"
const sessionWallet = useSessionKeyManager(wallet, connection, cluster);
// Access session wallet properties and methods here
// Example: sessionWallet.publicKey
// Example: sessionWallet.createSession
return (
// Your component JSX
);
}
```
To use the Session Key Manager across multiple components, you can set up the Provider Component and Context in your application in the following section.
# Contribution Guide
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/contribution-guide
Thank you for your interest in contributing to the Solana.Unity-SDK! This is an open-source community project and all contributions are welcomed, no matter how big or small. Contributions might include (but is not limited to) filing issues, adding documentation, fixing bugs, creating examples, and/or implementing features.
## Finding issues to work on
If you're looking to get started, check out the [open issues](https://github.com/magicblock-labs/Solana.Unity-SDK/issues). For simple documentation changes or typos, feel free to just open a pull request.
If you're considering larger changes or self motivated features, please file an issue and engage with the maintainers in [Telegram](https://t.me/+78KHQkUsy0ViMzQ6).
## Choosing an issue
If you'd like to contribute, please claim an issue by commenting, forking, and opening a pull request, even if empty. This allows the maintainers to track who is working on what issue as to not overlap work.
## Issue Guidelines
Please follow these guidelines:
Before coding:
* choose a branch name that describes the issue you're working on
While coding:
* Submit a draft PR asap
* Only change code directly relevant to your PR. Sometimes you might find some code that could really need some refactoring. However, if it's not relevant to your PR, do not touch it. File an issue instead. This allows the reviewer to focus on a single problem at a time.
# Add Signature
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/core-concepts/add-signature
Learn about Signatures on the official Solana [documentation](https://docs.solana.com/developing/programming-model/accounts#signers)
```csharp theme={null}
public class AddSignatureExample : IExample
{
private static readonly IRpcClient rpcClient = ClientFactory.GetClient(Cluster.TestNet);
private const string MnemonicWords =
"route clerk disease box emerge airport loud waste attitude film army tray " +
"forward deal onion eight catalog surface unit card window walnut wealth medal";
public void Run()
{
Wallet.Wallet wallet = new Wallet.Wallet(MnemonicWords);
Account fromAccount = wallet.GetAccount(10);
Account toAccount = wallet.GetAccount(8);
RequestResult> blockHash = rpcClient.GetRecentBlockHash();
Console.WriteLine($"BlockHash >> {blockHash.Result.Value.Blockhash}");
TransactionBuilder txBuilder = new TransactionBuilder()
.SetRecentBlockHash(blockHash.Result.Value.Blockhash)
.SetFeePayer(fromAccount)
.AddInstruction(SystemProgram.Transfer(fromAccount.PublicKey, toAccount.PublicKey, 10000000))
.AddInstruction(MemoProgram.NewMemo(fromAccount.PublicKey, "Hello from Sol.Net :)"));
byte[] msgBytes = txBuilder.CompileMessage();
byte[] signature = fromAccount.Sign(msgBytes);
byte[] tx = txBuilder.AddSignature(signature)
.Serialize();
Console.WriteLine($"Tx base64: {Convert.ToBase64String(tx)}");
RequestResult> txSim = rpcClient.SimulateTransaction(tx);
string logs = Examples.PrettyPrintTransactionSimulationLogs(txSim.Result.Value.Logs);
Console.WriteLine($"Transaction Simulation:\n\tError: {txSim.Result.Value.Error}\n\tLogs: \n" + logs);
RequestResult firstSig = rpcClient.SendTransaction(tx);
Console.WriteLine($"First Tx Signature: {firstSig.Result}");
}
}
```
# Associated Token Account
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/core-concepts/associated-token-account
Associated Token Account Program defines the convention and provides the mechanism for mapping the user's wallet address to the associated token accounts they hold.
For a comprehensive overview of Associated Token Account you can rely on the official Solana [documentation](https://spl.solana.com/associated-token-account)
```csharp theme={null}
using Solana.Unity.Programs;
using Solana.Unity.Rpc;
using Solana.Unity.Rpc.Builders;
using Solana.Unity.Rpc.Core.Http;
using Solana.Unity.Rpc.Messages;
using Solana.Unity.Rpc.Models;
using Solana.Unity.Rpc.Types;
using Solana.Unity.Wallet;
using System;
using System.Collections.Generic;
using System.Threading;
namespace Solana.Unity.Examples
{
public class AssociatedTokenAccountsExample : IExample
{
private static readonly IRpcClient RpcClient = ClientFactory.GetClient(Cluster.TestNet);
private const string MnemonicWords =
"route clerk disease box emerge airport loud waste attitude film army tray " +
"forward deal onion eight catalog surface unit card window walnut wealth medal";
public void Run()
{
Wallet.Wallet wallet = new Wallet.Wallet(MnemonicWords);
/*
* The following region creates and initializes a mint account, it also creates a token account
* that is initialized with the same mint account and then mints tokens to this newly created token account.
*/
#region Create and Initialize a token Mint Account
RequestResult> blockHash = RpcClient.GetRecentBlockHash();
ulong minBalanceForExemptionAcc =
RpcClient.GetMinimumBalanceForRentExemption(TokenProgram.TokenAccountDataSize).Result;
ulong minBalanceForExemptionMint =
RpcClient.GetMinimumBalanceForRentExemption(TokenProgram.MintAccountDataSize).Result;
Console.WriteLine($"MinBalanceForRentExemption Account >> {minBalanceForExemptionAcc}");
Console.WriteLine($"MinBalanceForRentExemption Mint Account >> {minBalanceForExemptionMint}");
Account ownerAccount = wallet.GetAccount(10);
Account mintAccount = wallet.GetAccount(1004);
Account initialAccount = wallet.GetAccount(1104);
Console.WriteLine($"OwnerAccount: {ownerAccount}");
Console.WriteLine($"MintAccount: {mintAccount}");
Console.WriteLine($"InitialAccount: {initialAccount}");
byte[] createAndInitializeMintToTx = new TransactionBuilder().
SetRecentBlockHash(blockHash.Result.Value.Blockhash).
SetFeePayer(ownerAccount).
AddInstruction(SystemProgram.CreateAccount(
ownerAccount,
mintAccount,
minBalanceForExemptionMint,
TokenProgram.MintAccountDataSize,
TokenProgram.ProgramIdKey)).
AddInstruction(TokenProgram.InitializeMint(
mintAccount.PublicKey,
2,
ownerAccount.PublicKey,
ownerAccount.PublicKey)).
AddInstruction(SystemProgram.CreateAccount(
ownerAccount,
initialAccount,
minBalanceForExemptionAcc,
TokenProgram.TokenAccountDataSize,
TokenProgram.ProgramIdKey)).
AddInstruction(TokenProgram.InitializeAccount(
initialAccount.PublicKey,
mintAccount.PublicKey,
ownerAccount.PublicKey)).
AddInstruction(TokenProgram.MintTo(
mintAccount.PublicKey,
initialAccount.PublicKey,
1_000_000,
ownerAccount)).
AddInstruction(MemoProgram.NewMemo(initialAccount, "Hello from Sol.Net")).
Build(new List { ownerAccount, mintAccount, initialAccount });
string createAndInitializeMintToTxSignature = Examples.SubmitTxSendAndLog(createAndInitializeMintToTx);
Examples.PollConfirmedTx(createAndInitializeMintToTxSignature);
#endregion
/*
* The following region creates an associated token account (ATA) for a random account and a certain token mint
* (in this case it's the previously created token mintAccount) and transfers tokens from the previously created
* token account to the newly created ATA.
*/
#region Create Associated Token Account
// this public key is from a random account created via www.sollet.io
// to test this locally I recommend creating a wallet on sollet and deriving this
PublicKey associatedTokenAccountOwner = new PublicKey("65EoWs57dkMEWbK4TJkPDM76rnbumq7r3fiZJnxggj2G");
PublicKey associatedTokenAccount =
AssociatedTokenAccountProgram.DeriveAssociatedTokenAccount(associatedTokenAccountOwner, mintAccount);
Console.WriteLine($"AssociatedTokenAccountOwner: {associatedTokenAccountOwner}");
Console.WriteLine($"AssociatedTokenAccount: {associatedTokenAccount}");
byte[] createAssociatedTokenAccountTx = new TransactionBuilder().
SetRecentBlockHash(blockHash.Result.Value.Blockhash).
SetFeePayer(ownerAccount).
AddInstruction(AssociatedTokenAccountProgram.CreateAssociatedTokenAccount(
ownerAccount,
associatedTokenAccountOwner,
mintAccount)).
AddInstruction(TokenProgram.Transfer(
initialAccount,
associatedTokenAccount,
25000,
ownerAccount)).// the ownerAccount was set as the mint authority
AddInstruction(MemoProgram.NewMemo(ownerAccount, "Hello from Sol.Net")).
Build(new List { ownerAccount });
string createAssociatedTokenAccountTxSignature = Examples.SubmitTxSendAndLog(createAssociatedTokenAccountTx);
Examples.PollConfirmedTx(createAssociatedTokenAccountTxSignature);
#endregion
}
}
}
```
# Staking
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/core-concepts/staking
SOL token holders can earn rewards and help secure the network by staking tokens to one or more validators. Rewards for staked tokens are based on the current inflation rate, total number of SOL staked on the network, and an individual validator’s uptime and commission (fee).
You can learn about Staking on the official Solana [documentation](https://spl.solana.com/stake-pool/overview#staking)
```csharp theme={null}
public class CreateAccountAndInitializeStakeExample : IExample
{
private static readonly IRpcClient rpcClient = ClientFactory.GetClient(Cluster.TestNet);
private const string MnemonicWords =
"clerk shoe noise umbrella apple gold alien swap desert rubber truck okay twenty fiscal near talent drastic present leg put balcony leader access glimpse";
public void Run()
{
var wallet = new Wallet.Wallet(new Mnemonic(MnemonicWords));
rpcClient.RequestAirdrop(wallet.Account.PublicKey, 100_000_000);
RequestResult> blockHash = rpcClient.GetRecentBlockHash();
ulong minbalanceforexception = rpcClient.GetMinimumBalanceForRentExemption(StakeProgram.StakeAccountDataSize).Result;
Account fromAccount = wallet.Account;
Account stakeAccount = wallet.GetAccount(22);
Authorized authorized = new Authorized()
{
Staker = fromAccount,
Withdrawer = fromAccount
};
Lockup lockup = new Lockup()
{
Custodian = fromAccount.PublicKey,
Epoch = 0,
UnixTimestamp = 0
};
Console.WriteLine($"BlockHash >> {blockHash.Result.Value.Blockhash}");
byte[] tx = new TransactionBuilder()
.SetRecentBlockHash(blockHash.Result.Value.Blockhash)
.SetFeePayer(fromAccount)
.AddInstruction(SystemProgram.CreateAccount(
fromAccount.PublicKey,
stakeAccount,
minbalanceforexception + 42,
StakeProgram.StakeAccountDataSize,
StakeProgram.ProgramIdKey))
.AddInstruction(StakeProgram.Initialize(
stakeAccount.PublicKey,
authorized,
lockup))
.Build(new List { fromAccount, stakeAccount });
Console.WriteLine($"Tx base64: {Convert.ToBase64String(tx)}");
RequestResult> txSim = rpcClient.SimulateTransaction(tx);
string logs = Examples.PrettyPrintTransactionSimulationLogs(txSim.Result.Value.Logs);
Console.WriteLine($"Transaction Simulation:\n\tError: {txSim.Result.Value.Error}\n\tLogs: \n" + logs);
RequestResult firstSig = rpcClient.SendTransaction(tx);
Console.WriteLine($"First Tx Result: {firstSig.Result}");
}
}
```
# Transaction Builder
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/core-concepts/transaction-builder
Learn about Transactions on the official Anchor [documentation](https://www.anchor-lang.com/docs/intro-to-solana)
```csharp theme={null}
using Solana.Unity.Programs;
using Solana.Unity.Programs.Models;
using Solana.Unity.Rpc;
using Solana.Unity.Rpc.Builders;
using Solana.Unity.Rpc.Core.Http;
using Solana.Unity.Rpc.Messages;
using Solana.Unity.Rpc.Models;
using Solana.Unity.Wallet;
using System;
using System.Collections.Generic;
namespace Solana.Unity.Examples
{
public class TransactionBuilderExample : IExample
{
private static readonly IRpcClient rpcClient = ClientFactory.GetClient(Cluster.TestNet);
private const string MnemonicWords =
"route clerk disease box emerge airport loud waste attitude film army tray " +
"forward deal onion eight catalog surface unit card window walnut wealth medal";
public void Run()
{
Wallet.Wallet wallet = new Wallet.Wallet(MnemonicWords);
Account fromAccount = wallet.GetAccount(10);
Account toAccount = wallet.GetAccount(8);
RequestResult> blockHash = rpcClient.GetRecentBlockHash();
Console.WriteLine($"BlockHash >> {blockHash.Result.Value.Blockhash}");
byte[] tx = new TransactionBuilder()
.SetRecentBlockHash(blockHash.Result.Value.Blockhash)
.SetFeePayer(fromAccount)
.AddInstruction(SystemProgram.Transfer(fromAccount.PublicKey, toAccount.PublicKey, 10000000))
.AddInstruction(MemoProgram.NewMemo(fromAccount.PublicKey, "Hello from Sol.Net :)"))
.Build(fromAccount);
Console.WriteLine($"Tx base64: {Convert.ToBase64String(tx)}");
RequestResult> txSim = rpcClient.SimulateTransaction(tx);
string logs = Examples.PrettyPrintTransactionSimulationLogs(txSim.Result.Value.Logs);
Console.WriteLine($"Transaction Simulation:\n\tError: {txSim.Result.Value.Error}\n\tLogs: \n" + logs);
RequestResult firstSig = rpcClient.SendTransaction(tx);
Console.WriteLine($"First Tx Signature: {firstSig.Result}");
}
}
}
```
# Transfer Token
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/core-concepts/transfer-token
The Token program defines a common implementation for Fungible and Non Fungible tokens.
You can learn about Token Program on the official Solana [documentation](https://spl.solana.com/token)
Balances can be transferred between Accounts using the Transfer instruction. The owner of the source Account must be present as a signer in the Transfer instruction when the source and destination accounts are different.
```csharp theme={null}
public class TransferTokenExample : IExample
{
private static readonly IRpcClient rpcClient = ClientFactory.GetClient(Cluster.TestNet);
private const string MnemonicWords =
"route clerk disease box emerge airport loud waste attitude film army tray " +
"forward deal onion eight catalog surface unit card window walnut wealth medal";
public void Run()
{
Wallet.Wallet wallet = new Wallet.Wallet(MnemonicWords);
RequestResult> blockHash = rpcClient.GetRecentBlockHash();
ulong minBalanceForExemptionAcc = rpcClient.GetMinimumBalanceForRentExemption(TokenProgram.TokenAccountDataSize).Result;
Console.WriteLine($"MinBalanceForRentExemption Account >> {minBalanceForExemptionAcc}");
Account mintAccount = wallet.GetAccount(31);
Console.WriteLine($"MintAccount: {mintAccount}");
Account ownerAccount = wallet.GetAccount(10);
Console.WriteLine($"OwnerAccount: {ownerAccount}");
Account initialAccount = wallet.GetAccount(32);
Console.WriteLine($"InitialAccount: {initialAccount}");
Account newAccount = wallet.GetAccount(33);
Console.WriteLine($"NewAccount: {newAccount}");
byte[] tx = new TransactionBuilder().SetRecentBlockHash(blockHash.Result.Value.Blockhash)
.SetFeePayer(ownerAccount)
.AddInstruction(SystemProgram.CreateAccount(
ownerAccount.PublicKey,
newAccount.PublicKey,
minBalanceForExemptionAcc,
TokenProgram.TokenAccountDataSize,
TokenProgram.ProgramIdKey))
.AddInstruction(TokenProgram.InitializeAccount(
newAccount.PublicKey,
mintAccount.PublicKey,
ownerAccount.PublicKey))
.AddInstruction(TokenProgram.Transfer(
initialAccount.PublicKey,
newAccount.PublicKey,
25000,
ownerAccount))
.AddInstruction(MemoProgram.NewMemo(initialAccount, "Hello from Sol.Net"))
.Build(new List { ownerAccount, newAccount, initialAccount });
Console.WriteLine($"Tx: {Convert.ToBase64String(tx)}");
RequestResult> txSim = rpcClient.SimulateTransaction(tx);
string logs = Examples.PrettyPrintTransactionSimulationLogs(txSim.Result.Value.Logs);
Console.WriteLine($"Transaction Simulation:\n\tError: {txSim.Result.Value.Error}\n\tLogs: \n" + logs);
RequestResult txReq = rpcClient.SendTransaction(tx);
Console.WriteLine($"Tx Signature: {txReq.Result}");
}
}
```
# Configurations
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/getting-started/configuration
Learn how to configure your preferred wallet
Learn how to configure your preferred wallet
The SDK supports a variety of wallets, including
| Wallet | Support | Type |
| ------------------------ | ------- | -------- |
| In-game (new or restore) | ✅ | In-app |
| In-game (Web3auth) | ✅ | In-app |
| Wallet Adapter | ✅ | External |
| Mobile Wallet Adapter | ✅ | External |
| WalletConnect | ✅ | External |
| Seed Vault | 🏗 | In-app |
## Interface
`IWalletBase` defines the common [interface](https://github.com/garbles-labs/Solana.Unity-SDK/blob/main/Runtime/codebase/IWalletBase.cs)
The WalletBase abstract class implements `IWalletBase` interface and provides convenient methods shared by all wallet adapters.
A few examples are:
* Connection to Mainnet/Devnet/Testnet or custom RPC
* Login/logout
* Account creation
* Get balance
* Get token accounts
* Sign/partially sign transactions
* Send transactions
## Login example
You can attach the [Web3.cs](https://github.com/magicblock-labs/Solana.Unity-SDK/blob/main/Runtime/codebase/Web3.cs) script
(../Runtime/Codebase/Web3.cs) to any game object on the scene, then call Web3.Instance.LoginWalletAdapter();
The complete list of methods is available
[here](https://github.com/garbles-labs/Solana.Unity-SDK/blob/main/Runtime/codebase/WalletBase.cs)
## Wallet Adapter
To configure a wallet following the [Wallet Adapter](https://solana-mobile.github.io/mobile-wallet-adapter/spec/spec.html) standard use the [SolanaWalletAdapterWebGL](https://github.com/magicblock-labs/Solana.Unity-SDK/blob/main/Runtime/codebase/SolanaWalletAdapterWebGL.cs) wallet implementation.
```csharp theme={null}
WalletBase wallet = new SolanaWalletAdapterWebGL(walletAdapterOptions, RpcCluster.DevNet, ...);
```
## WalletConnect
[WalletConnect](https://walletconnect.network/) enables your game to connect with a wide range of Solana wallets — including Binance Wallet, Jupiter Mobile, Trust, OKX Wallet, and many more — through a single integration.
On mobile, users can connect any supported wallet installed on their device. On desktop, they pair with their wallet by scanning a QR code. See the [full list of supported Solana wallets](https://walletguide.walletconnect.network/?chains=solana%3A5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp).
The integration is powered by [Reown AppKit for Unity](https://docs.reown.com/appkit/unity/core/usage#appkit-with-solana-unity-sdk), which provides an adapter package that plugs directly into the Solana Unity SDK. Once set up, all signing and session management go through AppKit while you continue using the same `Web3` APIs from `Solana.Unity.SDK`.
For installation and setup instructions, see the [Reown AppKit documentation](https://docs.reown.com/appkit/unity/core/usage#appkit-with-solana-unity-sdk).
## SMS
Solana Mobile Stack is a set of libraries for wallets and apps, allowing developers to create rich mobile experiences for the Solana network.
For more information about SMS check out the official [documentation](https://solanamobile.com/developers).
## Mobile Wallet Adapter
To configure a wallet following the Mobile Wallet Adapter standard use the [SolanaMobileWalletAdapter](https://github.com/magicblock-labs/Solana.Unity-SDK/blob/main/Runtime/codebase/SolanaMobileWalletAdapter.cs) wallet implementation.
```csharp theme={null}
WalletBase wallet = new SolanaMobileWalletAdapter(solanaMobileWalletOptions, RpcCluster.DevNet, ...);
```
## Configuring Deeplinks
Some of the wallet, e.g. Phantom, are currently implemented using DeepLinks. Deep links are URLs that link to a specific piece of content or functionality within an app, in the context of Solana transactions, deep links can be used to sign a transaction by allowing users to approve a transaction using their Solana wallet.
### Enabling deep linking for Android applications
To enable deep linking for Android applications, use an [intent filter](https://developer.android.com/guide/components/intents-filters). An intent filter overrides the standard Android App [Manifest](https://docs.unity3d.com/Manual/android-manifest.html) to include a specific intent filter section for [Activity](https://developer.android.com/reference/android/app/Activity).
To set up the wallet intent filter:
1. In the Project window, go to Assets > Plugins > Android.
2. Create a new file and call it AndroidManifest.xml. Unity automatically processes this file when you build your application.
3. Copy the [code sample](https://github.com/magicblock-labs/Solana.Unity-SDK/blob/main/Samples~/Solana%20Wallet/Plugins/Android/AndroidManifest.xml) into the new file and save it.
*android:scheme="unitydl" should match the value defined in the wallet configuration*
See the detailed explanation on the Unity [documentation page](https://docs.unity3d.com/Manual/deep-linking-android.html).
### Enabling deep linking for IOS applications
See the detailed explanation on the Unity [documentation page](https://docs.unity3d.com/Manual/deep-linking-android.html) .
*the defined schema should match the value defined in the wallet configuration*
# Installation
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/getting-started/installation
## Unity
Go [here](https://unity.com/download) to install Unity.
## Import the SDK package
* Open [Unity Package Manager](https://docs.unity3d.com/Manual/upm-ui.html) window.
* Click the add + button in the status bar.
* The options for adding packages appear.
* Select Add package from git URL from the add menu. A text box and an Add button appear.
*
* Enter the `https://github.com/magicblock-labs/Solana.Unity-SDK.git` Git URL in the text box and click Add.
* Once the package is installed, in the Package Manager inspector you will have Samples. Click on Import
* You may also install a specific package version by using the URL with the specified version.
* `https://github.com/magicblock-labs/Solana.Unity-SDK.git#X.Y.X`
* Please note that the version X.Y.Z stated here is to be replaced with the version you would like to get.
* You can find all the available releases [here](https://github.com/magicblock-labs/Solana.Unity-SDK/releases).
* The latest available release version is- [](https://github.com/magicblock-labs/Solana.Unity-SDK/releases/latest)
* Import the Sample Scene
*
* You will find a sample scene with a configured wallet in `Samples/Solana SDK/0.0.2/Simple Wallet/Solana Wallet/scenes/wallet_scene.unity`
This tutorial is made in Unity 2021.3.5f1
# Sample scene
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/getting-started/sample-scene
Play around with the sample scene
## Features
* Prefabs are templates that store Objects configuration. They can be used to create instances of the same assets and are easily reusable
* Scenes folder contains the full demo deployed here.
* Scrips folder contains different scripts examples (I.e. Assets/Samples/Solana SDK/0.1.1/Sample Wallet/Solana Wallet/Scripts/example/screens/SwapScreen.cs for Orca Swaps)
* Animations, Materials, Plugins and Textures are self-explanatory
Play around with the demo scene to familiarize yourself with the basic functionality of Wallets, NFT loading, transfers, swaps etc.
# Additional Examples
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/guides/additional-examples
More examples
[here](https://github.com/garbles-labs/Solana.Unity-Core/tree/master/src/Solana.Unity.Examples)
# DEX integration with Orca
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/guides/dex-integration
Orca is natively supported in the SDK. Utility methods are provided to easily build the transactions needed to make swaps, open positions, manage cash and interact with Whirlpools.
## Orca
Orca is the easiest place to trade cryptocurrency on the Solana blockchain. For a detailed description refer to the official [Orca documentation](https://docs.orca.so/orca-for-traders/master) and [Orca Developer Portal](https://orca-so.gitbook.io/orca-developer-portal/orca/welcome).
## Perform a Swap
* Create an IDex instance, providing a default account and the RPC client instance:
```csharp theme={null}
IDex dex = new OrcaDex(
WalletH.Account,
WalletH.Rpc
)
```
* Fetch token data:
```csharp theme={null}
TokenData tokenA = await dex.GetTokenBySymbol("USDC");
TokenData tokenB = await dex.GetTokenBySymbol("ORCA");
```
* Find the whirlpool:
```csharp theme={null}
Pool whirlpool = await dex.FindWhirlpoolAddress(tokenA.MintAddress, tokenB.MintAddress)
```
* Get a swap quote for 1 USDC:
```csharp theme={null}
SwapQuote swapQuote = await dex.GetSwapQuoteFromWhirlpool(
whirlpool.Address,
DecimalUtil.ToUlong(1, tokenA.Decimals),
tokenA.MintAddress,
slippageTolerance: 0.1,
);
```
```csharp theme={null}
var quote = DecimalUtil.FromBigInteger(swapQuote.EstimatedAmountOut, tokenB.Decimals);
Debug.Log(quote); // Amount of espected Orca token to receive
```
* Create the swap transaction:
```csharp theme={null}
Transaction tx = await dex.SwapWithQuote(
whirlpool,
swapQuote
);
```
* Sign and send the swap transaction:
```csharp theme={null}
await WalletH.Base.SignAndSendTransaction(tx);
```
## Open a position and increase the liquidity of the ORCA/USDC whirlpool
An example of adding 5 ORCA and 5 USDC to the liquidity of the pool, minting a metaplex NFT representing the position
```csharp theme={null}
OrcaDex dex = new OrcaDex(
WalletH.Account,
WalletH.Rpc
);
var orcaToken = await dex.GetTokenBySymbol("ORCA");
var usdcToken = await dex.GetTokenBySymbol("USDC");
var whirlpool = await dex.FindWhirlpoolAddress(
usdcToken.MintAddress,
orcaToken.MintAddress
);
Account mint = new Account();
Transaction tx = await dex.OpenPositionWithLiquidity(
whirlpool,
mint,
-1792,
1152,
DecimalUtil.ToUlong(5, tokenA.Decimals),
DecimalUtil.ToUlong(5, tokenB.Decimals),
commitment: Commitment.Confirmed
);
var txSer = tx.Build(new List() {
WalletH.Account,
mint
});
await WalletH.Base.SignAndSendTransaction(tx);
```
# Host Your Game on Github Pages
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/guides/host-your-game
Host your game for free using Github pages
Solana.Unity SDK is fully compatible with WebGL. In this tutorial you will compile the Solana.Unity-SDK [demo scene](https://garbles-labs.github.io/Solana.Unity-SDK/) and publish it using [Github pages](https://pages.github.com/).
With GitHub pages, GitHub allows you to host a webpage from your repository.
## Compile the game to WebGL
1. Download and Install [Unity](https://unity3d.com/get-unity/download)
2. Install the Solana.Unity-SDK following the [instructions](https://github.com/garbles-labs/Solana.Unity-SDK#installation) and import the example
3. Compile the scene to WebGL (be sure to [disable compression](https://www.youtube.com/watch?v=2jjESP58jsA), as GH pages does not support serving compressed files)
If you want to skip the compilation step, you can fork the SDK repository,
which contained a pre-compiled WebGL build in the
[gp-pages](https://github.com/garbles-labs/Solana.Unity-SDK/tree/gh-pages)
branch
## Host the demo on Github pages
* Create a new repository
* Navigate to the build folder containing the index.html
```shell theme={null}
git init
git add .
git commit -m "WebGL game"
git remote add origin
git push origin
```
* You repository should now looks similar to the SDK [gp-pages](https://github.com/garbles-labs/Solana.Unity-SDK/tree/gh-pages) branch.
* Enable gh-pages deployment from the repository settings
Github will provide a url for the live deployment: [garbles-labs.github.io/Solana.Unity-SDK](https://garbles-labs.github.io/Solana.Unity-SDK/)
Learn how to setup a [custom
domain](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site)
on Github pages
Follow [this guide](/pages/tools/solana-unity-sdk/guides/publishing-a-game) to publish your game as an xNFT
in less than 2 minutes.
Happy game development 🎈 and don't forget to ⭐ the repo
# Jupiter V6 integration
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/guides/jupiter
Integrate Jupiter v6 API in your game
## Jupiter
Jupiter V6 is natively supported in the SDK. Utility methods are provided to easily get swap quotes, build and send the transactions needed to perform swaps.
Jupiter is the key liquidity aggregator for Solana, offering the widest range of tokens and best route discovery between any token pair. For a detailed description refer to the official [Jupiter documentation](https://station.jup.ag/).
***
## Perform a Swap
Create an IDex instance, providing a default account:
```csharp theme={null}
IDexAggregator dex = new JupiterDexAg(Web3.Account);
```
Fetch token data:
```csharp theme={null}
TokenData tokenA = await dex.GetTokenBySymbol("SOL");
TokenData tokenB = await dex.GetTokenBySymbol("USDC");
```
Get a swap quote for 1 SOL:
```csharp theme={null}
SwapQuoteAg swapQuote = await dex.GetSwapQuote(
tokenA.MintAddress,
tokenB.MintAddress,
DecimalUtil.ToUlong(1, tokenA.Decimals)
);
```
```csharp theme={null}
var quote = DecimalUtil.FromBigInteger(swapQuote.OutputAmount, tokenB.Decimals);
Debug.Log(quote); // Amount of espected USDC token to receive
```
Display the route path:
```csharp theme={null}
Debug.Log(string.Join(" -> ", swapQuote.RoutePlan.Select(p => p.SwapInfo.Label)));
// Lifinity V2 -> Whirlpool
```
Create the swap transaction:
```csharp theme={null}
Transaction tx = await dex.Swap(swapQuote);
```
Sign and send the swap transaction:
```csharp theme={null}
await Web3.Wallet.SignAndSendTransaction(tx);
```
## Use the Jupiter Payments API
The Jupiter Payments API is also available, enabling you to utilize Jupiter + SolanaPay for facilitating user payments with any SPL token, allowing pricing in USDC or other tokens.
Create an IDex instance, providing a default account:
```csharp theme={null}
IDexAggregator dex = new JupiterDexAg(Web3.Account);
```
Fetch token data:
```csharp theme={null}
TokenData tokenA = await dex.GetTokenBySymbol("SOL");
TokenData tokenB = await dex.GetTokenBySymbol("USDC");
```
Get a swap quote for the amount of SOL needed for obtaining 5 UDSC:
```csharp theme={null}
SwapQuoteAg swapQuote = await dex.GetSwapQuote(
tokenA.MintAddress,
tokenB.MintAddress,
DecimalUtil.ToUlong(5, tokenB.Decimals),
SwapMode.ExactOut
);
```
```csharp theme={null}
var quote = DecimalUtil.FromBigInteger(swapQuote.InputAmount, tokenA.Decimals);
Debug.Log(quote); // Amount of espected SOL token to pay
```
Create the swap transaction:
```csharp theme={null}
Transaction tx = await dex.Swap(swapQuote);
```
Sign and send the swap transaction:
```csharp theme={null}
await Web3.Wallet.SignAndSendTransaction(tx);
```
# Minting an NFT
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/guides/mint-an-nft
For minting an NFT we will interact with the [Token Metadata](https://docs.metaplex.com/programs/token-metadata/) program, see the metaplex [documentation](https://docs.metaplex.com/) for a comprehensive overview.
Firstly, we need to create a new mint account for the NFT we want to mint and an associated token account for owning it.
```csharp theme={null}
var mint = new Account();
var associatedTokenAccount = AssociatedTokenAccountProgram
.DeriveAssociatedTokenAccount(Web3.Account, mint.PublicKey);
```
Secondly, let's define the metadata of the NFT.
```csharp theme={null}
var metadata = new Metadata()
{
name = "Test",
symbol = "TST",
uri = "https://y5fi7acw5f5r4gu6ixcsnxs6bhceujz4ijihcebjly3zv3lcoqkq.arweave.net/x0qPgFbpex4ankXFJt5eCcRKJzxCUHEQKV43mu1idBU",
sellerFeeBasisPoints = 0,
creators = new List { new(Web3.Account.PublicKey, 100, true)}
};
```
We can now construct the transaction, which consists of 5 instructions:
* Creating the Mint Account
* Initializing the Mint Account
* Creating the AssociatedTokenAccount
* Minting the NFT
* Creating the Metadata Account
* Creating the Master Edition
```csharp theme={null}
var transaction = new TransactionBuilder()
.SetRecentBlockHash(blockHash)
.SetFeePayer(Web3.Account)
.AddInstruction(
SystemProgram.CreateAccount(
Web3.Account,
mint.PublicKey,
minimumRent.Result,
TokenProgram.MintAccountDataSize,
TokenProgram.ProgramIdKey))
.AddInstruction(
TokenProgram.InitializeMint(
mint.PublicKey,
0,
Web3.Account,
Web3.Account))
.AddInstruction(
AssociatedTokenAccountProgram.CreateAssociatedTokenAccount(
Web3.Account,
Web3.Account,
mint.PublicKey))
.AddInstruction(
TokenProgram.MintTo(
mint.PublicKey,
associatedTokenAccount,
1,
Web3.Account))
.AddInstruction(MetadataProgram.CreateMetadataAccount(
PDALookup.FindMetadataPDA(mint),
mint.PublicKey,
Web3.Account,
Web3.Account,
Web3.Account.PublicKey,
metadata,
TokenStandard.NonFungible,
true,
true,
null,
metadataVersion: MetadataVersion.V3))
.AddInstruction(MetadataProgram.CreateMasterEdition(
maxSupply: null,
masterEditionKey: PDALookup.FindMasterEditionPDA(mint),
mintKey: mint,
updateAuthorityKey: Web3.Account,
mintAuthority: Web3.Account,
payer: Web3.Account,
metadataKey: PDALookup.FindMetadataPDA(mint),
version: CreateMasterEditionVersion.V3
)
);
```
Finally, let's sign and send the transaction:
```csharp theme={null}
var tx = Transaction.Deserialize(transaction.Build(new List {Web3.Account, mint}));
var res = await Web3.Wallet.SignAndSendTransaction(tx);
Debug.Log(res.Result);
```
The console will print the transaction signature, which you can investigate in the inspector and should looks similar to this [transaction](https://explorer.solana.com/tx/TPSviDzpzTFEyfJkYwmQzqaPJTTsGMZTuPuG9q1LiKrhZnwg5WWHH7ARR8eYAdoB8rt8qcjKwqbcZj43b84Ls5C?cluster=devnet),
You can lookup the mint address in the explorer, which should be similar to this [NFT](https://explorer.solana.com/address/4X199VtLKVJUeLMXzwXzSsFgapVQcrYx9vnqxNDkH2Xa?cluster=devnet)
# Publish Your Game as an Xnft
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/guides/publishing-a-game
## Compile the game for xNFT
You can now just compile your game to WebGL without any extra steps. Just make sure you have the latest version of the SDK and you're good to go.
Deploy your game as a normal WebGL game as you will need the url for publishing the xNFT in next step.
Your WebGL game will work both in the browser and inside Backpack, no need to
build and host a separate version for each platform.
Follow [this guide](/pages/tools/solana-unity-sdk/guides/host-your-game) to compile your game to WebGL and
host the build on Github pages
### (Optional) Use the xNFT WebGL Template
A customized WebGL Template is also provided for easier deployment of your game as an xNFT. It's not required, but it's recommended as it makes the game fully responsive to the screen size, and it will just look good when running in both the browser and as a xNFT app.
To use the xNFT WebGL Template, follow these steps:
* Open [Build Settings](https://docs.unity3d.com/Manual/BuildSettings.html) window and change to WebGL platform if you haven't already:
* Select WebGL as the platform
* Click the **Switch Platform** button to apply changes.
When your Unity project is set to WebGL Platform, the SDK automagically imports a new WebGL Template into the /Assets/WebGLTemplate folder, named xNFT:
* Open **Player Settings** window from the Build Settings, and select the xNFT template.
Now you can build your game and host it as a normal WebGL game.
## Publish your game as an xNFT
If you haven't already, go get your Backpack user. You can download Backpack [here](https://www.backpack.app/downloads).
After you have your game hosted in a webserver and you have Backpack account, you can just go to [https://www.xnft.gg/publish](https://www.xnft.gg/publish) to deploy your xnft in mainnet-beta or [https://devnet.xnft.gg/publish](https://devnet.xnft.gg/publish) to deploy the xnft in devnet.
Connect your Backpack wallet and then follow the instructions:
* copy this basic [xnft.json](https://github.com/coral-xyz/xnft-quickstart/blob/master/xnft.json) configuration file.
* edit the "entrypoints" "default" "web" to point to your game's url
* edit xnft.json (add a "tag": "game" line) and add your app's icon and screenshots in an Assets folder.
* Zip togehter the xnft.json file and the Assets folder.
* Go to [xnft.gg/publish](https://www.xnft.gg/publish)
* Drop the zipped Manifest and mint.
Congrats! You're done publishing your game as an xNFT!
## Video Demo
Here's a quick demo on how you can build your game and test it in the browser and as an xNFT app inside Backpack.
# Introduction
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/overview
Open-Source Unity-Solana SDK with NFT support & Full RPC coverage.
Step-by-step guides to setting up your system and installing the
Solana.Unity-SDK.
Learn how to set up your game wallets.
Solana.Unity-SDK core concepts.
Guides to help you get started.
## Solana.Unity-SDK
Solana.Unity SDK is comprehensive set of open-source tools to easily access Solana in your Unity-based games. You can install the SDK with the Unity Package Manager or on the Unity Asset Store. and set up your preferred wallet among the available options. Solana.Unity-SDK uses [Solana.Unity-Core](https://github.com/garbles-labs/Solana.Unity-Core) implementation, native .NET Standard 2.0 (Unity compatible) with full RPC API coverage, MPL, native DEXes operations and more.
## Features
* Full JSON RPC API coverage
* Wallet and accounts: Set up of a non-custodial Solana wallet in Unity (sollet and solana-keygen compatible)
* Phantom and Web3auth support (non-custodial signup/login through social accounts)
* Transaction decoding from base64 and wire format and encoding back into wire format
* Message decoding from base64 and wire format and encoding back into wire format
* Instruction decompilation
* TokenWallet object to send and receive SPL tokens and JIT provisioning of Associated Token Accounts
* Basic UI examples
* NFTs
* Compile games to xNFTs (Backpack)
* Native DEX operations (Orca, Jupiter coming soon...)
* Websockets to register/trigger custom events (account change, signature status, programs, ...)
* Solana Mobile Stack support
* Solana Wallet Adapter
# Overview
Source: https://docs.magicblock.gg/pages/tools/wallets-and-onramp/overview
Accelerate user onboarding with existing wallet and onramp solutions
***
## Quick Access
Find a wallet that works for you
Move funds at the speed of light in 100+ countries
Solana's Official Developer Resource
***
## Wallet Comparison
When interacting with Solana, your wallet choice affects user **security**, **usability**, and **onboarding experience**. Here’s a breakdown of different wallet types:
| **Wallet Type** | **Description** | **Examples** | **Recommended** |
| -------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| **Hot Wallet** | Internet-connected, fast, and convenient for frequent use. | [Phantom](https://phantom.app), [Backpack](https://backpack.app), [Solflare](https://solflare.com) | Everyday users, DeFi, NFTs, dApps |
| **Cold Wallet (Hardware)** | Offline storage with top-tier security. Private keys never touch the internet. | [Ledger](https://www.ledger.com), [Trezor](https://trezor.io/) | Long-term holders, large portfolio protection |
| **Custodial Wallet** | Keys managed by an exchange or third party for simplicity and recovery support. | [Coinbase Exchange](https://www.coinbase.com), [Binance](https://www.binance.com) | Beginners, users who prefer convenience over full control |
| **Smart Contract Wallet** | On-chain programmable wallets with features like multi-sig and recovery. | [Squads](https://squads.so), | Teams, DAOs, advanced DeFi setups |
| **Embedded Wallet** | Abstracts keys for smooth Web2-like onboarding (via email, social, etc.). | [Privy](https://www.privy.io), [Web3Auth](https://web3auth.io) | Apps that onboard non-crypto users |
***
## Privy
You can easily empower non-crypto users with social login and onramp through Privy's embedded wallet.
Web3 onboarding for non-crypto users
***
# Best Practices
Source: https://docs.magicblock.gg/pages/verifiable-randomness-functions-vrfs/how-to-guide/best-practices
Guidelines for integrating verifiable randomness
Use these tips to get the most reliable results when requesting randomness.
Ensure your rollup delegates to a trusted oracle queue with the `DelegateOracleQueue` instruction so that requests are fulfilled promptly.
### Seed Selection
* Combine user input with server timestamps or game state to create unpredictable seeds.
* Avoid letting players specify the entire `caller_seed` to prevent grinding.
### Callback Validation
* Always check the `vrf_program_identity` to confirm the callback is from MagicBlock's official signer.
* Reject callbacks that do not match the original request parameters.
### State Management
* Store any pending request identifiers so that retries or unexpected callbacks can be handled safely.
* Track the oracle queue your requests were delegated to so results can be matched back correctly.
* Keep random outputs ephemeral—consume them immediately within your [rollup logic](/pages/ephemeral-rollups-ers/introduction/ephemeral-rollup) and avoid reusing them.
Following these guidelines reduces the risk of manipulation and ensures consistent randomness across sessions.
# Quickstart
Source: https://docs.magicblock.gg/pages/verifiable-randomness-functions-vrfs/how-to-guide/quickstart
Learn how to request and consume Solana VRF randomness onchain using the MagicBlock VRF SDK.
***
**Building with an AI coding agent?** Install the MagicBlock Dev Skill to give your agent MagicBlock-specific patterns — delegation flows, Magic Actions, cranks, VRF, and more.
**Hit an error?** Ask your coding agent with the skill installed, not the docs assistant. The assistant only sees the docs, so it cannot debug your code.
Quick install for Claude Code:
```bash theme={null}
npx skills add https://github.com/magicblock-labs/magicblock-dev-skill
```
Using Cursor, Codex, Windsurf, Cline, or another agent? See the [AI Dev Skill](/pages/overview/additional-information/ai-dev-skill) page for all install targets.
### Quick Access
Check out basic VRF examples:
Repo for roll dice example
Roll a dice onchain
Roll a dice within 100 ms onchain
***
Need the product overview first? Start with the Solana VRF landing page, then follow this quickstart.
## Step-By-Step Guide
Any Solana program can request and consume verifiable randomness onchain within seconds using the MagicBlock VRF SDK. By the end of this guide, you'll have a working example that rolls a dice using verifiable randomness.
Write your program}>
Write your Solana program as you normally.
Add request and consume randomness instructions.
}
>
Add CPI hooks that request and consume randomness via callback from a
verified oracle.
Deploy your program on Solana}>
Deploy your Solana program using Anchor CLI.
Execute transactions for onchain randomness.}>
Send transactions to generate and consume randomness onchain.
***
## Roll Dice Example
The following software packages may be required, other versions may also be compatible:
| Software | Version | Installation Guide |
| ---------- | ------- | --------------------------------------------------------------- |
| **Solana** | 3.1.9 | [Install Solana](https://docs.anza.xyz/cli/install) |
| **Rust** | 1.89.0 | [Install Rust](https://www.rust-lang.org/tools/install) |
| **Anchor** | 1.0.2 | [Install Anchor](https://www.anchor-lang.com/docs/installation) |
| **Node** | 24.10.0 | [Install Node](https://nodejs.org/en/download/current) |
### Code Snippets
A simple roll dice program where player initialize state account to store, request and consume randomness:
```rust theme={null}
pub const PLAYER: &[u8] = b"playerd";
#[program]
pub mod random_dice {
use super::*;
pub fn initialize(ctx: Context) -> Result<()> {
msg!(
"Initializing player account: {:?}",
ctx.accounts.player.key()
);
Ok(())
}
// ... Additional instructions will be added here
}
/// Context for initializing player
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(init_if_needed, payer = payer, space = 8 + 1, seeds = [PLAYER, payer.key().to_bytes().as_slice()], bump)]
pub player: Account<'info, Player>,
pub system_program: Program<'info, System>,
}
/// Player struct
#[account]
pub struct Player {
pub last_result: u8,
}
```
[⬆️ Back to Top](#code-snippets)
1. Add `ephemeral-rollups-sdk` with the `anchor` and `vrf` features to your program
```bash theme={null}
cargo add ephemeral-rollups-sdk --features anchor,vrf
```
Import the `vrf` and `vrf_callback` macros, `create_request_scoped_randomness_ix`, `RequestRandomnessParams`, and `SerializableAccountMeta`:
```rust theme={null}
use ephemeral_rollups_sdk::anchor::{vrf, vrf_callback};
use ephemeral_rollups_sdk::vrf::instructions::{create_request_scoped_randomness_ix, RequestRandomnessParams};
use ephemeral_rollups_sdk::vrf::types::SerializableAccountMeta;
```
2. Add instructions `roll_dice` to request randomness and `callback_roll_dice` to consume randomness, along with its context:
```rust theme={null}
use ephemeral_rollups_sdk::{
anchor::{vrf, vrf_callback},
vrf::{
self,
instructions::{create_request_scoped_randomness_ix, RequestRandomnessParams},
types::SerializableAccountMeta,
},
};
#[program]
pub mod random_dice {
use super::*;
// ... `initialize` instruction
// Request Randomness
pub fn roll_dice(ctx: Context, client_seed: u8) -> Result<()> {
msg!("Requesting randomness...");
let ix = create_request_scoped_randomness_ix(RequestRandomnessParams {
payer: ctx.accounts.payer.key(),
oracle_queue: ctx.accounts.oracle_queue.key(),
callback_program_id: ID,
callback_discriminator: instruction::CallbackRollDice::DISCRIMINATOR.to_vec(),
caller_seed: [client_seed; 32],
// Specify any account that is required by the callback
accounts_metas: Some(vec![SerializableAccountMeta {
pubkey: ctx.accounts.player.key(),
is_signer: false,
is_writable: true,
}]),
callback_args: Some(vec![client_seed]),
..Default::default()
});
ctx.accounts
.invoke_signed_vrf(&ctx.accounts.payer.to_account_info(), &ix)?;
Ok(())
}
// Consume Randomness
pub fn callback_roll_dice(
ctx: Context,
randomness: [u8; 32],
client_seed: u8,
) -> Result<()> {
msg!("client_seed={}", client_seed);
let rnd_u8 = vrf::rnd::random_u8_with_range(&randomness, 1, 6);
msg!("Consuming random number: {:?}", rnd_u8);
let player = &mut ctx.accounts.player;
player.last_result = rnd_u8; // Update the player's last result
Ok(())
}
}
#[vrf]
#[derive(Accounts)]
pub struct DoRollDiceCtx<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(seeds = [PLAYER, payer.key().to_bytes().as_slice()], bump)]
pub player: Account<'info, Player>,
/// CHECK: The oracle queue
#[account(
mut,
constraint =
oracle_queue.key() == vrf::consts::DEFAULT_QUEUE || // Devnet
oracle_queue.key() == vrf::consts::DEFAULT_TEST_QUEUE || // Local
oracle_queue.key() == vrf::consts::DEFAULT_EPHEMERAL_QUEUE || // ER Devnet
oracle_queue.key() == vrf::consts::DEFAULT_EPHEMERAL_TEST_QUEUE // ER Local
)]
pub oracle_queue: UncheckedAccount<'info>,
}
// `#[vrf_callback]` enforces that only the VRF program (via CPI) can invoke the
// callback — omitting it leaves the callback spoofable by any caller.
#[vrf_callback]
#[derive(Accounts)]
pub struct CallbackRollDiceCtx<'info> {
#[account(mut)]
pub player: Account<'info, Player>,
}
// ... Other context and account struct.
```
**VRF SDK constants** (`ephemeral_rollups_sdk::vrf::consts`) — reference these instead of hardcoding addresses, both in your program and in client/test code:
| Constant | Purpose | Address |
| ------------------------------ | --------------------------------- | ---------------------------------------------- |
| `VRF_PROGRAM_ID` | VRF program | `Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz` |
| `VRF_PROGRAM_IDENTITY` | Callback signer PDA | `9irBy75QS2BN81FUgXuHcjqceJJRuc9oDkAe8TKVvvAw` |
| `DEFAULT_QUEUE` | Base-layer queue (mainnet/devnet) | `Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh` |
| `DEFAULT_EPHEMERAL_QUEUE` | ER queue (mainnet/devnet) | `5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc` |
| `DEFAULT_TEST_QUEUE` | Base-layer queue (localnet) | `GKE6d7iv8kCBrsxr78W3xVdjGLLLJnxsGiuzrsZCGEvb` |
| `DEFAULT_EPHEMERAL_TEST_QUEUE` | ER queue (localnet) | `Sc9MJUngNbQXSXGP3F67KvKwVnhaYn6kcioxXNVowYT` |
Pass the queue that matches where your transaction runs as `oracle_queue`. Mainnet and Devnet share the same queue addresses; localnet uses the test queues.
> `Request Randomness` is the process of generating a random `hashId` with the relevant callback instruction for the verified oracles to be triggered.
> `Consume Randomness` is the process of using the verifiable randomness by your program which is provided and triggered through verified oracle.
[⬆️ Back to Top](#code-snippets)
Now you’re program is upgraded and ready! Build and deploy to the desired
cluster:
```bash theme={null}
anchor build && anchor deploy
```
[⬆️ Back to Top](#code-snippets)
Ready to execute transactions for onchain randomness!
```bash theme={null}
anchor test --skip-build --skip-deploy --skip-local-validator
```
**VRF SDK constants** (`ephemeral_rollups_sdk::vrf::consts`) — reference these instead of hardcoding addresses, in both your program and client/test code:
| Constant | Purpose | Address |
| ------------------------------ | --------------------------------- | ---------------------------------------------- |
| `VRF_PROGRAM_ID` | VRF program | `Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz` |
| `VRF_PROGRAM_IDENTITY` | Callback signer PDA | `9irBy75QS2BN81FUgXuHcjqceJJRuc9oDkAe8TKVvvAw` |
| `DEFAULT_QUEUE` | Base-layer queue (mainnet/devnet) | `Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh` |
| `DEFAULT_EPHEMERAL_QUEUE` | ER queue (mainnet/devnet) | `5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc` |
| `DEFAULT_TEST_QUEUE` | Base-layer queue (localnet) | `GKE6d7iv8kCBrsxr78W3xVdjGLLLJnxsGiuzrsZCGEvb` |
| `DEFAULT_EPHEMERAL_TEST_QUEUE` | ER queue (localnet) | `Sc9MJUngNbQXSXGP3F67KvKwVnhaYn6kcioxXNVowYT` |
Pass the queue that matches where your transaction runs as `oracle_queue`. Mainnet and Devnet share the same queue addresses; localnet uses the test queues.
Run the following test:
```typescript theme={null}
import * as anchor from "@coral-xyz/anchor";
import { Program, web3 } from "@coral-xyz/anchor";
import { RandomDice } from "../target/types/random_dice";
import { PublicKey } from "@solana/web3.js";
// Devnet base-layer VRF queue (override with VRF_BASE_QUEUE for local runs)
const DEFAULT_BASE_QUEUE = new PublicKey(
process.env.VRF_BASE_QUEUE || "Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh",
);
describe("roll-dice", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const provider = anchor.getProvider() as anchor.AnchorProvider;
const program = anchor.workspace.RandomDice as Program;
const playerPda = web3.PublicKey.findProgramAddressSync(
[Buffer.from("playerd"), provider.publicKey!.toBytes()],
program.programId,
)[0];
it("Initialized player!", async () => {
const tx = await program.methods
.initialize()
.rpc({ skipPreflight: true, commitment: "confirmed" });
console.log("Your transaction signature", tx);
});
it("Do Roll Dice!", async function () {
// The base-chain callback can take up to 10s, so raise Mocha's timeout.
this.timeout(20_000);
// Generate the seed BEFORE subscribing so the handler closes over it.
// The program logs "client_seed=N" inside callback_roll_dice — we match
// on that exact substring to pin the callback to our specific request.
const clientSeed = Math.floor(Math.random() * 256);
const seedTag = `client_seed=${clientSeed}`;
// Pre-arm a one-shot promise that the onLogs handler resolves with the
// matching signature. No polling — we just await it, racing a timeout.
let resolveSig!: (sig: string) => void;
const sigPromise = new Promise((r) => {
resolveSig = r;
});
const callbackSubId = provider.connection.onLogs(
program.programId,
(info) => {
if (
!info.err &&
info.logs.some((l) => l.includes("CallbackRollDice")) &&
info.logs.some((l) => l.includes(seedTag))
) {
resolveSig(info.signature);
}
},
"confirmed",
);
try {
const tx = await program.methods
.rollDice(clientSeed)
.accounts({ oracleQueue: DEFAULT_BASE_QUEUE })
.rpc({ skipPreflight: true, commitment: "confirmed" });
console.log("rollDice tx:", tx);
// Base-chain VRF response is slower than ER (~1-5s typical) so 10s timeout.
const sig = await Promise.race([
sigPromise,
new Promise((r) => setTimeout(() => r(null), 10_000)),
]);
if (!sig) throw new Error("callbackRollDice not observed within 10s.");
console.log("callbackRollDice tx:", sig);
const player = await program.account.player.fetch(playerPda, "processed");
console.log("player:", player);
} finally {
await provider.connection.removeOnLogsListener(callbackSubId);
}
});
});
```
[⬆️ Back to Top](#code-snippets)
***
Want to run VRF end to end on your machine? Use the Local Development guide for the fully local stack, the Surfpool alternative, and the local vrf-oracle flow.
***
## Solana Explorer
Get insights about your transactions and accounts on Solana:
Official Solana Explorer
Explore Solana Blockchain
## Solana RPC Providers
Send transactions and requests through existing RPC providers:
Free Public Nodes
Free Shared Nodes
Dedicated High-Performance Nodes
## Solana Validator Dashboard
Find real-time updates on Solana's validator infrastructure:
Get Validator Insights
Discover Validator Metrics
## Server Status Subscriptions
Subscribe to Solana's and MagicBlock's server status:
Subscribe to Solana Server Updates
Subscribe to MagicBlock Server Status
***
## MagicBlock Products
Execute real-time, zero-fee transactions securely on Solana.
Protect sensitive data with compliance — built on top of Ephemeral Rollups.
Move SPL tokens at rollup speed — public or private transfers, swaps, and private payments for trading and DeFi apps.
Combine real-time execution, session keys, token custody, price feeds, automation, and settlement.
Add provably fair onchain randomness to games, raffles, and real-time apps.
Access low-latency onchain price feeds for trading and DeFi.
***
# FAQ
Source: https://docs.magicblock.gg/pages/verifiable-randomness-functions-vrfs/introduction/faq
Common questions about Solana VRF
Solana VRF covers the most common questions about verifiable randomness before you ship on-chain apps.
### Why not use block hashes for randomness?
Block hashes can be predicted or manipulated by miners and validators, making them unsuitable for fair gameplay. MagicBlock's VRF proves the randomness was generated independently of block production.
### How fast is the randomness callback?
The VRF operates within the [ephemeral rollup](/pages/ephemeral-rollups-ers/introduction/ephemeral-rollup) execution window, so results are typically available within a single transaction round, without external polling.
### Can I audit the randomness proofs?
Yes. Each request includes a proof that anyone can verify on-chain using the same cryptography as the VRF signer. This transparency ensures players can confirm fairness.
### Who runs the oracles?
MagicBlock Solana VRF uses a permissioned set of oracles managed through on-chain queues. You can delegate your rollup to a specific queue so trusted operators fulfill your requests.
# Pricing
Source: https://docs.magicblock.gg/pages/verifiable-randomness-functions-vrfs/introduction/pricing
Pricing for provably fair randomness requests with MagicBlock Solana VRF.
MagicBlock Solana VRF pricing is charged per fulfilled randomness request. Fees cover proof generation and posting the verified result on-chain.
| VRF type |
Amount (SOL) |
Description |
| ER (\<50 ms) |
Free |
Per randomness request |
| Solana (\<500 ms) |
0.0008 |
Per randomness request |
| Solana (1/2 seconds) |
0.0005 |
Per randomness request |
## VRF Cost Simulator: 30 Days
Build a Solana VRF request and callback flow with the MagicBlock VRF SDK.
Return to the Solana VRF overview.
# Security
Source: https://docs.magicblock.gg/pages/verifiable-randomness-functions-vrfs/introduction/security
How Solana VRF randomness proofs are verified
Solana VRF proofs are cryptographically bound to the input `caller_seed` and to MagicBlock's VRF signer identity. Your callback enforces this with:
```rust theme={null}
#[account(address = ephemeral_rollups_sdk::vrf::consts::VRF_PROGRAM_IDENTITY)]
pub vrf_program_identity: Signer<'info>,
```
Only the official MagicBlock oracle can trigger the callback, preventing spoofed or manipulated results. Invalid proofs automatically fail, and other programs cannot front-run the request.
MagicBlock Solana VRF checks for conditions like `InvalidProof` and `Unauthorized` so incorrect signatures or unauthorized callers are rejected before your game logic runs. The VRF program has a published audit, so treat that report as the source of truth before going live.
Because everything executes inside the same deterministic [ephemeral rollup](/pages/ephemeral-rollups-ers/introduction/ephemeral-rollup) that runs your game logic, the random value cannot be reused or delayed.
For actionable integration guidance — seed selection, callback validation, and state management — see [Best Practices](/pages/verifiable-randomness-functions-vrfs/how-to-guide/best-practices).
Read the audit report and security notes.
Return to the Solana VRF overview.
# Overview
Source: https://docs.magicblock.gg/pages/verifiable-randomness-functions-vrfs/introduction/solana-vrf
Use MagicBlock Solana VRF to request provably fair randomness
MagicBlock's Solana VRF gives Solana programs a verifiable randomness primitive for gameplay, lotteries, matchmaking, and other real-time flows that need fair outcomes without trusting blockhashes or app servers.
Solana VRF is available on both Solana mainnet and MagicBlock Ephemeral Rollups.
Build a Solana VRF request and callback flow with the MagicBlock VRF SDK.
See how requests, proofs, oracle queues, and callbacks work.
Review per-request pricing and estimate monthly VRF costs.
Read the audit report and security notes.
Inspect the program, SDK, proof implementation, and examples on GitHub.
## What is Solana VRF?
Solana VRF is a verifiable random function implementation for Solana programs. Your program requests randomness, an oracle computes a random value with a cryptographic proof, and the MagicBlock VRF program verifies that proof before calling back into your program.
That proof makes the result auditable. Users and programs can verify that a random outcome was produced from the committed request instead of being chosen after the fact by a validator, server, or game operator.
## Why use MagicBlock Solana VRF?
Traditional randomness sources fall short on-chain: users cannot verify how off-chain numbers were generated, validators or operators can influence blockhash-based outcomes, and off-chain delivery does not always align with on-chain execution timing. Solana VRF addresses this:
* **Built for Solana programs**: request randomness through the `ephemeral-rollups-sdk` VRF module and consume the result in your own callback instruction.
* **Designed for real-time apps**: MagicBlock's ephemeral rollup execution model keeps randomness delivery low-latency for games and interactive flows.
* **Verifiable by design**: proofs are validated on-chain before your callback logic runs.
* **Open source and audited**: the VRF program is public, with audit coverage linked from the security docs.
## How Solana VRF works
1. Your program submits a randomness request with a `caller_seed`, callback discriminator, and callback accounts.
2. The request is added to an oracle queue for fulfillment.
3. A verified oracle computes the random value and proof.
4. The MagicBlock VRF program verifies the proof.
5. Your callback receives the random bytes and converts them into game or app logic, such as dice rolls, loot drops, or raffle winners.
## Common use cases
* Loot drops and gacha
* Matchmaking and shuffling
* Raffles and giveaways
* Randomized resource generation
* Random character attributes or NFT traits
* Any Solana app that needs auditable fairness
## Start with the right guide
Use the [Solana VRF quickstart](/pages/verifiable-randomness-functions-vrfs/how-to-guide/quickstart) when you are ready to implement request and callback instructions. Use the [security guide](/pages/verifiable-randomness-functions-vrfs/introduction/security) to review callback validation, signer checks, and audit links before going live.
# Technical Details
Source: https://docs.magicblock.gg/pages/verifiable-randomness-functions-vrfs/introduction/technical-details
How Solana VRF integrates with MagicBlock
Random numbers are generated via a VRF built on Curve25519's Ristretto group and proven using a Schnorr-like signature as described in [RFC 9381](https://datatracker.ietf.org/doc/html/rfc9381). The proof and output are returned to the rollup with a signed callback from the MagicBlock VRF signer PDA. Your program verifies the caller and then uses the randomness in gameplay logic.
Helper utilities like `random_u32`, `random_u8_with_range`, and `random_bool` make it simple to convert the `[u8; 32]` output into usable values. Because the request and consume steps occur inside the ephemeral execution window, users get real-time results with verifiable fairness and without relying on external servers.
## Flow
The flow starts with a “Request for randomness”.
1. Your program will CPI into the MagicBlock VRF program and append a request to the queue.
2. Once your randomness request is in the queue, an oracle will release the request and perform the randomness computation.
3. Upon completion, it returns the result and proof to the MagicBlock VRF program. After verifying the proof, the VRF program will callback into your program into a predefined function that will “consume” the randomness.
MagicBlock's VRF Program is open-source and audited. See the Solana VRF overview for the product summary.
## Oracle queues
Every randomness request names an **oracle queue** account (the `oracle_queue` field of `RequestRandomnessParams`). Like every Solana account, the queue lives on Solana — but a **delegated** queue is directly writable only from inside an ephemeral rollup, while a **non-delegated** queue is directly writable on the base layer. Request randomness from the queue that matches where your transaction runs — the base-layer queue from Solana, or the delegated queue from inside the ephemeral rollup. Reference the SDK constants from `ephemeral_rollups_sdk::vrf::consts` instead of hardcoding addresses wherever possible.
| Network | Base-layer queue | Delegated queue (ephemeral rollup) |
| -------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| Mainnet | `DEFAULT_QUEUE`
`Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh` | `DEFAULT_EPHEMERAL_QUEUE`
`5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc` |
| Devnet | `DEFAULT_QUEUE`
`Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh` | `DEFAULT_EPHEMERAL_QUEUE`
`5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc` |
| Localnet | `DEFAULT_TEST_QUEUE`
`GKE6d7iv8kCBrsxr78W3xVdjGLLLJnxsGiuzrsZCGEvb` | `DEFAULT_EPHEMERAL_TEST_QUEUE`
`Sc9MJUngNbQXSXGP3F67KvKwVnhaYn6kcioxXNVowYT` |
Mainnet and Devnet share the same default queue addresses — only the cluster you connect to differs. Localnet uses dedicated **test queues** that the local validator clones from Devnet; the `DEFAULT_TEST_QUEUE` / `DEFAULT_EPHEMERAL_TEST_QUEUE` constants ship with the VRF SDK.
Read the full audit report
Learn how to add randomness capabilities