Skip to content

POST /v1/swap/build

Build an unsigned swap transaction. You sign it client-side, then submit via POST /v1/swap.

Venum does not add a platform swap fee on /v1/swap/build or /v1/swap. Any fees shown in routes come from the venue or the network path, not from Venum.

/v1/swap/build returns JSON by default. If you send Accept: application/octet-stream, it returns raw unsigned transaction bytes instead.

Internally runs the same routing engine as POST /v1/quote: direct pools, 2-hop and 3-hop routes through every tracked token, freshness gating (60 s cutoff), freshness-weighted scoring, and the same exact-CL re-quote validation against on-chain tick state. The selected route is built as either a single swap instruction (direct) or N chained swap instructions sharing intermediate ATAs (multi-hop).

Build pass behavior

Two policies apply only to /v1/swap/build (and not /v1/quote), because they're driven by what the user can actually sign and land:

  • Short-route bias. Within each confidence tier, 1-hop and 2-hop routes are tried before 3-hop. Stock wallets submit transactions without lookup tables, so the 1232-byte size limit is hard, and 3-hop concentrated-liquidity routes routinely overflow it. The bias makes the build pass try shorter routes first so it finds something that fits.
  • No PDA init on the user's behalf. The build pass refuses any route that would require initializing a tick-array (Orca/Raydium CLMM) or bin-array (Meteora DLMM) PDA. Each one is ~0.07 SOL of rent and would silently quote a tiny user out of their own funds. When a candidate hits this, the build pass skips it and falls through to the next route — which almost always exists through pools with already-initialized arrays at zero marginal cost. If no route satisfies this, the endpoint returns 404.

If a candidate's serialized tx exceeds the 1232-byte limit, the build pass also falls through to the next candidate instead of returning 500. The combination of these fallbacks makes /v1/swap/build resilient to the long tail of "this route exists but the dApp can't actually sign it" cases.

Request

http
POST /v1/swap/build
Content-Type: application/json
x-api-key: YOUR_API_KEY

Body

FieldTypeRequiredDefaultDescription
inputMintstringYesBase58 mint or token symbol (e.g. "SOL", "USDC")
outputMintstringYesBase58 mint or token symbol
amountstringYesAmount in smallest units
slippageBpsnumberNo100Slippage tolerance (1-5000 bps)
userPublicKeystringYesBase58 public key of the signer
createAtaIfMissingbooleanNotrueCreate input/output token accounts if they don't exist
excludePoolsstring[]No[]Pool addresses to skip when ranking (e.g. retry after a failure)
simulatebooleanNofalseRun simulateTransaction to compute exact CU usage and bubble pre-signature errors. Adds ~100-200 ms latency. See Simulation below.

Simulation

When simulate: true, the build pass:

  1. Builds the tx with setComputeUnitLimit(1_400_000) (the network max)
  2. Calls connection.simulateTransaction(tx, { sigVerify: false, replaceRecentBlockhash: true })
  3. On success, replaces the CU limit with consumedUnits × 1.1 and returns the smaller, cheaper-to-execute tx
  4. On failure (simulate.value.err), that candidate route is dropped and the build pass falls through to the next viable route. If every candidate fails, the endpoint returns 400 with simulation details/logs.

The simulated field in the response indicates whether the returned computeUnits came from a real sim (true) or a per-DEX heuristic (false).

Example

json
{
  "inputMint": "SOL",
  "outputMint": "USDC",
  "amount": "1000000000",
  "slippageBps": 100,
  "userPublicKey": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
}

Full mint addresses also accepted:

json
{
  "inputMint": "So11111111111111111111111111111111111111112",
  "outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
  "amount": "1000000000",
  "slippageBps": 100,
  "userPublicKey": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
}

Response

200 OK

json
{
  "transaction": "AQAAAA...base64...==",
  "quoteId": "q_3f4a8b2c0d6e1f7a8b2c0d6e1f7a8b2c",
  "route": {
    "dex": "orca-whirlpool",
    "poolAddress": "HJPjoWUr...",
    "outputAmount": "134520000"
  },
  "estimatedOutput": "134520000",
  "minOutput": "133174800",
  "feeLamports": "0",
  "feeBps": 0,
  "computeUnits": 184302,
  "simulated": true
}

200 OK (binary mode)

http
POST /v1/swap/build
Accept: application/octet-stream
x-api-key: YOUR_API_KEY

Response:

  • body: raw unsigned VersionedTransaction bytes
  • headers:
    • x-quote-id
    • x-route-json (base64url-encoded JSON metadata envelope)
    • x-estimated-output
    • x-simulated-output
    • x-min-output
    • x-fee-lamports
    • x-fee-bps
    • x-compute-units
    • x-simulated

x-route-json decodes to:

json
{
  "route": {
    "dex": "orca-whirlpool",
    "poolAddress": "HJPjoWUr...",
    "hops": []
  },
  "estimatedOutput": "134520000",
  "minOutput": "133174800",
  "feeBps": 0,
  "computeUnits": 184302,
  "simulated": true
}

Response Fields

FieldTypeDescription
transactionstringBase64-encoded unsigned VersionedTransaction
quoteIdstringQuote identifier — required for submission (30s TTL)
routeobjectSelected route details
estimatedOutputstringExpected output in smallest units
minOutputstringMinimum output after slippage
feeLamportsstringVenum fee in input token units (currently always "0")
feeBpsnumberFee rate in basis points (currently always 0)
computeUnitsnumberAllocated compute units (real if simulated, hardcoded per-DEX otherwise)
simulatedbooleanWhether computeUnits was computed via simulateTransaction or estimated

Binary Example

ts
const response = await fetch('https://api.venum.dev/v1/swap/build', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    'x-api-key': API_KEY,
    'accept': 'application/octet-stream',
  },
  body: JSON.stringify({
    inputMint: 'SOL',
    outputMint: 'USDC',
    amount: '1000000000',
    userPublicKey: wallet.publicKey.toBase58(),
  }),
})

const quoteId = response.headers.get('x-quote-id')!
const routeJson = response.headers.get('x-route-json')!
const routeMeta = JSON.parse(Buffer.from(routeJson, 'base64url').toString('utf8'))
const unsignedBytes = new Uint8Array(await response.arrayBuffer())
const tx = VersionedTransaction.deserialize(unsignedBytes)

Transaction Contents

The unsigned transaction contains, in order:

  1. SetComputeUnitLimit — sized per DEX type (200K-600K CU)
  2. ATA creation (if createAtaIfMissing: true and account missing) — idempotent createAssociatedTokenAccount for input/output tokens. Always idempotent so the same tx is safe to retry.
  3. Native SOL wrap (when inputMint is SOL) — createAssociatedTokenAccountIdempotent for the user's WSOL account, then SystemProgram.transfer of amount lamports into it, then SyncNative so the SPL token balance reflects the deposit. No-op if the input is any other mint.
  4. Output WSOL ATA creation (when outputMint is SOL) — idempotent createAssociatedTokenAccount for the user's WSOL account so the swap has a valid output_token_account to write into. Without this, swaps to native SOL fail with AccountNotInitialized (Anchor 3012) on Raydium CLMM and similar DEXes.
  5. Swap instruction(s) — DEX-specific swap (may include tick/bin array init)
  6. Native SOL unwrap (when outputMint is SOL) — closeAccount on the WSOL ATA, returning all SOL (the swap output + the temporary rent) to the user's main account.

Set createAtaIfMissing: false if all of the user's token accounts are pre-created to skip the RPC check and keep transaction size minimal. The native-SOL wrap/unwrap and output-WSOL-ATA steps are unconditional regardless of this flag — they're driven by the input/output mint, not by RPC state.

Errors

StatusDescription
400Invalid request body
401Missing or invalid API key
400Invalid request body, or simulate: true and every candidate route failed simulation.
404No pools found, no valid quote, or every candidate route was filtered or failed validation/build constraints. Treat as "skip this opportunity", not "retry the API".
429Rate limit exceeded
503Blockhash not available (retry in a moment)

Rate Limit

TierLimit
free5 / min
starter30 / min
pro150 / min