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

# Track Bags Payout Events

> Read completed Bags payout payments across all tokens for a ticker or bot, or for one token to draw markers on its price chart

When a token pays its fees out through [Bags Payouts](https://docs.bags.fm/how-to-guides/pay-fees-to-x-money.md), each cash payment to the token's handle is recorded as a **payment event**. This guide shows how to read those events with two endpoints: one for every token at once, one for a single token.

## What counts as an event

An event is one **completed end-to-end send**. A Peer buyer paid the handle, the payment was verified, and the buyer's USDC was released. Nothing earlier in the pipeline is an event:

* **Never events:** fee claims, bridges, listed or open orders, and fills that are still pending or were pruned.
* **Several per payout:** one payout can be filled by several buyers, so it can hold several events, one per buyer.
* **Real cash:** `amountUsd` is the exact cash the recipient received, not a price estimate. `usdcAmount` is the raw USDC the buyer received for it, and `discountBps` is the order's discount below market.

```json theme={null}
{
  "paymentId": "0x8f3c2b7e1d4a5f6e9c0b1a2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f",
  "payoutId": "6720f1c2a9b3e45d7f8a1b2c",
  "tokenMint": "CyXBDcVQuHyEDbG661Jf3iHqxyd9wNHhE2SiQdNrBAGS",
  "cashoutMethod": "xmoney",
  "username": "rebeccaperrotto",
  "displayHandle": "@rebeccaperrotto",
  "amountUsd": 786.04,
  "fiatPaidCents": "78604",
  "usdcAmount": "45746116",
  "usdcDecimals": 6,
  "discountBps": 1000,
  "timestamp": 1794040000,
  "isFirstPayment": true
}
```

| Field | Meaning |
| - | - |
| `paymentId` | The Peer intent hash of the fill. It is the same id `GET /bags-payout/payment?id=` takes. |
| `payoutId` | The payout the payment belongs to. Read it with [`GET /bags-payout/payout?id=…`](https://docs.bags.fm/api-reference/get-bags-payout). |
| `displayHandle` | The handle as it should be shown: `@handle` for X Money; Venmo and Cash App names are shown bare. |
| `amountUsd` / `fiatPaidCents` | The cash paid, in dollars and as an integer string of cents. |
| `usdcAmount` / `usdcDecimals` | The raw USDC the buyer received, and its decimals (6). |
| `timestamp` | When the handle was paid, in unix seconds. |
| `isFirstPayment` | This is the first payment to that handle (same method) from this token. |

Feed events also carry `token: { name, symbol, image }`, read from the token's Bags launch. Each field can be `null`.

## Which endpoint to use

| Endpoint | Use it for | Order |
| - | - | - |
| [`GET /bags-payout/feed`](https://docs.bags.fm/api-reference/get-bags-payout-feed) | An "all payouts" ticker, a leaderboard, or a bot that posts every payment. Covers every token and includes token metadata. | Newest first, or largest first with `sort=amount` |
| [`GET /bags-payout/token/events`](https://docs.bags.fm/api-reference/get-bags-payout-token-events) | One token's payments, for example markers on its price chart or a payments tab on its page. | Newest first (offset mode) or oldest first (time mode) |

## Prerequisites

* An API key from the [Bags Developer Portal](https://dev.bags.fm), sent as the `x-api-key` header.
* Node.js 18 or later for the TypeScript examples (they use the built-in `fetch`). See the [TypeScript and Node.js Setup Guide](https://docs.bags.fm/how-to-guides/typescript-node-setup.md).

## Filter by payout method

Both endpoints return **only X Money payments by default** (`method=xmoney`). Pass `method=cashapp` or `method=venmo` for one of the other methods, or `method=all` for every method.

```bash theme={null}
# Every Cash App payment, newest first
curl -H "x-api-key: $BAGS_API_KEY" \
  "https://public-api-v2.bags.fm/api/v1/bags-payout/feed?method=cashapp"

# Every payment of any method
curl -H "x-api-key: $BAGS_API_KEY" \
  "https://public-api-v2.bags.fm/api/v1/bags-payout/feed?method=all"
```

`cashoutMethod` on each event tells you which method it was paid through, which matters with `method=all`.

## The payment feed

### Newest first, paged with `before`

With the default `sort=time`, the feed is ordered newest first. To read older events, pass the last event's `timestamp` as `before`; you get only events older than it.

```bash theme={null}
curl -H "x-api-key: $BAGS_API_KEY" \
  "https://public-api-v2.bags.fm/api/v1/bags-payout/feed?limit=50"

# Next page: before = timestamp of the last event above
curl -H "x-api-key: $BAGS_API_KEY" \
  "https://public-api-v2.bags.fm/api/v1/bags-payout/feed?limit=50&before=1794040000"
```

> [!NOTE]
>
> `before` is in whole seconds and excludes that second, so if two payments share the exact second at a page boundary, the second one is not on the next page. That is rare for payouts. For a gap-free walk of the whole history, page with `sort=amount` and `cursor`, which is exact, or read one token's history in time mode.

### Largest first, paged with `cursor`

With `sort=amount`, events are ordered by cash paid, largest first. Page by passing the response's `nextCursor` back as `cursor`. `nextCursor` is `null` on the last page.

```bash theme={null}
curl -H "x-api-key: $BAGS_API_KEY" \
  "https://public-api-v2.bags.fm/api/v1/bags-payout/feed?sort=amount&limit=20"

curl -H "x-api-key: $BAGS_API_KEY" \
  "https://public-api-v2.bags.fm/api/v1/bags-payout/feed?sort=amount&limit=20&cursor=<NEXT_CURSOR>"
```

> [!WARNING]
>
> The two paging styles do not mix. `before` with `sort=amount` is a `400`, and so is `cursor` with `sort=time` or a cursor that was not returned by the API. Treat the cursor as opaque.

### Amount bounds and first payments

* `minAmountCents` and `maxAmountCents` bound the cash paid, in USD cents, both inclusive. `minAmountCents=10000` keeps payments of \$100 or more. Sending a minimum above the maximum is a `400`.
* `onlyFirstPayments=true` keeps only each handle's first payment per token, which is the moment a token's payout first reached its recipient. It works with both sorts and with the amount bounds: the first payments are picked first, then filtered and sorted.

```bash theme={null}
# First payments of $100 or more, largest first
curl -H "x-api-key: $BAGS_API_KEY" \
  "https://public-api-v2.bags.fm/api/v1/bags-payout/feed?sort=amount&minAmountCents=10000&onlyFirstPayments=true"
```

### TypeScript: read the whole feed

```typescript read-payout-feed.ts theme={null}
const API_BASE = "https://public-api-v2.bags.fm/api/v1";
const API_KEY = process.env.BAGS_API_KEY!;

type PayoutMethod = "xmoney" | "cashapp" | "venmo";

interface PayoutEvent {
    paymentId: string;
    payoutId: string;
    tokenMint: string;
    cashoutMethod: PayoutMethod;
    username: string;
    displayHandle: string;
    amountUsd: number;
    fiatPaidCents: string;
    usdcAmount: string;
    usdcDecimals: number;
    discountBps: number;
    timestamp: number;
    isFirstPayment: boolean;
}

interface PayoutFeedEvent extends PayoutEvent {
    token: { name: string | null; symbol: string | null; image: string | null };
}

interface FeedPage {
    events: PayoutFeedEvent[];
    hasMore: boolean;
    nextCursor: string | null;
}

async function getJson<T>(path: string, params: Record<string, string | number | boolean | undefined>): Promise<T> {
    const url = new URL(`${API_BASE}${path}`);
    for (const [key, value] of Object.entries(params)) {
        if (value !== undefined) url.searchParams.set(key, String(value));
    }
    const res = await fetch(url, { headers: { "x-api-key": API_KEY } });
    const body = await res.json();
    if (!body.success) {
        // 400 for invalid parameters, 429 when rate limited, 500 on server errors
        throw new Error(`${path} failed (${res.status}): ${body.response}`);
    }
    return body.response as T;
}

/** Newest first, paged with `before`. */
async function* feedByTime(filters: { method?: PayoutMethod | "all"; onlyFirstPayments?: boolean } = {}) {
    let before: number | undefined;
    while (true) {
        const page = await getJson<FeedPage>("/bags-payout/feed", { ...filters, sort: "time", limit: 100, before });
        yield* page.events;
        if (!page.hasMore || page.events.length === 0) return;
        before = page.events[page.events.length - 1].timestamp;
    }
}

/** Largest first, paged with `cursor`. */
async function* feedByAmount(filters: { method?: PayoutMethod | "all"; minAmountCents?: number; maxAmountCents?: number } = {}) {
    let cursor: string | undefined;
    while (true) {
        const page = await getJson<FeedPage>("/bags-payout/feed", { ...filters, sort: "amount", limit: 100, cursor });
        yield* page.events;
        if (!page.hasMore || !page.nextCursor) return;
        cursor = page.nextCursor;
    }
}

// The ten largest X Money payments so far
let rank = 0;
for await (const event of feedByAmount()) {
    console.log(`${++rank}. $${event.amountUsd.toFixed(2)} to ${event.displayHandle} from $${event.token.symbol ?? event.tokenMint}`);
    if (rank === 10) break;
}
```

### TypeScript: a live payments bot

For a ticker or a bot that posts each payment once, poll the first page of the feed and remember which `paymentId`s you have already handled. This reuses `getJson` and the types from `read-payout-feed.ts` above.

> [!TIP]
>
> Deduplicate by `paymentId`, not by timestamp. An event only appears once its fill is released, and `timestamp` is when the handle was paid, so a payment can show up after events with a later timestamp. A "newer than the last timestamp I saw" check can skip it.

```typescript payout-bot.ts theme={null}
const seen = new Set<string>();

async function poll(post: (event: PayoutFeedEvent) => Promise<void>) {
    const page = await getJson<FeedPage>("/bags-payout/feed", { limit: 100 });
    // Oldest of the new ones first, so posts come out in order
    for (const event of [...page.events].reverse()) {
        if (seen.has(event.paymentId)) continue;
        seen.add(event.paymentId);
        await post(event);
    }
}

async function main() {
    // Mark what is already in the feed as seen so the bot does not replay history on start
    const first = await getJson<FeedPage>("/bags-payout/feed", { limit: 100 });
    for (const event of first.events) seen.add(event.paymentId);

    while (true) {
        try {
            await poll(async (event) => {
                const tag = event.isFirstPayment ? " (first payment)" : "";
                console.log(`${event.displayHandle} was paid $${event.amountUsd.toFixed(2)} from $${event.token.symbol ?? "?"}${tag}`);
            });
        } catch (error) {
            console.error(error); // back off on 429 and retry
        }
        await new Promise((resolve) => setTimeout(resolve, 30_000));
    }
}

main();
```

Persist `seen` (for example in a file or database) if the bot restarts, and keep only recent ids so it does not grow forever.

## One token's events

### Offset mode: newest first

The default mode pages with `limit` (1 to 100, default 100) and `offset` (default 0), newest first. `hasMore` is `true` when more events exist past the page.

```bash theme={null}
curl -H "x-api-key: $BAGS_API_KEY" \
  "https://public-api-v2.bags.fm/api/v1/bags-payout/token/events?tokenMint=<TOKEN_MINT>&limit=20&offset=0"
```

### Time mode: a chart range

With `mode=time`, pass `from` and `to` in unix seconds. Both are inclusive and required, and `from` must not be after `to`. Events come back **oldest first**, which is the order a chart expects, up to 1000 events per request. `hasMore` is `true` when the range holds more than 1000.

```bash theme={null}
curl -H "x-api-key: $BAGS_API_KEY" \
  "https://public-api-v2.bags.fm/api/v1/bags-payout/token/events?tokenMint=<TOKEN_MINT>&mode=time&from=1793000000&to=1794100000"
```

> [!NOTE]
>
> Each mode only accepts its own parameters. `limit` or `offset` with `mode=time`, or `from` or `to` in offset mode, is a `400`.

### TypeScript: payment markers on a price chart

This example draws one marker per candle that had payments on a [TradingView Lightweight Charts](https://tradingview.github.io/lightweight-charts/) candlestick chart (v5). It reads the chart's range in time mode, follows `hasMore` past the 1000-event cap, and highlights candles with a first payment. It reuses `getJson` and `PayoutEvent` from `read-payout-feed.ts` above.

```typescript payout-markers.ts theme={null}
import { createChart, createSeriesMarkers, CandlestickSeries, type SeriesMarker, type UTCTimestamp } from "lightweight-charts";

interface TokenEventsPage {
    events: PayoutEvent[];
    hasMore: boolean;
}

/** Every event in [from, to], oldest first. Follows `hasMore` past the 1000-event cap. */
async function getTokenEventsInRange(tokenMint: string, from: number, to: number, method: PayoutMethod | "all" = "xmoney"): Promise<PayoutEvent[]> {
    const events: PayoutEvent[] = [];
    const ids = new Set<string>();
    while (from <= to) {
        const page = await getJson<TokenEventsPage>("/bags-payout/token/events", { tokenMint, mode: "time", from, to, method });
        for (const event of page.events) {
            if (ids.has(event.paymentId)) continue; // `from` is inclusive, so the boundary second repeats
            ids.add(event.paymentId);
            events.push(event);
        }
        if (!page.hasMore || page.events.length === 0) break;
        const last = page.events[page.events.length - 1].timestamp;
        if (last === from) break; // more than 1000 events in one second; stop instead of looping
        from = last;
    }
    return events;
}

/** One marker per candle: payments in the same candle are summed. */
function toMarkers(events: PayoutEvent[], candleSeconds: number): SeriesMarker<UTCTimestamp>[] {
    const buckets = new Map<number, { totalUsd: number; count: number; hasFirst: boolean }>();
    for (const event of events) {
        const time = Math.floor(event.timestamp / candleSeconds) * candleSeconds;
        const bucket = buckets.get(time) ?? { totalUsd: 0, count: 0, hasFirst: false };
        bucket.totalUsd += Number(event.fiatPaidCents) / 100;
        bucket.count += 1;
        bucket.hasFirst ||= event.isFirstPayment;
        buckets.set(time, bucket);
    }
    return [...buckets.entries()]
        .sort(([a], [b]) => a - b) // markers must be in ascending time order
        .map(([time, { totalUsd, count, hasFirst }]) => ({
            time: time as UTCTimestamp,
            position: "belowBar",
            shape: hasFirst ? "circle" : "arrowUp",
            color: hasFirst ? "#f5a524" : "#22c55e",
            text: count > 1 ? `$${totalUsd.toFixed(0)} paid (${count})` : `$${totalUsd.toFixed(0)} paid`,
        }));
}

// Usage: `candles` are your own OHLC bars for the token, with `time` in unix seconds.
async function drawPayoutChart(container: HTMLElement, tokenMint: string, candles: { time: UTCTimestamp; open: number; high: number; low: number; close: number }[]) {
    const candleSeconds = 15 * 60; // must match the candle interval of `candles`
    const chart = createChart(container, { timeScale: { timeVisible: true } });
    const series = chart.addSeries(CandlestickSeries);
    series.setData(candles);

    const from = candles[0].time;
    const to = candles[candles.length - 1].time + candleSeconds - 1;
    const events = await getTokenEventsInRange(tokenMint, from, to);
    createSeriesMarkers(series, toMarkers(events, candleSeconds));
}
```

Rounding each `timestamp` down to the candle interval puts the marker on the candle the payment happened in. To refresh a live chart, request time mode again from the last candle's start and replace the markers. Pass `"all"` as the `method` argument of `getTokenEventsInRange` if the chart should show Cash App and Venmo payments too.

## First payments

`isFirstPayment` is `true` on the first payment to a handle, with the same method, from a token. If a token's handle is [changed](https://docs.bags.fm/how-to-guides/pay-fees-to-x-money.md#change-the-handle-later), the first payment to the new handle is a first payment too. Use it to:

* Celebrate a token's first payout in a bot or ticker, or filter the feed to them with `onlyFirstPayments=true`.
* Highlight the first payment on a token's chart, as the marker example above does.

## Errors

Errors use the usual shape, `{ "success": false, "response": "<message>" }`.

| Status | Cause |
| - | - |
| `400` | An invalid parameter: `limit` out of range, an unknown `sort`, `mode` or `method`, `before` with `sort=amount`, `cursor` with `sort=time` or a malformed cursor, `minAmountCents` above `maxAmountCents`, an invalid `tokenMint`, a missing `from` or `to` in time mode, `from` after `to`, parameters of the other mode, or any parameter not listed for the endpoint. |
| `429` | Rate limited. Back off and retry; see [Rate Limits](https://docs.bags.fm/principles/rate-limits.md). |
| `500` | Server error. Retry later. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.