The Jupiter DCA API in Solana Tracker's Data API returns indexed state for Jupiter recurring (DCA) orders by wallet, token, trading pair, or order address, and Datastream pushes each open, fill, deposit, withdrawal, and close as it happens. Use REST to answer "what orders exist and how far along are they", and the WebSocket rooms to see cycles execute. It is a read-only data API: it does not create, change, or execute orders.
This guide covers the endpoints, the order shape, the live event types, and a TypeScript client that combines them, using @solana-tracker/data-api 0.5.0 and the Solana Data API.
When to use the Jupiter DCA API
A recurring order is a funded account that spends a fixed input amount per cycle into an output token. Its state (deposited, used, remaining, next cycle) changes slowly; its fills arrive on a schedule. Those two things need different tools.
| Question | Use | Notes |
|---|---|---|
| How many DCA orders are buying or selling this token? | getDcaTokenFlow(mint) | Counts and USD volume per side |
| Which orders are buying this token, largest first? | getDcaTokenBuyers(mint, params) | Token is the output mint; paginated |
| Which orders are selling it? | getDcaTokenSellers(mint, params) | Token is the input mint |
| What is this wallet running? | getDcaWallet(wallet, params) | Status summary plus orders |
| Orders for one exact direction, such as USDC into SOL | getDcaPair(inputMint, outputMint, params) | Direction matters |
| One DCA account | getDcaOrder(address) | Address is the DCA account, not the owner |
| Show cycles as they execute | Datastream subscribe.dca.* | Premium plan or higher |
| Every swap on the token, DCA or not | Trade history or trade rooms | See the trades WebSocket guide |
If you need raw program instructions for something the index does not cover, Yellowstone gRPC gives you the transactions and leaves decoding to you.
Query recurring orders with the Jupiter DCA API
All REST methods take the program as a parameter. getDcaPrograms() returns the supported programs; today that is one entry, { id: "jupiter", label: "Jupiter", programId: "DCA265Vj8a9CEuX1eb1LWRnDT7uK6q1xMipnNyatn23M" }. program defaults to jupiter, and dex or platform are accepted as aliases.
| SDK method | REST route | Default / max limit |
|---|---|---|
getDcaWallet(wallet, params) | GET /dca/wallet/{wallet} | 50 / 100 |
getDcaWalletOrders(wallet, params) | GET /dca/wallet/{wallet}/orders | 50 / 100 |
getDcaOrder(address, { program }) | GET /dca/order/{address} | n/a |
getDcaTokenFlow(mint, params) | GET /dca/token/{mint} | n/a |
getDcaTokenBuyers(mint, params) | GET /dca/token/{mint}/buyers | 200 / 1000 |
getDcaTokenSellers(mint, params) | GET /dca/token/{mint}/sellers | 200 / 1000 |
getDcaTokenUsers(mint, { program, limit }) | GET /dca/token/{mint}/users | 200 / 1000 |
getDcaPair(inputMint, outputMint, params) | GET /dca/pair/{inputMint}/{outputMint} | 50 / 100 |
List endpoints share these parameters:
status:active,paused,completed,pending, orall(defaultall).sort(aliassortBy):volume(default),deposited,remaining,progress,recent,created,status.cursor: the previous page's last orderaddress, returned aspagination.nextCursor. It is only valid with the samesort,status, andprogram; anything else returns 400INVALID_CURSOR.
Status values mean specific things. pending is funded but the first cycle has not run. active has fills and is waiting for, or due on, the next cycle. paused is paused on-chain or overdue well past twice its cycle frequency. completed has no remaining input.
Response shape
Paginated endpoints return orders plus a pagination block. A trimmed getDcaWallet response from the docs example:
{
"wallet": "2FbbdMWfrfGpyyK4Wh76vyxbmDGcp3LxZNhwBwFpV5UK",
"summary": { "total": 1, "active": 1, "paused": 0, "completed": 0, "pending": 0 },
"orders": [
{
"program": "jupiter",
"address": "125PQmudTAXHUUGxtSra1xTzGu3NRCMbRU7i9cikEpwd",
"owner": "2FbbdMWfrfGpyyK4Wh76vyxbmDGcp3LxZNhwBwFpV5UK",
"status": "active",
"direction": "buying",
"pair": "USDC → SOL",
"input": { "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "symbol": "USDC", "decimals": 6, "priceUsd": 1 },
"output": { "mint": "So11111111111111111111111111111111111111112", "symbol": "SOL", "decimals": 9, "priceUsd": 142.5 },
"deposited": 1000, "depositedUsd": 1000,
"used": 200, "remaining": 800, "remainingUsd": 800,
"received": 5.2, "receivedUsd": 741,
"perCycle": 50, "frequency": "1h", "progressPercent": 20,
"createdAt": "2024-01-01T00:00:00.000Z",
"nextCycleAt": "2024-01-02T00:00:00.000Z",
"raw": { "deposited": "1000000000", "used": "200000000" }
}
],
"pagination": { "limit": 50, "nextCursor": null, "hasMore": false, "count": 1, "total": 1 }
}
Amounts are in token units; raw holds base-unit strings whose keys depend on the program. Every *Usd field can be null or missing when no price is available, so keep that distinct from zero. getDcaTokenFlow returns { mint, buyers: { count, volumeUsd }, sellers: { count, volumeUsd } }. Errors use { error: { code, message } } with codes such as INVALID_MINT, INVALID_CURSOR, INVALID_LIMIT, and TIMEOUT; the SDK surfaces the message on DataApiError and the body on error.details. Owners are bare addresses; to put names on them, see the Solana wallet identity API.
Stream DCA events over Datastream
The subscribe.dca namespace maps to these rooms. Each event goes to the global room, its type room, and every scoped room it matches, so overlapping subscriptions deliver the same action more than once.
| Subscription | Room |
|---|---|
dca.all() | dca:jupiter |
dca.opened() / filled() / closed() | dca:jupiter:opened and so on |
dca.deposit() / withdraw() / collectedFee() | dca:jupiter:deposit, :withdraw, :collected_fee |
dca.position() | dca:jupiter:position |
dca.token(mint).buyers() / .sellers() | dca:jupiter:{mint}:buyers / :sellers |
dca.wallet(owner) | dca:jupiter:wallet:{owner} |
dca.order(address) | dca:jupiter:{address} |
Transaction events carry eventName (Opened, Filled, Closed, Deposit, Withdraw, CollectedFee), signature, slot, timestamp in milliseconds, eventIndex, the DCA address, owner, and an order snapshot whose fields are all optional. Raw amounts such as inAmount and outAmount are base-unit strings; USD values sit under usd and may be absent. Opened includes openInstruction (openDca or openDcaV2), so V2 orders arrive in the same rooms.
Position events are different: an account snapshot with writeVersion, the full order, and no signature or eventName. They are throttled per DCA account and also appear in the token, wallet, and order rooms. A snapshot is state, not a fill.
Complete TypeScript example
Install and run:
npm init -y && npm pkg set type=module
npm install @solana-tracker/data-api@^0.5.0
npm install --save-dev tsx typescript @types/node
node --env-file=.env --import tsx index.ts
.env.example:
# Required. Data API key from https://www.solanatracker.io/account/data-api
ST_API_KEY=
# Optional. Datastream key or full wss:// URL (Premium plan or higher)
ST_DATASTREAM_KEY=
# Optional. Token to analyse (default: wrapped SOL)
TOKEN_MINT=So11111111111111111111111111111111111111112
index.ts reads the flow overview, pages through active buy-side orders, then streams the token's buyer room. Keys come from your Data API dashboard. The DCA methods exist in every 0.3.1+ release; if you are upgrading from an older SDK, see the Solana Tracker Data API SDK 0.5 migration notes.
import { Client, DataApiError, Datastream, RateLimitError } from '@solana-tracker/data-api';
import type { DcaListParams, DcaOrder, DcaStreamEvent } from '@solana-tracker/data-api';
const apiKey = process.env.ST_API_KEY?.trim();
if (!apiKey) {
console.error('Missing ST_API_KEY');
process.exit(1);
}
const mint = process.env.TOKEN_MINT?.trim() || 'So11111111111111111111111111111111111111112';
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(mint)) {
console.error('TOKEN_MINT is not a valid Solana address');
process.exit(1);
}
const dsKey = process.env.ST_DATASTREAM_KEY?.trim();
const client = new Client({ apiKey });
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
// The SDK has no timeout or retries. Retry 408 (server timeout), 429, 5xx and network errors.
async function call<T>(label: string, fn: () => Promise<T>, attempts = 4): Promise<T> {
for (let i = 1; ; i++) {
let timer: NodeJS.Timeout | undefined;
try {
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error(`${label} timed out`)), 20_000);
});
return await Promise.race([fn(), timeout]);
} catch (error) {
const status = error instanceof DataApiError ? error.status : undefined;
const retryable = status === undefined || status === 408 || status === 429 || status >= 500;
if (!retryable || i >= attempts) throw error;
const wait = error instanceof RateLimitError && error.retryAfter ? error.retryAfter * 1000 : 500 * 2 ** i;
await sleep(wait);
} finally {
clearTimeout(timer);
}
}
}
const usd = (n: number | null | undefined) => (typeof n === 'number' ? `$${n.toFixed(2)}` : 'n/a');
const orders = new Map<string, DcaOrder>(); // current state by DCA account address
async function loadBuyers(maxPages = 2): Promise<number> {
// Keep program, status and sort fixed: the cursor is only valid with the same values.
const base: DcaListParams = { program: 'jupiter', status: 'active', sort: 'volume', limit: 100 };
let cursor: string | undefined;
let total = 0;
for (let page = 0; page < maxPages; page++) {
const res = await call('buyers', () => client.getDcaTokenBuyers(mint, { ...base, cursor }));
for (const o of res.orders) orders.set(o.address, o);
total = res.pagination.total;
if (!res.pagination.hasMore || !res.pagination.nextCursor) break;
cursor = res.pagination.nextCursor;
}
return total;
}
async function main(): Promise<void> {
const flow = await call('flow', () => client.getDcaTokenFlow(mint));
console.log(`Buyers ${flow.buyers.count} (${usd(flow.buyers.volumeUsd)}), sellers ${flow.sellers.count} (${usd(flow.sellers.volumeUsd)})`);
const total = await loadBuyers();
console.log(`Loaded ${orders.size} of ${total} active buy-side orders`);
for (const o of [...orders.values()].slice(0, 10)) {
console.log(`${o.owner.slice(0, 4)} ${o.pair} ${o.perCycle} ${o.input.symbol}/${o.frequency} ${o.progressPercent}% left ${usd(o.remainingUsd)}`);
}
if (!dsKey) {
console.log('ST_DATASTREAM_KEY not set: skipping the live stream');
return;
}
const ds = new Datastream({
wsUrl: dsKey.startsWith('wss://') ? dsKey : `wss://datastream.solanatracker.io/${dsKey}`,
autoReconnect: true,
reconnectDelay: 1_000,
reconnectDelayMax: 30_000,
});
ds.on('error', () => console.error('Datastream error (check key and plan)'));
const seen = new Set<string>();
const lastWrite = new Map<string, number>(); // DCA address -> newest writeVersion applied
const sub = ds.subscribe.dca.token(mint).buyers();
const listener = sub.on((event: DcaStreamEvent) => {
if (!('eventName' in event)) {
// Position snapshot: replace state, never count it as a fill. Ignore older or repeated writes.
if ((lastWrite.get(event.address) ?? -1) >= event.writeVersion) return;
lastWrite.set(event.address, event.writeVersion);
orders.set(event.address, event.order);
return;
}
const key = `${event.signature}:${event.eventIndex}:${event.eventName}`;
if (seen.has(key)) return;
seen.add(key);
if (event.eventName === 'Filled') {
console.log(`FILL ${event.address.slice(0, 4)} in ${event.inAmount} out ${event.outAmount} (raw units) ${usd(event.usd.inAmountUsd)}`);
} else {
console.log(`${event.eventName.toUpperCase()} ${event.address.slice(0, 4)} owner ${event.owner.slice(0, 4)}`);
}
});
// Events sent while disconnected are not replayed: re-read state from REST after each reconnect.
let connects = 0;
ds.on('connected', () => {
if (++connects > 1) loadBuyers().catch((e) => console.warn('resync failed:', e instanceof Error ? e.message : e));
});
process.once('SIGINT', () => {
listener.unsubscribe();
ds.unsubscribe(sub.room);
ds.disconnect();
process.exit(0);
});
}
main().catch((error) => {
console.error(error instanceof DataApiError ? `HTTP ${error.status ?? '?'}: ${error.message}` : String(error));
process.exit(1);
});
The snippet keeps a seen set for the whole session; a long-running process should cap it, as the companion project does.
Production pitfalls
- Remaining is not demand.
remainingis input the owner can still withdraw at any time. Label any forward projection (remaining divided by per-cycle amount, times frequency) as an estimate that assumes no withdrawals, pauses, or failed cycles. - Buy side alone is not net flow. Compare buyers with sellers, and remember that DCA is one source of flow among many venues.
- Do not count snapshots as fills. Position events have no signature. Count executions from
Filledevents only, and replace stored order state from snapshots, keeping the newest byslotandwriteVersion. - Expect duplicates across rooms. If you subscribe to
dca.all()anddca.token(mint).buyers(), one fill arrives twice. Deduplicate transaction events on signature, event index, and event name. - Retry 408 on token endpoints. Token-level queries can time out server-side and return
TIMEOUT; results may also be cached. Back off and retry rather than failing the page. - Keep the cursor tied to its query. Changing
sortorstatusbetween pages invalidates the cursor. - Raw amounts need decimals. Stream amounts are base-unit strings. Use the
decimalsfrom the order snapshot or REST, and do not parse large values into floats for accounting. - Keep keys server-side. The Datastream URL contains your key; never ship it to a browser or log it.
FAQ
Does the Jupiter DCA API create or cancel orders?
No. It reads indexed order state and events. Order creation and cancellation happen on-chain through the program itself.
Are DCA V2 orders included?
Yes. Both openDca and openDcaV2 opens appear; check openInstruction on Opened events if you need to tell them apart.
What is the difference between buyers and sellers for a token?
Buyers are orders whose output mint is the token. Sellers are orders whose input mint is the token.
Why is a USD field null?
No price was available for that token when the value was computed. Show it as unknown and keep the token-unit amount.
Do I need Datastream for this?
No. The REST methods use your Data API key. Only the live rooms need Datastream, which requires a Premium plan or higher.
How do I follow a single order?
Call getDcaOrder(address) for current state and subscribe to dca.order(address) for its events. Use the DCA account address, not the owner wallet.
References
- Get wallet DCA orders
- Get paginated wallet DCA orders
- Token DCA flow overview
- DCA buyers for a token
- DCA orders for a trading pair
- List DCA programs
- Jupiter DCA all events room
- Buyers by token room
- Position updates room
- @solana-tracker/data-api on npm
Companion project
The full example adds seller-side orders, an optional wallet summary, status and sort validation, aligned tables, exact unit formatting for fills, and position-snapshot ordering. It is at github.com/solanatracker/examples/tree/main/18-jupiter-dca-api-recurring-orders.
cp .env.example .env && npm install && npm start