Beam: Project Story ⚡
The story behind building a non-custodial Solana payment protocol that lets anyone pay with any token and settle in exact USDC.
Inspiration
It started with a simple frustration. As a freelancer and open-source contributor, I wanted to get paid in USDC—stable, predictable, no volatility. But my clients? They held SOL, BONK, JUP, RAY.
Every payment became a chore:
- Client goes to an exchange or DEX
- Swaps their token → USDC (paying fees, slippage)
- Sends USDC to my wallet
- I wait, verify, confirm
Why isn't there a "Stripe for Solana" where the payer uses whatever they have and the recipient gets exactly what they asked for?
That question became Beam.
What It Does
Beam is a non-custodial Solana payment-link protocol and dApp. Merchants and creators specify an exact amount in USDC (e.g. $10.00 USDC), while payers can check out using any token in their Solana wallet (SOL, JUP, BONK, RAY, USDT, PYTH, etc.).
Beam calculates the exact route via Jupiter Aggregator and constructs a single atomic Solana Versioned Transaction (v0) that:
- Swaps the input token → exact USDC amount (exact-out)
- Creates the recipient's USDC Associated Token Account (idempotent)
- Transfers the exact requested USDC to the recipient
If anything slips or fails, the entire transaction reverts atomically—ensuring complete safety for both parties. No partial states. No trust required.
The Atomic Transaction Formula
$$ \text{Single Tx} = \begin{cases} \text{DEX Swap (exact-out via Jupiter)} \
- \text{Recipient ATA Creation (idempotent)} \
- \text{TransferChecked (exact USDC amount)} \end{cases} $$
How We Built It
Architecture Overview
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Payer │────►│ Atomic Tx (v0) │────►│ Recipient │
│ (any SPL) │ │ 1. Jupiter Swap │ │ Exact USDC │
└─────────────┘ │ 2. ATA Create │ └─────────────────┘
│ 3. Transfer USDC │
└──────────────────┘
Key Components
| Component | Purpose | Tech |
|---|---|---|
| Intent Store | Persist payment requests (amount, recipient, expiry) | In-memory + JSON backup |
| Quote Engine | Calculate exact-in for exact-out USDC via Jupiter | /quote + /swap APIs |
| Tx Composer | Decompile Jupiter's v0 tx, append ATA + transfer, recompile with ALTs | @solana/web3.js v2, VersionedTransaction |
| Verifier | On-chain delta verification via parsed token balance changes | getParsedTransaction + RPC |
The Composer: Where the Magic Happens
The hardest part wasn't calling Jupiter—it was stitching the result into a valid v0 transaction with:
- Address Lookup Tables (ALTs) — Jupiter returns ALTs for compressed accounts. We preserve and extend them.
- Idempotent ATA Creation —
createAssociatedTokenAccountIdempotentInstructionso recipient doesn't need pre-existing USDC ATA. - TransferChecked — Enforces exact decimals + amount, prevents over/under-transfer.
- Fresh Blockhash + Signatures — Recompile with payer as fee-payer, ready for wallet signing.
// Simplified flow in src/swap/composer.ts
const jupiterTx = await jupiterClient.swap({
inputMint,
outputMint: USDC_MINT,
exactOut: targetAmountRaw
});
// Decompile → Append instructions → Recompile with ALTs
const composedTx = composeAtomicTx({
jupiterTx,
recipient,
targetAmountRaw,
payerPublicKey
});
On-Chain Verification
The backend re-fetches the transaction via getParsedTransaction and computes:
$$ \Delta \text{USDC}_{\text{recipient}} = \text{postTokenBalances} - \text{preTokenBalances} $$
Only if $\Delta \geq \text{targetAmountRaw}$ do we mark intent.status = "paid".
Challenges We Ran Into
1. Jupiter v6 API Changes Mid-Development
Jupiter migrated from v6 to v6.1 with breaking changes in response formats.
Solution: Abstracted behind JupiterClient interface. Swapped implementations in one file. Added integration tests against live devnet.
2. ALTs Mismatch on Recompile
Initial composer failed with AddressLookupTableAccountInvalid because we weren't preserving all ALTs from Jupiter's response.
Solution: Extract addressLookupTableAccounts from Jupiter's VersionedTransaction, merge with any new ones (e.g., for recipient ATA), pass full array to compileToV0Message.
3. Phantom Wallet Signing v0 Transactions
Early versions: Phantom prompted "Sign Transaction" but returned Transaction not VersionedTransaction.
Solution: Detect wallet adapter version. Use signAndSendTransaction for v0-aware wallets; fallback to legacy for older versions.
4. RPC Rate Limits on Verification
getParsedTransaction is heavy. On devnet, quick retries hit rate limits.
Solution: Exponential backoff + configurable RPC endpoint (Helius/QuickNode). Added verify endpoint idempotency key.
5. Decimal Precision Hell
SPL tokens: 6 decimals (USDC), 9 decimals (SOL), variable for others. TransferChecked requires exact decimals.
Solution: Token metadata cache (token-catalog.json) with mint → decimals mapping. Validate on quote + build.
Accomplishments We're Proud Of
- True atomic settlement — One transaction, exact USDC out, or full revert. No custodial risk, no partial failures.
- Exact-out pricing with slippage protection — Payers never overpay; surplus stays in their wallet.
- Zero-config dev experience —
bun install→bun run dev→ working on localhost:3000 in seconds. - On-chain verification, not webhook trust — Cryptographic proof of settlement via RPC-parsed balance deltas.
- Versioned Transaction (v0) composition from scratch — Decompiling Jupiter's tx, extending with ATA creation + TransferChecked, recompiling with ALTs and fresh blockhash.
- Preloaded demo intents — Coffee ($5) and Invoice ($25) links work instantly for hackathon demos.
- Full TypeScript + Bun stack — Type-safe end-to-end, native testing, 10x faster installs.
What We Learned
1. Exact-Out Math Is Subtle
Jupiter's /quote with swapMode: "ExactOut" returns the minimum input for a given output. But on-chain, prices move. We added a slippage buffer (default 1%):
$$ \text{inputAmount} = \text{quote.inputAmount} \times (1 + \text{slippageBps} / 10000) $$
Surplus tokens? Stay in payer's wallet. The swap is exact-out; any favorable price movement benefits the payer.
2. Versioned Transactions Are Powerful But Fragile
- ALTs must match exactly what the runtime expects
- Message decompilation/recompilation loses signature data—must rebuild from scratch
TransactionMessage.decompile()+compileToV0Message()is the pattern
3. On-Chain Verification > Webhook Trust
We don't trust the frontend. The backend re-fetches the transaction and computes balance deltas. Only cryptographic proof marks an intent paid.
4. Bun + TypeScript = Delightful DX
- Native
bun test,bun --watch, zero-config TypeScript bun installis 10x faster than npm- Top-level await in scripts—no wrapper functions
What's Next for Beam
| Feature | Status |
|---|---|
| Multi-recipient splits (e.g., 80% merchant, 20% platform) | 🔄 Design |
| Recurring intents (subscriptions via cron + atomic tx) | 📋 Planned |
| Cross-chain via Wormhole (Ethereum USDC → Solana USDC) | 💭 Research |
| Mobile-first PWA (offline intent creation, QR checkout) | 🎨 Design |
| Merchant dashboard (analytics, refunds, webhooks) | 📋 Planned |
Acknowledgments
- Jupiter Aggregator — The best swap infrastructure on Solana
- Solana Foundation — For Versioned Transactions, ALTs, and
TransferChecked - Bun Team — For making TypeScript feel like a scripting language
- Phantom & Solflare — For first-class v0 transaction signing UX
Built with ☕, 🌙, and the belief that payments should be as composable as the rest of crypto.
Built With
- blockchain
- solana
- typescript
Log in or sign up for Devpost to join the conversation.