How to Build a Swap App with Venum API
This guide shows how to build a simple non-custodial Solana swap app on top of the Venum API.
It is inspired by the public Venum Swap flow, but keeps the architecture generic and avoids product-specific internals. The goal is to help you build your own swap UI, wallet flow, and transaction lifecycle without rebuilding DEX routing and transaction assembly from scratch.
What Your Swap App Needs
At a minimum, a swap app needs five pieces:
- Token selection
- Amount input and quote refresh
- Wallet connection
- Transaction build, sign, and submit flow
- Transaction status feedback
Venum handles the routing and swap transaction construction. Your app handles the user experience.
Recommended Architecture
Use a thin backend in front of the Venum API.
Browser UI
-> Your backend
-> Venum API
-> Solana DEX routing + transaction buildThis gives you three practical benefits:
- Your API key stays server-side
- You can add your own auth, logging, and rate limiting
- You can reshape responses for your frontend without coupling the UI to raw API payloads
Core Flow
The app flow is straightforward:
Select tokens -> Request quote -> Connect wallet -> Build unsigned swap -> Sign -> Submit -> Track statusThe main Venum endpoints involved are:
POST /v1/quotePOST /v1/swap/buildPOST /v1/swapGET /v1/tx/:signatureor the transaction stream for status updates
Step 1: Create a Quote Endpoint in Your Backend
Your frontend should call your server, not Venum directly.
// Example: /api/quote
export async function postQuote(req: Request) {
const body = await req.json();
const response = await fetch('https://api.venum.dev/v1/quote', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.VENUM_API_KEY!,
},
body: JSON.stringify({
inputMint: body.inputMint,
outputMint: body.outputMint,
amount: body.amount,
slippageBps: body.slippageBps,
}),
});
return Response.json(await response.json(), { status: response.status });
}On the frontend, refresh the quote whenever the user changes:
- input token
- output token
- amount
- slippage
Add a short debounce so you do not fire a request on every keystroke.
Step 2: Build the Swap UI Around Quotes
Your quote-driven UI should usually show:
- estimated output amount
- best route or DEX label
- price impact
- slippage setting
- loading and no-route states
A good pattern is:
- pre-wallet: show quotes immediately so the page feels usable on first paint
- post-wallet: build the unsigned transaction only when the user is ready to swap
That keeps the UI fast while still using POST /v1/swap/build as the source of truth before signing.
Step 3: Connect a Wallet
Keep the swap non-custodial:
- your app never sees the user's private key
- your backend never signs on behalf of the user
- the wallet signs the unsigned transaction locally
All you need from the wallet for build is the public key.
const userPublicKey = wallet.publicKey.toBase58();Step 4: Build the Unsigned Transaction
When the user presses Swap, call your backend build route.
// Example: /api/swap/build
export async function postSwapBuild(req: Request) {
const body = await req.json();
const response = await fetch('https://api.venum.dev/v1/swap/build', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.VENUM_API_KEY!,
},
body: JSON.stringify({
inputMint: body.inputMint,
outputMint: body.outputMint,
amount: body.amount,
slippageBps: body.slippageBps,
userPublicKey: body.userPublicKey,
}),
});
return Response.json(await response.json(), { status: response.status });
}The response gives you an unsigned transaction plus the quoteId needed for submission.
Before you show the wallet prompt, surface useful state in the UI:
- building transaction
- simulation or validation failure
- insufficient balance
- stale quote
Step 5: Sign Client-Side
Once you receive the unsigned transaction, deserialize it and ask the wallet to sign.
import { VersionedTransaction } from '@solana/web3.js';
const tx = VersionedTransaction.deserialize(
Buffer.from(build.transaction, 'base64')
);
const signed = await wallet.signTransaction(tx);
const signedTransaction = Buffer.from(signed.serialize()).toString('base64');This is also the point where you can let advanced users inspect the route and transaction details before signing.
Step 6: Submit the Signed Transaction
Send the signed transaction back through your backend.
// Example: /api/swap/submit
export async function postSwapSubmit(req: Request) {
const body = await req.json();
const response = await fetch('https://api.venum.dev/v1/swap', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.VENUM_API_KEY!,
},
body: JSON.stringify({
signedTransaction: body.signedTransaction,
quoteId: body.quoteId,
}),
});
return Response.json(await response.json(), { status: response.status });
}Your UI should move through clear states:
- Quoting
- Building
- Signing
- Sending
- Confirming
- Confirmed or failed
That state machine matters more than most teams think. It is the difference between a swap app that feels trustworthy and one that feels broken during wallet and network delays.
Step 7: Show Status After Submission
After submission, track transaction status with either:
GET /v1/tx/:signature- the transaction status stream if you want live updates
Polling example:
async function waitForStatus(signature: string) {
for (;;) {
const response = await fetch(`/api/tx/${signature}`);
const status = await response.json();
if (status.confirmed || status.failed) return status;
await new Promise((resolve) => setTimeout(resolve, 500));
}
}At minimum, show:
- submitted signature
- linking out to an explorer
- success state
- readable failure state
Step 8: Make Retries Safe
A production swap app must survive flaky networks without double-sending. Three rules cover it:
- Retries are idempotent.
/v1/swapand/v1/senddedup on the transaction signature — resending the same signed transaction after a502returns the original result instead of broadcasting twice. So the safe retry is simply: send the same bytes again. - Branch on
code, not prose. Error bodies carry a stablecode(QUOTE_EXPIRED,NO_ROUTE,RATE_LIMITED,SEND_FAILED,SUBMISSION_FAILED, …). Thesignatureis included on submit failures, so you never derive it locally. - Refresh quotes just-in-time.
/v1/swap/buildreturnsexpiresAt(epoch ms). Re-build before it elapses; a410 QUOTE_EXPIREDjust means re-quote.
async function submit(signedTxBase64: string) {
for (let attempt = 0; attempt < 4; attempt++) {
const res = await fetch('/api/swap', { // your backend proxies to Venum
method: 'POST',
body: JSON.stringify({ transaction: signedTxBase64 }),
});
const body = await res.json();
if (res.ok) return body; // { signature, status: 'submitted' }
if (body.code === 'SEND_FAILED' || body.code === 'SUBMISSION_FAILED') {
const wait = Number(res.headers.get('Retry-After')) * 1000 || 250 * 2 ** attempt;
await new Promise((r) => setTimeout(r, wait));
continue; // resending the same tx is safe
}
throw new Error(`${body.code}: ${body.error}`);
}
}Throttle proactively with the X-RateLimit-Remaining / X-RateLimit-Reset headers instead of waiting for 429. See Reliability & Retries for the full contract.
Frontend Patterns Worth Copying
The best swap apps tend to share the same UX patterns:
- Quote before wallet connect so the app feels instant
- Pause quote refresh while the wallet popup is open
- Lock the CTA while a transaction is in flight
- Preserve token pair and amount in the URL for shareable links
- Show route, pool, or venue information in a compact details row
- Let users adjust slippage without forcing them into advanced settings screens
You do not need a complicated architecture to get these right. One swap card with a clear async lifecycle is usually enough.
Keep the API Key Off the Client
Do not embed your Venum API key in browser code.
Bad:
fetch('https://api.venum.dev/v1/quote', {
headers: { 'x-api-key': 'public-key-in-client-code' },
});Good:
fetch('/api/quote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});This keeps the browser simple and your credentials private.
Production Checklist
- Keep your API key server-side only
- Debounce quotes during input typing
- Disable swaps for zero amount and no-route states
- Handle quote expiry by re-quoting and rebuilding
- Surface wallet rejection separately from on-chain failure
- Show transaction signatures and explorer links
- Log build and submit errors on your backend
- Test on mobile wallets as well as desktop extensions
Minimal App Structure
You can ship a clean first version with just these pieces:
src/
api/
quote.ts
swap-build.ts
swap-submit.ts
tx-status.ts
components/
SwapCard.tsx
TokenSelector.tsx
WalletButton.tsx
lib/
tokens.ts
format.ts
wallet.tsStart with one swap card. Add watchlists, charts, portfolio views, and routing analytics later.
Next Steps
- Quick Start for the first quote/build/submit flow
- Reliability & Retries for idempotency, backoff, and the rate-limit header contract
- Swap Building for unsigned transaction details
- Build a Telegram Trading Bot — same primitives, packaged as a Telegram bot you can ship from your phone
- Build a Trading Bot for the production-grade backend
- Composable Instructions if you want to extend swaps with your own instructions
- API Reference for request and response fields
