Skip to content

Swap Building

The swap build endpoint returns an unsigned VersionedTransaction that you sign client-side and submit back.

Flow

1. POST /v1/swap/build  →  unsigned TX (base64)
2. Sign with your wallet (client-side)
3. POST /v1/swap         →  submit through Venum's landing fan-out

Non-Custodial

Venum never touches your private keys. The transaction is built with your public key as the signer, returned unsigned, and you sign it locally. We only see the signed transaction when you submit it.

Request

bash
POST /v1/swap/build
json
{
  "inputMint": "So11111111111111111111111111111111111111112",
  "outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
  "amount": "1000000000",
  "slippageBps": 100,
  "userPublicKey": "YourWalletPublicKeyBase58"
}

What's in the Transaction

The returned transaction contains:

  1. Compute budgetSetComputeUnitLimit sized per DEX type (200K-600K)
  2. ATA management — optional associated token account creation when needed
  3. Native SOL wrap/unwrap — when the route starts or ends in SOL
  4. Swap instruction(s) — the actual DEX swap path

Quote ID

The response includes a quoteId that's valid for 30 seconds. You must include this when submitting the signed transaction so Venum can tie the submission back to the built route.

json
{
  "transaction": "AQAAAA...base64...==",
  "quoteId": "q_42_1712000000000",
  "estimatedOutput": "134520000",
  "simulatedOutput": "134518900",
  "displayedQuote": { "quoteId": "dq_ab12...", "outputAmount": "134600000", "driftBps": 6 },
  "minOutput": "133174800",
  "feeLamports": "0",
  "feeBps": 0,
  "computeUnits": 400000
}

The build runs a fresh route plan at build time — never against stale state — so its estimatedOutput can differ from the quote you showed the user. Pass the quote's quoteId (from /v1/quote or /v1/quote/stream) in the build request and the response echoes a displayedQuote object:

  • outputAmount — the output your displayed quote promised.
  • driftBps — how much worse the freshly-built route is than that displayed output (0 when the build is equal or better).

Check driftBps against your own tolerance before signing. If you want Venum to reject the build server-side instead of surfacing the drift, also pass maxDriftBps — the build then returns 409 QUOTE_MOVED when the fresh output falls more than that many bps below the displayed quote (opt-in; omit it and the build always proceeds and just reports driftBps).

typescript
const build = await fetch('/api/v1/swap/build', { method: 'POST', body: JSON.stringify({
  inputMint, outputMint, amount, slippageBps, userPublicKey,
  quoteId,            // the id from the quote you displayed
  // maxDriftBps: 100 // optional: 409 QUOTE_MOVED if the build is >1% worse
}) }).then(r => r.json());

if (build.displayedQuote && build.displayedQuote.driftBps > YOUR_TOLERANCE_BPS) {
  throw new Error(`build ${build.displayedQuote.driftBps}bps worse than displayed quote — not signing`);
}

displayedQuote is null when no resolvable quoteId was passed (the build ran unpinned) — in that case compare estimatedOutput to your own reference before signing.

The build response includes an attestation — a detached Ed25519 signature from Venum over the transaction bytes and the swap summary. Verifying it against Venum's pinned key catches a response tampered in transit (a swapped destination, widened slippage, a redirected output) before you sign. Venum signs; you verify. See Response attestation for the message format and a verification snippet, and GET /v1/attestation/pubkey for the key to pin.

Signing Example

typescript
import { VersionedTransaction } from '@solana/web3.js';

// Decode the unsigned transaction
const txBytes = Buffer.from(response.transaction, 'base64');
const tx = VersionedTransaction.deserialize(txBytes);

// Sign with your keypair
tx.sign([yourKeypair]);

// Encode back to base64 for submission
const signedBase64 = Buffer.from(tx.serialize()).toString('base64');