Documentation
How @ works.
A complete reference for sendat. The product is built on three pieces — a small Solana program, an off-chain verifier, and email plumbing — that compose into something that feels like email and behaves like a non-custodial vault. This page walks through each one.
Sending an email moves money
The single mental model: your email is your wallet. After a one-time Google sign-in on /start, a Solana wallet is bound to your verified address and lives on the server, controlled by your identity. You never touch a key.
To send, you compose a normal email. Our address goes in To, the command (amount + recipient) goes in the Subject, and the Body becomes the note your recipient sees.
From: you@gmail.com ← DKIM-signed by Google To: send@sendat.email ← our inbound address Subject: Send 0.1 SOL to mom@gmail.com ← amount + token + recipient Body: For your birthday. ← optional, used as sender note
Want to check your own balance instead? Email balance@sendat.email from your registered address (subject and body can be anything). We reply with your live wallet balance.
The lifecycle that follows is straightforward, though every step is verifiable.
1. Your email provider DKIM-signs your message.
2. Cloudflare Email Routing receives it for send@sendat.email
and hands it to a Worker.
3. The Worker forwards the raw RFC822 message to our backend's
POST /inbound endpoint over an authenticated channel.
4. The backend re-verifies the DKIM signature. Any tampering
with the From, Subject, or Body breaks the signature → reject.
5. The backend parses the subject ("Send X SOL to email"),
matches your verified From to a registered wallet, and
immediately signs create_envelope with your server wallet
(Privy) and submits to Solana mainnet. No confirmation step
— your DKIM-signed email IS the authorization.
6. The fee goes to the treasury; the net amount locks in a
PDA vault on the @ program. An event is emitted.
7. You get a confirmation email — "Sent X SOL to <recipient>"
with a Solscan link.
8. The recipient receives a clean transactional email
with one button to claim.
9. They sign in with Google. The backend verifies their email
ownership, issues a verifier-signed authorization, and
submits claim_envelope. Funds land in their wallet.
10. You get a second email — "Claimed — they received your X SOL".
11. The envelope account closes. Rent is returned to you.Two parts of this lifecycle deserve their own section: how the program actually moves the funds, and how the verifier signature works without making us custodial. We get there in the next two sections.
Withdrawing to your own Solana wallet
Funds in your @ wallet aren't trapped. From /start, both senders and recipients can withdraw any amount to any Solana address at any time — Phantom, Solflare, a Ledger pubkey, a multisig, whatever. The withdraw is a plain SystemProgram.transfer signed by your server-wallet's authorization key; we keep a single rent-exempt minimum (~0.00089 SOL) on the source so the account doesn't get garbage-collected.
That makes the @ wallet a soft on-ramp: you can sit on the balance and keep transacting by email, or move it out the instant you want self-custody. Nothing in the protocol prevents the latter, and nothing in the protocol forces the former.
The Solana program
All the money lives in a single Anchor program deployed on mainnet. It exposes four instructions and uses two account types. The code is small on purpose — small surface area, fewer places to hide bugs.
Program ID: rbqtJWapDC5GFPjD4CYij4D7FY8ZWf5Nwggf6FYkwXn
Accounts
pub struct Config {
pub authority: Pubkey, // admin
pub verifier: Pubkey, // off-chain signer for claims
pub treasury: Pubkey, // receives the protocol fee
pub fee_bps: u16, // 50 = 0.5%, hard-capped at 200
pub bump: u8,
}pub struct Envelope {
pub sender: Pubkey,
pub email_hash: [u8; 32], // SHA-256 of normalized email
pub amount: u64, // post-fee, base units
pub token_mint: Option<Pubkey>, // v1: always None (SOL). Reserved for SPL.
pub expiry_unix: i64,
pub nonce: u64,
pub claimed: bool,
pub created_unix: i64,
pub bump: u8,
pub vault_bump: u8,
}The recipient's email never appears on-chain in plaintext. Only its SHA-256 hash does. We can't iterate the chain and learn who's getting paid by who.
Instructions
initializeOne-time, run after deploy. Sets the verifier authority, treasury, and fee bps.
create_envelope(email_hash, amount, expiry_days, nonce)Locks SOL. The fee is transferred to the treasury, the net amount is held in the envelope PDA directly as lamports. SPL support is reserved for a later version. Emits EnvelopeCreated.
claim_envelope(recipient_wallet)Pays the recipient. Must be preceded in the same transaction by an ed25519 verify instruction proving the verifier authorized this exact (envelope, recipient) pair. Closes the envelope account.
reclaim_expired()Permissionless. After expiry, anyone can call it. Funds and rent return to the original sender. Funds can never get stuck.
What this guarantees
Once an envelope is created, the bytes of the program decide where the funds can go:
- — Until expiry: only a transaction authorized by the verifier can claim, and only to a specific recipient.
- — After expiry: only the original sender (anyone can submit; the funds always return to them).
- — There is no admin instruction that can move escrow funds. Not even by the deployer.
The verifier signature
This is the cleverest piece. How do you authorize a claim when you refuse to store the email on-chain, and the recipient doesn't have a wallet yet?
We use Solana's native ed25519 verification program. Off-chain, the sendat verifier signs a 32-byte message that uniquely identifies the (envelope, recipient, nonce) tuple. On-chain, our program reads the preceding instruction in the same transaction, asserts it was the ed25519 program, and pulls the public key and message it covered. If the pubkey matches the configured verifier and the message matches what our program re-derives from its own state, the claim is authorized.
message = SHA-256( envelope_pubkey ‖ recipient_wallet ‖ nonce_le_8_bytes )
Instruction 0: Ed25519 native program
data: { signature, public_key, message }
Instruction 1: @ program — claim_envelope(recipient)
introspects instruction (current_index − 1):
require it's the ed25519 program
require public_key == config.verifier
require message == SHA-256(envelope ‖ recipient ‖ nonce)
transfer funds to recipient, close accountWhy this works:
- — The ed25519 native program does the actual signature math. Forging a signature is computationally infeasible.
- — The signature commits to a specific recipient pubkey. The verifier can't sign once and have it work for any recipient.
- — The nonce makes each envelope unique. A signature for envelope A can't be replayed against envelope B.
- — The envelope account closes on claim. A signature for a given envelope can't be reused for the same envelope twice.
The verifier is the gatekeeper of identity, but not of funds. The most an adversary who steals the verifier key could do is authorize a specific recipient for a specific envelope. They cannot drain the program, mint envelopes, change recipients post-hoc, or extract funds to themselves.
What we can and cannot do
A clear statement of trust assumptions.
What is mathematically enforced
- — Funds in an envelope are owned by the program, not by us. No private key on our side can move them.
- — After expiry, the sender can recover their own funds without anyone's permission.
- — Email hashes are one-way. The chain doesn't reveal recipient identities.
- — A claim signature can authorize exactly one recipient for exactly one envelope.
- — Withdraw is always available. Anyone with a @ wallet (sender or recipient) can move the full balance to any external Solana address at any time. There is no lock-up, no waiting period, and no per-address allowlist.
What you trust us with
- — Verifier key custody. If the verifier key is leaked, an attacker can authorize claims they shouldn't. Mitigation: KMS, rotation, monitoring.
- — DKIM verification correctness. If our DKIM checker accepts a spoofed signature, an attacker can send from your address. We use mailauth, the same library hardened email infra uses.
- — Server wallet operator (Privy). Privy holds the server-controlled keys that sign your outbound sends. Their security model is your wallet's security model.
- — Email account. Whoever controls your inbox can also send valid DKIM-signed emails from it. Defending the inbox is on you.
What we can never do, even malicious
- — Move escrow funds to ourselves.
- — Block a sender from reclaiming after expiry.
- — Sign claims that bypass the on-chain pubkey check.
- — Read your recipient list from the chain.
The $Email token + fee loop
$Email is the protocol token. Fair-launched on PumpFun — no team allocation, no presale, no vesting. Two independent SOL streams flow into it, and both reduce supply.
Contract
Stream 1 — Protocol fee (0.5% on every send)
Every envelope pays 0.5% of the gross amount to a treasury wallet at create time. The fee is hard-capped in the program at 200 bps (2%); we ship at 50 bps and that's enforced on-chain.
The treasury is not idle. A cron sweeps accumulated SOL, swaps it for $Email on Jupiter / PumpSwap, and burns 100% of what it buys. Every send shrinks the supply.
Stream 2 — PumpFun creator fees (20% of creator fees)
$Email is launched on PumpFun, which pays a creator fee on every trade of the token. We split that stream:
- — 80% to the team wallet. Funds operations (Resend, Helius RPC, Privy, hosting) and salaries. Withdrawable to any Solana address — no time-lock.
- — 20% into the same buy-and-burn pipeline. Joins the protocol-fee stream in the 15-minute swap on Jupiter and is burned with it. Every trade also shrinks the supply.
We picked burn over a holder-airdrop on purpose. Anything we send to "holders" gets farmed by snapshot sniping and whale wallets. Burning is transparent (the burn tx is public), un-farmable, and rewards every holder equally — by making the supply smaller.
envelope sent ─┐
│
├──→ treasury (SOL)
│ │
PumpFun trade ──┤ ├──→ swap on Jupiter ──→ burn $Email
(20% of fee) ┘ │
│
team wallet (80%) ←─────┘Both streams converge in the same burn address. You can verify the full flow on-chain at any time — envelope create_fee tx → treasury → swap → burn.
Edge cases
Recipient never claims
After 30 days, anyone can submit reclaim_expired. We run a daily cron for our own users; you can also hit Solana directly. Funds always return to the original sender.
Sender mistypes an amount or recipient
We don't bounce typos for you, so re-read the subject before hitting Send. If the recipient address is wrong and they never claim, the funds return to you automatically after 30 days via reclaim_expired. If the recipient is right but you wanted a different amount, ask them to claim and refund the difference.
Spoofed sender
If DKIM verification fails, the email is rejected at the gate. Providers without DKIM (rare) cannot send money — we don't fall back to permissive checks.
Recipient already has a Solana wallet
On claim, Privy provisions a Solana wallet bound to their Google account. If they already have one tied to that email, they receive into it. They can later export the key and move to a non-custodial wallet they run themselves.
API surface
The backend exposes a handful of endpoints. Most are internal (Cloudflare Email Worker). A few are public.
GET /statsPublic, no auth. Returns aggregated counts. Used by the dashboard.
GET /verifierPublic. Returns the verifier authority pubkey.
GET /claim-info/:idPublic. Anonymized envelope info for the claim page (amount, token, status). Never returns the recipient email.
POST /claimRecipient-side claim. Requires a Privy access token for email-ownership proof. Server submits claim_envelope.
POST /registerBinds a Google-verified email to a Privy server-wallet on first /start visit.
POST /wallet/activityPrivy-auth. Returns the signed-in user's sent + received envelopes.
POST /wallet/withdrawPrivy-auth. SystemProgram.transfer signed by the user's server-wallet to any external Solana address.
POST /inboundInternal. The Cloudflare Email Worker posts the raw RFC822 message here. DKIM-verified, then dispatched to send@ or balance@.
