DEV Community

Cover image for Building a Token Swap on Solana in 50 Lines of TypeScript (Using DFlow's JIT Routing)
Vincent Vego
Vincent Vego

Posted on

Building a Token Swap on Solana in 50 Lines of TypeScript (Using DFlow's JIT Routing)

I spent last week integrating every major swap API on Solana into a side project. Jupiter, Raydium directly, a couple of smaller ones. Then I tried DFlow — and it's the one I kept.
Not because of branding or docs (though the docs are solid). Because of one technical decision that changes everything: JIT routing.
This post walks through what JIT routing actually is, why it produces better results than pre-computed routing, and how to build a working token swap in ~50 lines of TypeScript.

The Problem With Pre-Computed Routes
Every swap aggregator on Solana works roughly the same way:

You request a quote → the API computes the best route across liquidity venues
You receive a serialized transaction with that route baked in
You sign and submit
The transaction lands on-chain 400ms–2s later

The issue is step 4. Between when the route was computed and when your transaction executes, the market has moved. Pools rebalance, other trades land, arbitrage bots reposition. The route that was optimal at quote time is stale at execution time.
This is why the price you see is never the price you get. The gap is small on each trade but compounds fast.
JIT (Just-In-Time) routing solves this by deferring route optimization to the moment of on-chain execution — not quote time. The route is computed and executed atomically. No stale data. No gap.
DFlow's entire Trading API is built around this. Every swap, every trade, every fill goes through the JIT engine.

What We're Building
A simple TypeScript script that:

Connects to a Solana wallet
Swaps SOL → USDC through DFlow's Swap API
Uses declarative mode (intent-based, protocol handles routing)
Streams real-time market data via WebSocket

If you want the full picture of DFlow's products before diving into code, dflow-trade.com has a good overview — Swap API, JIT Routing, Prediction Markets, and the new MCP tooling for AI agents.

Prerequisites
bashnpm init -y
npm install @solana/web3.js@1 bs58 dotenv
Create a .env file:
envDFLOW_API_KEY=your_api_key_here
RPC_URL=https://api.mainnet-beta.solana.com
WALLET_PRIVATE_KEY=your_base58_private_key

API key: contact api@dflow.net or check DFlow docs for access.

Step 1: The Swap Function
Here's the core swap — request a quote, deserialize the transaction, sign, send:
typescript// swap.ts
import { Connection, Keypair, VersionedTransaction } from "@solana/web3.js";
import bs58 from "bs58";
import "dotenv/config";

// Config
const API_BASE = "https://quote-api.dflow.net";
const API_KEY = process.env.DFLOW_API_KEY!;
const connection = new Connection(process.env.RPC_URL!);
const wallet = Keypair.fromSecretKey(bs58.decode(process.env.WALLET_PRIVATE_KEY!));

// Token mints
const SOL = "So11111111111111111111111111111111111111112";
const USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";

async function swap(
inputMint: string,
outputMint: string,
amountLamports: string,
slippageBps: string = "50"
) {
// 1. Request optimized quote
const params = new URLSearchParams({
inputMint,
outputMint,
amount: amountLamports,
slippageBps,
userPublicKey: wallet.publicKey.toBase58(),
});

const response = await fetch(${API_BASE}/order?${params}, {
headers: { "x-api-key": API_KEY },
});

if (!response.ok) {
throw new Error(Quote failed: ${response.status} ${await response.text()});
}

const order = await response.json();

// 2. Deserialize and sign
const tx = VersionedTransaction.deserialize(
Buffer.from(order.transaction, "base64")
);
tx.sign([wallet]);

// 3. Send with preflight checks
const signature = await connection.sendRawTransaction(tx.serialize(), {
skipPreflight: false,
maxRetries: 3,
});

// 4. Confirm
const confirmation = await connection.confirmTransaction(signature, "confirmed");

if (confirmation.value.err) {
throw new Error(Transaction failed: ${JSON.stringify(confirmation.value.err)});
}

return {
signature,
inputAmount: amountLamports,
expectedOutput: order.expectedOutput,
priceImpact: order.priceImpact,
};
}

// Execute: swap 0.1 SOL → USDC
swap(SOL, USDC, "100000000")
.then((result) => {
console.log("Swap successful!");
console.log(Signature: ${result.signature});
console.log(Expected output: ${result.expectedOutput});
console.log(Price impact: ${result.priceImpact}%);
})
.catch(console.error);
Run it:
bashnpx tsx swap.ts
That's it. ~50 lines from zero to executed swap. The JIT routing, MEV protection, and multi-venue aggregation all happen inside the API — your code just sends intent and receives a result.

Step 2: Real-Time Market Data via WebSocket
DFlow provides a WebSocket endpoint for streaming live market updates. This is useful for building trading UIs, monitoring bots, or triggering conditional swaps.
typescript// stream.ts

const DFLOW_WS = "wss://api.prod.dflow.net/ws";

function startStream(ticker: string) {
const ws = new WebSocket(DFLOW_WS);

ws.onopen = () => {
console.log(Connected. Subscribing to ${ticker}...);
ws.send(JSON.stringify({
action: "subscribe",
channel: "market",
ticker,
}));
};

ws.onmessage = (event) => {
const data = JSON.parse(event.data.toString());

switch (data.type) {
  case "price_update":
    console.log(`[${ticker}] Price: ${data.price} | Volume: ${data.volume}`);
    break;
  case "orderbook_update":
    console.log(`[${ticker}] Book depth: ${data.bids?.length} bids, ${data.asks?.length} asks`);
    break;
  case "trade":
    console.log(`[${ticker}] Trade: ${data.side} ${data.amount} @ ${data.price}`);
    break;
  default:
    console.log(`[${ticker}] ${data.type}:`, data);
}
Enter fullscreen mode Exit fullscreen mode

};

ws.onerror = (err) => console.error("WebSocket error:", err);

ws.onclose = () => {
console.log("Disconnected. Reconnecting in 3s...");
setTimeout(() => startStream(ticker), 3000);
};
}

startStream("SOL-USDC");
Auto-reconnect included. In production you'd add exponential backoff and a message queue, but this covers the basics.

Step 3: Declarative vs Imperative — When to Use Which
DFlow supports two swap modes. Understanding the difference matters for your architecture.
Imperative Swaps
You request quote → API returns a specific route → You sign that exact route
Use when: your UI needs to show the user exactly which venues their trade goes through. Think "transparency mode" — the user sees "50% through Venue A, 50% through Venue B" before signing.
Trade-off: the route is frozen at signature time. If conditions change between signing and landing, you get a worse fill or a failed tx.
Declarative Swaps
You submit intent ("swap X for Y, min output Z") → Protocol optimizes at execution
Use when: you want the best possible execution and don't need to display route details. This is what trading bots, backend services, and high-frequency apps should use.
Advantage: route adapts to real-time conditions at the moment of finality. Fewer failed transactions. Better fills. And because no pre-computed route is broadcast, sandwich bots can't front-run your trade — that's MEV protection built into the architecture.
For most applications, declarative is the better default. Use imperative only when route transparency is a product requirement.

Bonus: Prediction Markets in 20 Lines
DFlow also tokenizes Kalshi prediction markets on Solana. Here's how you'd fetch available markets and check prices:
typescript// predictions.ts
const PRED_API = "https://prediction-markets-api.dflow.net";

async function getMarkets(category: string = "sports") {
const response = await fetch(${PRED_API}/markets?category=${category}, {
headers: { "x-api-key": process.env.DFLOW_API_KEY! },
});
return response.json();
}

async function getPrice(marketId: string) {
const response = await fetch(${PRED_API}/markets/${marketId}/price, {
headers: { "x-api-key": process.env.DFLOW_API_KEY! },
});
return response.json();
}

// List sports markets and check first one
getMarkets("sports").then(async (markets) => {
console.log(Found ${markets.length} markets\n);

for (const m of markets.slice(0, 5)) {
const price = await getPrice(m.id);
console.log(${m.title});
console.log(YES: $${price.yes} | NO: $${price.no});
console.log(Closes: ${m.closeTime}\n);
}
});
Each position is a real SPL token. You can hold it in Phantom, transfer it, or build composable strategies on top. When the event resolves, winning tokens are redeemed for USDC.
Kalshi is CFTC-regulated, so these aren't offshore prediction bets — they're federally regulated event contracts tokenized on Solana. Kalshi backed the ecosystem with a $2M grants program for builders.

Architecture Recap
Here's how everything connects:
Your App

├─→ Swap API ──→ JIT Routing Engine ──→ Liquidity Venues
│ (real-time optimization + MEV protection)

├─→ Prediction Markets API ──→ CLPs ──→ Kalshi (off-chain)
│ (CFTC-regulated)

└─→ WebSocket Stream ──→ Live market data
One API. Three product surfaces. All non-custodial, all on Solana.

Resources
WhatWhereProduct overview + FAQdflow-trade.comAPI docspond.dflow.netSecurity auditsdflow-trade.com/#securityTwitter@DFlowProtocolAPI key requestapi@dflow.net

Takeaways
If you're building anything that involves token swaps on Solana, the execution layer you choose matters more than most people realize. The difference between pre-computed routing and JIT routing is the difference between "close enough" and "actually optimal."
DFlow handles routing, MEV protection, and settlement — you handle the product. That's a good split.
Full product breakdown, security audits, and a 30-question FAQ at dflow-trade.com.

Have questions or building something on DFlow? Drop a comment — happy to dig into the technical details.

Top comments (0)