Skip to main content

Quick Access

Check out example:

GitHub

SPL Tokens Anchor Implementation

Live Example App

Try private payments

Overview

Private Payments are the privacy use case built on top of SPL tokens on ER. 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 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.

API reference

Endpoints for deposit, transfer, withdraw, balances, stealth pools, and auth.

Reference program

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:
1

Request a challenge

GET /v1/spl/challenge returns a message for the user to sign.
2

Log in

POST /v1/spl/login exchanges the signed challenge for a bearer token.
3

Call protected endpoints

Send Authorization: Bearer <token> 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 or your own RPC.

Private transfers

  • POST /v1/spl/transfer with visibility: "private".
Two destination modes: 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: 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

Ephemeral SPL Tokens — Overview

The primitive behind private payments.

SDK Quickstart

The on-chain/SDK integration path.