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
POST /v1/swap/build
Content-Type: application/json
x-api-key: YOUR_API_KEYBody
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
inputMint | string | Yes | — | Base58 mint or token symbol (e.g. "SOL", "USDC") |
outputMint | string | Yes | — | Base58 mint or token symbol |
amount | string | Yes | — | Amount in smallest units |
slippageBps | number | No | 100 | Slippage tolerance (1-5000 bps) |
userPublicKey | string | Yes | — | Base58 public key of the signer |
createAtaIfMissing | boolean | No | true | Create input/output token accounts if they don't exist |
excludePools | string[] | No | [] | Pool addresses to skip when ranking (e.g. retry after a failure) |
simulate | boolean | No | false | Run 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:
- Builds the tx with
setComputeUnitLimit(1_400_000)(the network max) - Calls
connection.simulateTransaction(tx, { sigVerify: false, replaceRecentBlockhash: true }) - On success, replaces the CU limit with
consumedUnits × 1.1and returns the smaller, cheaper-to-execute tx - 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 returns400with 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
{
"inputMint": "SOL",
"outputMint": "USDC",
"amount": "1000000000",
"slippageBps": 100,
"userPublicKey": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
}Full mint addresses also accepted:
{
"inputMint": "So11111111111111111111111111111111111111112",
"outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "1000000000",
"slippageBps": 100,
"userPublicKey": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
}Response
200 OK
{
"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)
POST /v1/swap/build
Accept: application/octet-stream
x-api-key: YOUR_API_KEYResponse:
- body: raw unsigned
VersionedTransactionbytes - headers:
x-quote-idx-route-json(base64url-encoded JSON metadata envelope)x-estimated-outputx-simulated-outputx-min-outputx-fee-lamportsx-fee-bpsx-compute-unitsx-simulated
x-route-json decodes to:
{
"route": {
"dex": "orca-whirlpool",
"poolAddress": "HJPjoWUr...",
"hops": []
},
"estimatedOutput": "134520000",
"minOutput": "133174800",
"feeBps": 0,
"computeUnits": 184302,
"simulated": true
}Response Fields
| Field | Type | Description |
|---|---|---|
transaction | string | Base64-encoded unsigned VersionedTransaction |
quoteId | string | Quote identifier — required for submission (30s TTL) |
route | object | Selected route details |
estimatedOutput | string | Expected output in smallest units |
minOutput | string | Minimum output after slippage |
feeLamports | string | Venum fee in input token units (currently always "0") |
feeBps | number | Fee rate in basis points (currently always 0) |
computeUnits | number | Allocated compute units (real if simulated, hardcoded per-DEX otherwise) |
simulated | boolean | Whether computeUnits was computed via simulateTransaction or estimated |
Binary Example
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:
SetComputeUnitLimit— sized per DEX type (200K-600K CU)- ATA creation (if
createAtaIfMissing: trueand account missing) — idempotentcreateAssociatedTokenAccountfor input/output tokens. Always idempotent so the same tx is safe to retry. - Native SOL wrap (when
inputMintis SOL) —createAssociatedTokenAccountIdempotentfor the user's WSOL account, thenSystemProgram.transferofamountlamports into it, thenSyncNativeso the SPL token balance reflects the deposit. No-op if the input is any other mint. - Output WSOL ATA creation (when
outputMintis SOL) — idempotentcreateAssociatedTokenAccountfor the user's WSOL account so the swap has a validoutput_token_accountto write into. Without this, swaps to native SOL fail withAccountNotInitialized(Anchor 3012) on Raydium CLMM and similar DEXes. - Swap instruction(s) — DEX-specific swap (may include tick/bin array init)
- Native SOL unwrap (when
outputMintis SOL) —closeAccounton 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
| Status | Description |
|---|---|
400 | Invalid request body |
401 | Missing or invalid API key |
400 | Invalid request body, or simulate: true and every candidate route failed simulation. |
404 | No 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". |
429 | Rate limit exceeded |
503 | Blockhash not available (retry in a moment) |
Rate Limit
| Tier | Limit |
|---|---|
| free | 5 / min |
| starter | 30 / min |
| pro | 150 / min |
