All Resources
Jupiter DCA API: Track Recurring Orders and Live Fills
Data APITypeScript11 min readSolana Tracker

Jupiter DCA API: Track Recurring Orders and Live Fills

Jupiter DCA API in TypeScript: query recurring orders by wallet, token or pair, page with cursors, and stream fills, opens and closes over WebSocket.

  • data-api,
  • dca,
  • jupiter,
  • datastream,
  • typescript

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.

QuestionUseNotes
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 SOLgetDcaPair(inputMint, outputMint, params)Direction matters
One DCA accountgetDcaOrder(address)Address is the DCA account, not the owner
Show cycles as they executeDatastream subscribe.dca.*Premium plan or higher
Every swap on the token, DCA or notTrade history or trade roomsSee 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 methodREST routeDefault / max limit
getDcaWallet(wallet, params)GET /dca/wallet/{wallet}50 / 100
getDcaWalletOrders(wallet, params)GET /dca/wallet/{wallet}/orders50 / 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}/buyers200 / 1000
getDcaTokenSellers(mint, params)GET /dca/token/{mint}/sellers200 / 1000
getDcaTokenUsers(mint, { program, limit })GET /dca/token/{mint}/users200 / 1000
getDcaPair(inputMint, outputMint, params)GET /dca/pair/{inputMint}/{outputMint}50 / 100

List endpoints share these parameters:

  • status: active, paused, completed, pending, or all (default all).
  • sort (alias sortBy): volume (default), deposited, remaining, progress, recent, created, status.
  • cursor: the previous page's last order address, returned as pagination.nextCursor. It is only valid with the same sort, status, and program; anything else returns 400 INVALID_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.

SubscriptionRoom
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. remaining is 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 Filled events only, and replace stored order state from snapshots, keeping the newest by slot and writeVersion.
  • Expect duplicates across rooms. If you subscribe to dca.all() and dca.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 sort or status between pages invalidates the cursor.
  • Raw amounts need decimals. Stream amounts are base-unit strings. Use the decimals from 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

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

Runnable Node.js project — clone from GitHub, add keys to .env, then npm start. Use a private local environment for credentials. Check the companion package version before running.

Build on this example

Ask for changes, a different stack or the next feature. Answers use the Data API SDK and link the docs.