> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dexpaprika.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Real-Time Swap Transactions Streaming

> Stream individual DEX swaps over SSE with USD amounts already attached. Per-pool or per-token, 36 chains, no log decoding and no node.

Every swap on a pool, pushed the moment it settles, with both legs priced in USD before it reaches you. No node, no `Swap` event decoding, no ABI per DEX.

This page is the working reference: how to subscribe, what each field means, what the error codes are, the three things that will trip you up, and what it costs. Endpoint reference is at [GET /sse/transactions](/streaming/stream-real-time-transactions-for-a-single-asset) and [POST /sse/transactions](/streaming/subscribe-to-multiple-transactions-streaming).

<Warning>
  **This feed requires an API key.** Unlike `/sse/prices`, which streams 36 showcase assets keylessly, transactions and reserves are key-only. Without one you get `403` and a JSON body, on a connection you expected to stay open.

  A [free key](https://console.dexpaprika.com) takes a minute, needs no card, and opens all three feeds on every token we index.
</Warning>

## Before anything else: pick your method carefully

Two methods, and the difference between them is three orders of magnitude in cost.

| `method` | Subscribes to                    | Measured rate                                  |
| -------- | -------------------------------- | ---------------------------------------------- |
| `pool`   | one pool                         | **93 events/min** on Orca SOL/USDC             |
| `token`  | *every pool the token trades in* | **19,326 events/min** on SOL, across 531 pools |

Both figures were measured on 2026-08-21 in the same hour. `token` on a major asset is 208 times the traffic of a single pool, because a major asset is in thousands of pools.

Since [each delivered event costs one credit](/knowledge-base/credit-usage), that rate is your bill:

| Subscription       | Credits/min | A free key's 300,000 lasts | Pro's 5,000,000 lasts |
| ------------------ | ----------- | -------------------------- | --------------------- |
| `pool` on SOL/USDC | 93          | 2.2 days                   | 37 days               |
| `token` on SOL     | 19,326      | **15.5 minutes**           | **4 hours 19 min**    |

<Warning>
  `method: "token"` on a top-20 asset will exhaust a free key over lunch. Use it for long-tail tokens that trade in a handful of pools, or when you genuinely need cross-pool coverage and have budgeted for it. For one venue, use `pool`.
</Warning>

## Subscribe

Both forms need `Authorization` carrying the key as the **entire** header value. No `Bearer`, no other scheme word.

<CodeGroup>
  ```bash GET, one subscription theme={null}
  curl -N -H "Authorization: $DEXPAPRIKA_API_KEY" \
    "https://streaming.dexpaprika.com/sse/transactions?method=pool&chain=solana&address=Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE"
  ```

  ```bash POST, up to 25 theme={null}
  curl -N -X POST "https://streaming.dexpaprika.com/sse/transactions" \
    -H "Authorization: $DEXPAPRIKA_API_KEY" \
    -H "Accept: text/event-stream" \
    -H "Content-Type: application/json" \
    -d '[
      {"chain":"solana","address":"Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE","method":"pool","request_id":1},
      {"chain":"ethereum","address":"0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640","method":"pool","request_id":2}
    ]'
  ```
</CodeGroup>

`request_id` is echoed on every event so you can tell subscriptions apart on a multiplexed connection. It is optional and defaults to the array index, which is fine until you reorder the array. Set it explicitly.

Add `?limit=N` to close the connection after N events. It is the cheapest way to sample a feed before you commit to it.

## What an event looks like

A real capture from the Orca SOL/USDC pool:

```
event: pool
request_id: 1
data: {"chain":"solana","pool_id":"Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE","block_number":"440660176","block_timestamp":"2026-08-21T09:49:04Z","tx_hash":"xdEpqxQp4Pp1NNceoaiqkSrc7EYn5zQGNNsobBdsu1au9EiE88TX3GQ1iY1hxsJjkpMq9ufqLd5irVthJSddmPQ","sender":"9TiD2JbKz7tphbs1LzjFR7HbXhVfeRW2vhWVisKqwme","recipient":"AdphhurSRH4euoAasqypvoJLbD8XpuztVSzg2nqmPxcx","token_0":"So11111111111111111111111111111111111111112","token_1":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v","amount_0":4841787021,"amount_1":-445711274,"amount_0_usd":445.8084604429579,"amount_1_usd":445.4707026504548,"price_0_usd":92.07519011253054,"price_1_usd":0.9994602529404603,"volume_usd":225.2765305105}
```

The event name is the method: `pool` or `token`.

| Field                          | Type       | Meaning                                                                   |
| ------------------------------ | ---------- | ------------------------------------------------------------------------- |
| `chain`                        | string     | Network id                                                                |
| `pool_id`                      | string     | The pool the swap happened in. On `method: "token"` this varies per event |
| `block_number`                 | **string** | Block or slot height                                                      |
| `block_timestamp`              | string     | ISO 8601, UTC                                                             |
| `tx_hash`                      | string     | Transaction signature or hash                                             |
| `sender`                       | string     | Address that sent the input token. Often a router, not the trader         |
| `recipient`                    | string     | Address that received the output                                          |
| `token_0`, `token_1`           | string     | Contract addresses. **See the ordering pitfall below**                    |
| `amount_0`, `amount_1`         | **number** | Raw, signed, not decimal-adjusted. **See the precision pitfall below**    |
| `amount_0_usd`, `amount_1_usd` | number     | Each leg's USD value at the block                                         |
| `price_0_usd`, `price_1_usd`   | number     | Per-token USD price at the block                                          |
| `volume_usd`                   | number     | Roughly half the notional. **See the volume pitfall below**               |

**Direction is not a field.** Derive it from the sign of `amount_0`: negative means `token_0` left the pool, so the trader bought it. In the event above `amount_0` is positive, so SOL went in and USDC came out: a sell.

**There are no symbols and no decimals.** REST rows carry `token_0_symbol`; stream events do not. If you need "1.13 WETH" rather than a raw integer, fetch decimals once from [the pool endpoint](/api-reference/pools/get-a-pool-on-a-network) and cache them. The `_usd` fields are already computed, so most consumers never need the raw amounts at all.

## Three pitfalls

These are the ones that cost real time. Each was measured, not inferred.

### 1. `token_0` is not the same token in REST and in the stream

Same pool, same minute, on Ethereum:

```
STREAM  token_0 = 0xa0b8…eb48 (USDC)   price_0_usd = 1.00
REST    token_0 = 0xc02a…6cc2 (WETH)   price_0_usd = 2398.24
```

Both surfaces are internally consistent, so nothing looks wrong. If you join stream events to REST rows by position, you read WETH amounts as USDC and price them at a dollar.

It does not reproduce everywhere. On Solana and on Base the two agree. The stream follows the pool's on-chain ordering; REST puts the major token first; they coincide only when those happen to match, which they do on Base (`0x4200…` sorts below `0x8335…`) and on Solana, and do not on Ethereum (`0xa0b8…` sorts below `0xc02a…`).

<Warning>
  **Resolve tokens by address, never by index.** Do not assume `_0` means the same thing across two DexPaprika surfaces, and do not verify this on Solana only: the bug hides there.
</Warning>

### 2. Raw amounts arrive as JSON numbers, and JavaScript will round them

`amount_0` and `amount_1` are JSON numbers, not strings. For an 18-decimal token they exceed `Number.MAX_SAFE_INTEGER` routinely:

```js theme={null}
JSON.parse('{"amount_1":32267156849925919299}').amount_1
// 32267156849925920000   <- 701 wei gone, silently
```

The [reserves feed](/streaming/reserves-streaming) sends `reserve`, `delta` and `block` as strings precisely to avoid this. The transactions feed does not.

This also hides on Solana. Nine-decimal SOL and six-decimal USDC keep raw amounts small: across 93 captured Solana events the largest absolute amount was 90,504,206,102, four orders of magnitude below the limit. Test on an 18-decimal EVM token or you will not see it.

If you need exact raw amounts, parse with a big-integer-aware reader rather than `JSON.parse`, or work from the `_usd` fields, which are ordinary floats and safe.

`block_number` is already a string for the same reason. Use `Number()` on it only for comparisons, never for arithmetic you care about.

### 3. `volume_usd` is about half the notional

Measured across both chains, `volume_usd` sits at 0.4999 to 0.5057 of each leg's USD value:

```
amount_0_usd = 445.81   amount_1_usd = 445.47   volume_usd = 225.28
```

The ratio is stable within a pool and differs slightly between pools, so treat it as "one side of the trade" rather than a formula. The practical consequence: do not sum `volume_usd` and compare it against a REST 24h volume figure without checking the convention, or you will be out by a factor of two.

When you want the size of a trade, `amount_0_usd` and `amount_1_usd` are unambiguous.

## Error codes

Every one of these was produced against the live endpoint. They arrive as an HTTP status with a JSON body, before the stream opens.

| Status | Body                                                                                       | Cause                             | Fix                                                                                                 |
| ------ | ------------------------------------------------------------------------------------------ | --------------------------------- | --------------------------------------------------------------------------------------------------- |
| `403`  | `{"error":"preview_only","tier":"keyless"}`                                                | No key. This feed is not keyless  | [Register a free key](https://console.dexpaprika.com)                                               |
| `401`  | `{"message":"api key verification has failed"}`                                            | Key present and rejected          | Check for a truncated paste. `ApiKey` or `Token` in front of the key also causes this               |
| `400`  | `{"message":"unsupported chain: X (is not supported)"}`                                    | Unknown `chain`                   | Use an id from [GET /networks](/api-reference/networks/get-a-list-of-available-blockchain-networks) |
| `400`  | `{"message":"no valid subscriptions: subscriptions (all provided assets were not found)"}` | Address not indexed on that chain | Check the address, and that it matches the chain                                                    |
| `400`  | `{"message":"invalid subscription: Method (must be one of [pool token])"}`                 | Bad `method`                      | Only `pool` and `token` exist here                                                                  |
| `400`  | `{"message":"at least one subscription is required: subscriptions (is empty)"}`            | `[]` body                         | Send at least one subscription                                                                      |
| `400`  | `{"message":"too many subscriptions: subscriptions (max 25 allowed)"}`                     | More than 25 entries              | Split across connections, up to 10 per IP                                                           |
| `400`  | `{"message":"Bad Request"}`                                                                | Body is not a JSON array          | POST takes an array, even for one subscription                                                      |

<Note>
  **`403` and `401` mean different things and the difference is useful.** `403` is "you sent no key". `401` is "you sent a key and it was rejected". Branch on them: the first needs registration, the second needs a look at the header.
</Note>

## In-stream events

Once the stream is open, HTTP status no longer applies. Watch the event name:

| Event            | Meaning                                                              |
| ---------------- | -------------------------------------------------------------------- |
| `pool` / `token` | A swap. Which name you get matches the `method` you asked for        |
| `ping`           | Heartbeat, `{"time":<unix>}`, **every 15 seconds**, measured         |
| `warning`        | A non-fatal notice about one subscription. The connection stays open |
| `error`          | A stream-level failure                                               |

Filter on the event name. A handler that parses every `data:` line will try to read `{"time":1787305761}` as a swap.

## Good practices

**Trust the heartbeat, not the socket.** A TCP connection that has silently died looks identical to a quiet pool. Pings arrive every 15 seconds; if you have seen nothing for 45, reconnect. Do not use "no swaps" as a liveness signal, because on a slow pool that is the normal state.

**Reconnect with backoff, and never in a tight loop.** A retry storm turns one rate limit into a persistent one. Start at a second, double up to a minute, and add jitter so a fleet does not resynchronise.

**Multiplex deliberately.** 25 subscriptions per connection, 10 concurrent connections per IP, so 250 subscriptions is the ceiling on a free key. Group by how you will process the data, not by chain: `request_id` is what routes an event, and it is per subscription.

**Deduplicate on `tx_hash` plus `pool_id`.** A reconnect can redeliver the block you were in the middle of. One swap that touches two subscribed pools also arrives twice, once per `request_id`, which is correct but is not what a naive counter expects.

**Size the cost before you leave it running.** Use `?limit=100` and time it. A hundred events tells you the rate, the rate tells you the monthly bill, and both take a minute to find out. [Plan your credit usage](/knowledge-base/credit-usage) works through the arithmetic.

**Persist what you need at write time.** The feed is a live tail, not a queryable history. For backfill and for anything older than your connection, use [the REST transactions endpoint](/api-reference/pools/get-transactions-of-a-pool-on-a-network-paging-can-be-used-up-to-100-pages).

## A working consumer

Handles the event names, the heartbeat, reconnection, and the two pitfalls that bite in JavaScript.

```js theme={null}
// swap-tail.mjs: Node 18+, no dependencies. Needs DEXPAPRIKA_API_KEY.
const KEY = process.env.DEXPAPRIKA_API_KEY;
const SUBS = [
  { chain: "solana", address: "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE", method: "pool", request_id: 1 },
];

// Tokens are resolved by ADDRESS, never by index: _0 is not the same token
// across REST and streaming for some EVM pools.
const WATCH = {
  So11111111111111111111111111111111111111112: "SOL",
  EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: "USDC",
};

const seen = new Set();
let lastBeat = Date.now();

async function tail() {
  const res = await fetch("https://streaming.dexpaprika.com/sse/transactions", {
    method: "POST",
    // The key is the whole Authorization value. No "Bearer".
    headers: {
      authorization: KEY,
      accept: "text/event-stream",
      "content-type": "application/json",
    },
    body: JSON.stringify(SUBS),
  });

  if (!res.ok) {
    // 403 = no key, 401 = key rejected, 400 = the subscription is wrong.
    // Only the last one is worth retrying after a change.
    throw new Error(`${res.status} ${await res.text()}`);
  }

  const decoder = new TextDecoder();
  let buffer = "";

  for await (const chunk of res.body) {
    buffer += decoder.decode(chunk, { stream: true });
    const frames = buffer.split("\n\n");
    buffer = frames.pop() ?? "";

    for (const frame of frames) {
      const name = frame.match(/^event: (.+)$/m)?.[1];
      const data = frame.match(/^data: (.+)$/m)?.[1];
      if (!name || !data) continue;

      if (name === "ping") { lastBeat = Date.now(); continue; }
      if (name === "warning") { console.warn("[warning]", data); continue; }
      if (name === "error") { throw new Error(`stream error: ${data}`); }
      if (name !== "pool" && name !== "token") continue;

      const s = JSON.parse(data);

      // A reconnect can redeliver, and one swap can match two subscriptions.
      const id = `${s.tx_hash}:${s.pool_id}`;
      if (seen.has(id)) continue;
      seen.add(id);

      // amount_0/amount_1 are raw JSON numbers and lose precision above
      // 2^53 on 18-decimal tokens. The _usd fields are ordinary floats.
      const sold = s.amount_0 > 0 ? WATCH[s.token_0] : WATCH[s.token_1];
      const bought = s.amount_0 > 0 ? WATCH[s.token_1] : WATCH[s.token_0];
      const size = Math.abs(s.amount_0 > 0 ? s.amount_0_usd : s.amount_1_usd);

      console.log(
        `${s.block_timestamp}  ${sold} -> ${bought}  $${size.toFixed(2)}  ${s.tx_hash.slice(0, 12)}`,
      );
    }
  }
}

// Watchdog: pings every 15s, so 45s of silence means the socket is gone,
// not that the pool is quiet.
setInterval(() => {
  if (Date.now() - lastBeat > 45_000) {
    console.error("no heartbeat, exiting for the supervisor to restart");
    process.exit(1);
  }
}, 5_000);

let delay = 1_000;
for (;;) {
  try {
    await tail();
    delay = 1_000;
  } catch (err) {
    console.error(String(err));
    if (/^4(0[013])/.test(String(err))) process.exit(1); // fix the request, do not retry
    await new Promise((r) => setTimeout(r, delay + Math.random() * 500));
    delay = Math.min(delay * 2, 60_000);
  }
}
```

## Where to go next

<CardGroup cols={2}>
  <Card title="Endpoint reference" icon="code" href="/streaming/stream-real-time-transactions-for-a-single-asset">
    Parameters, schemas and every event type.
  </Card>

  <Card title="Plan your credit usage" icon="calculator" href="/knowledge-base/credit-usage">
    What a continuous stream costs, with worked models.
  </Card>

  <Card title="Production streaming" icon="shield-check" href="/streaming/guides/production-reserve-streaming">
    Reconnection, missed blocks and error classification. Written for reserves, applies here.
  </Card>

  <Card title="Pool reserves" icon="droplet" href="/streaming/reserves-streaming">
    The other key-only feed: liquidity movement per block.
  </Card>
</CardGroup>
