# Raydium LP Dashboard Public API > Read-only Solana pool data and resumable WebSocket signals for monitored Raydium CLMM and Orca Whirlpool positions. No API key or ADMIN_PASSWORD is required. ## Documentation - [Human-readable API reference](/docs): Server-rendered reference, endpoint index, request/response examples and error codes. - [Complete API reference (Markdown)](/docs/api.md): Canonical field semantics for all five public endpoints, including position.opened, position.increased, position.decreased and position.closed. - [OpenAPI 3.1 schema (YAML)](/docs/openapi.yaml): HTTP contracts and x-websocket message schemas. Ordinary HTTP clients do not implement the WebSocket protocol. - [Complete context in one request](/llms-full.txt): Full Markdown reference, OpenAPI schema and runnable Node.js subscriber. - [Node.js subscriber example](/docs/examples/subscribe-position-openings.mjs): Ordered message handling, durable cursor and reconnect logic; prints signals only. ## Integration facts - Resolve relative links against this document's origin. Use the same origin for HTTPS API calls and WSS subscriptions. Replace https://your-dashboard.example.com in examples with that deployed origin. - GET /api/public/pools: directory; dex=raydium|orca|meteora (default raydium). - GET /api/public/pools/volume and GET /api/public/pools/{poolId}/volume: captured USD volume/fees; dex=raydium|orca, interval=1m|5m|1h|24h. Meteora volume is not supported. - WS /api/public/pool-creations/ws: Raydium CLMM pool creation events. Separate feed and cursor from position events. - WS /api/public/position-openings/ws?creator=ADDRESS&dex=all: original-owner position.opened, position.increased, position.decreased and position.closed signals in existing monitored pools only, not every pool or all wallet activity. dex=all|raydium|orca. Repeat creator=ADDRESS_A&creator=ADDRESS_B to subscribe to 1–100 addresses on one socket; no comma-separated query values. Duplicate addresses are deduplicated; the 100-parameter limit is checked before deduplication. ready.creators is the sorted unique array; legacy ready.creator is the sole address or null for multiple addresses. Each data.creator remains one event owner. - WebSockets are RFC 6455, not SSE, Socket.IO or Solana logsSubscribe. Query parameters subscribe at handshake; do not send JSON subscribe messages. - after is an exclusive publication cursor string, not a slot. Omit for live-only; after=0 replays retained history. Save ready.cursor, not ready.latestCursor. Commit events in order and deduplicate by eventId before advancing checkpoint.cursor. - Position feed retains ten days of signals. Close matching requires a retained position with a known original owner. 409 means cursor ahead; 410 means expired position cursor. All selected addresses share one publication-ordered cursor. Bind cursors to feed, origin, sorted unique address set and DEX. To change the set, reconnect with new subscription state (live-only or explicit after=0 replay with deduplication); do not merge old per-address cursors by taking their maximum. No dynamic subscribe/unsubscribe messages. - Copy raw tickLower/tickUpper in chain token A/B order. tokenA.amountRaw and tokenB.amountRaw are decimal integer strings; use BigInt/BN, not floating point. Display prices may be reversed or ScaledUiAmount-adjusted. amountUsd is an estimate, not an execution quote. - Initial funding is position.opened only. Subsequent liquidity adds/removes emit position.increased/position.decreased, sharing the same cursor. liquidityDelta is an unsigned u128 string, not remaining liquidity or a ratio; direction comes from type. token amounts are raw u64 strings; feeAmountRaw is separate collected Raydium fees, transferFeeRaw can be null on legacy logs. Fee-only collections are not decreases. A same-transaction decrease followed by closing that exact position emits only position.closed; decreases on other positions remain. Handle both new types before checkpointing. Changes require a retained opening with a known original owner. - Map position.closed.openingEventId to the copied position. creator stays the original owner; it is not necessarily the close signer. Partial liquidity withdrawal and local retention expiry are not close signals. - Signals use confirmed commitment. Construct transactions with current DEX state, quotes, token transfer fees and caller-selected slippage. The service does not sign or place orders. --- # Raydium LP Dashboard 公共 API 公共 API 提供当前监控池目录(Raydium,可选 Orca/Meteora 24h 榜单)、Raydium/Orca 分钟级成交量与实际手续费、单池历史成交量、CLMM 建池 WebSocket 及按创建者地址订阅开仓、加仓、减仓和关仓的 WebSocket。所有接口只读、无需 `ADMIN_PASSWORD`,并开放跨域 GET 请求。 以下示例使用: ```text https://your-dashboard.example.com ``` 替换为实际部署域名即可。HTTP 成交量金额单位为 USD,时间使用 UTC ISO 8601;建池事件的 Unix 秒/毫秒字段见第 4 节,仓位原始数量与时间字段见第 5 节。 ## 1. 获取当前监控的池子 ```http GET /api/public/pools?dex={dex}&excludeMeme={boolean} ``` | 参数 | 必填 | 默认值 | 说明 | | --- | --- | --- | --- | | `dex` | 否 | `raydium` | 池子来源:`raydium`、`orca`、`meteora` | | `excludeMeme` | 否 | `true` | 是否排除 Meme 池;对 `dex=raydium` 和 `dex=orca` 生效。支持 `true`/`false`(也兼容 `1`/`0`) | - `raydium`(默认,与不传 `dex` 行为一致):返回 Raydium RWA 池目录与手工添加池合并后的当前监控目录。默认 `excludeMeme=true`,只保留包含 USDC 的池,以及 xStocks/Backpack 股票代币与 USDC、USDG、SOL 或 WSOL 的池;传 `excludeMeme=false` 可返回全部监控池。`manuallyAdded` 表示该池是否在 `/ops/pool-registry` 中手工登记;手工池同样遵循 Meme 过滤参数。 - `orca`:返回 Orca RWA 榜单,与交易量监控使用同一个 `categories=security` TOP 50 接口,按 24h 手续费降序;默认只保留 xStocks / Backpack Securities 与 USDC、USDG、SOL 或 WSOL 组成的池,传 `excludeMeme=false` 可返回该接口的完整结果。 - `meteora`:返回 Meteora RWA 榜单,与 `/meteora` 菜单页同源,24h 统计窗口、按手续费降序 TOP 50。 `excludeMeme` 不过滤 Meteora,该 DEX 的榜单始终保持原有返回口径。 ```bash curl 'https://your-dashboard.example.com/api/public/pools' curl 'https://your-dashboard.example.com/api/public/pools?dex=raydium&excludeMeme=false' curl 'https://your-dashboard.example.com/api/public/pools?dex=orca' ``` 响应示例(`dex=raydium`): ```json { "data": [ { "id": "POOL_ADDRESS", "dex": "raydium", "pair": "TOKEN/USDC", "baseToken": { "symbol": "TOKEN", "mint": "BASE_MINT_ADDRESS", "decimals": 6, "logoURI": "https://example.com/token-logo.png" }, "quoteToken": { "symbol": "USDC", "mint": "QUOTE_MINT_ADDRESS", "decimals": 6, "logoURI": "https://example.com/usdc-logo.png" }, "feeRate": 0.0025, "feeBps": 25, "tvlUsd": 1250000.5, "volume24hUsd": null, "fees24hUsd": null, "manuallyAdded": true, "addedAt": "2026-07-15T02:30:00.000Z" } ], "meta": { "count": 1, "generatedAt": "2026-07-15T03:00:00.000Z" } } ``` `baseToken.logoURI` 和 `quoteToken.logoURI` 来自对应 DEX 的 token metadata;元数据未提供图片地址时返回 `null`。 `dex=orca` / `dex=meteora` 时返回榜单条目,差异如下: - `manuallyAdded` 固定为 `false`,`addedAt` 固定为 `null`。 - `decimals` 来自上游 token metadata;上游未提供时为 `null`。 - `volume24hUsd` 和 `fees24hUsd` 为 24h 窗口的交易量与手续费(USD);`dex=raydium` 时这两个字段固定为 `null`。 - 排序保持上游榜单顺序(24h 手续费降序)。 - 上游不可用时返回 `502`。 ```json { "data": [ { "id": "POOL_ADDRESS", "dex": "orca", "pair": "SOL/USDC", "baseToken": { "symbol": "SOL", "mint": "BASE_MINT_ADDRESS", "decimals": 9, "logoURI": "https://example.com/sol-logo.png" }, "quoteToken": { "symbol": "USDC", "mint": "QUOTE_MINT_ADDRESS", "decimals": 6, "logoURI": "https://example.com/usdc-logo.png" }, "feeRate": 0.0004, "feeBps": 4, "tvlUsd": 52300000.12, "volume24hUsd": 18200000.45, "fees24hUsd": 72800.18, "manuallyAdded": false, "addedAt": null } ], "meta": { "count": 1, "generatedAt": "2026-07-15T03:00:00.000Z" } } ``` ## 2. 一次查询全部池成交量 ```http GET /api/public/pools/volume?dex={dex}&interval={interval}&limit={limit}&excludeMeme={boolean} ``` 一次返回当前全部 Raydium 或 Orca 监控池的成交量和手续费时间序列。参数与单池接口一致: | 参数 | 必填 | 默认值 | 说明 | | --- | --- | --- | --- | | `dex` | 否 | `raydium` | 成交量来源:`raydium` 或 `orca`;`meteora` 暂无链上成交量监控 | | `interval` | 否 | `1h` | 聚合时间段:`1m`、`5m`、`1h`、`24h` | | `limit` | 否 | `24` | 每个池返回最近多少个 `interval`,正整数 | | `excludeMeme` | 否 | `true` | 是否排除 Meme 池;`dex=orca` 时只保留 xStocks / Backpack Securities 与 USDC、USDG、SOL 或 WSOL 组成的池 | ```bash curl 'https://your-dashboard.example.com/api/public/pools/volume?interval=1h&limit=24' curl 'https://your-dashboard.example.com/api/public/pools/volume?dex=orca&interval=1m&limit=60' curl 'https://your-dashboard.example.com/api/public/pools/volume?interval=5m&limit=12&excludeMeme=false' ``` 响应中的顶层 `totals` 是全部返回池在查询窗口内的合计;每个池也有自己的 `totals` 和固定 `limit` 个时间桶。`data` 按窗口交易量从高到低排列,交易量相同时按交易对名称排列。 ```json { "query": { "dex": "raydium", "interval": "1h", "limit": 24, "excludeMeme": true, "windowStart": "2026-07-14T04:00:00.000Z", "windowEnd": "2026-07-15T03:03:20.000Z" }, "totals": { "volumeUsd": 128300.5, "feesUsd": 320.75, "buyVolumeUsd": 71400.25, "sellVolumeUsd": 56900.25, "trades": 241, "pools": 18 }, "data": [ { "pool": { "id": "POOL_ADDRESS", "dex": "raydium", "pair": "TOKEN/USDC", "baseToken": { "symbol": "TOKEN", "mint": "BASE_MINT_ADDRESS", "decimals": 6, "logoURI": null }, "quoteToken": { "symbol": "USDC", "mint": "QUOTE_MINT_ADDRESS", "decimals": 6, "logoURI": null }, "feeRate": 0.0025, "feeBps": 25, "tvlUsd": 1250000.5, "volume24hUsd": null, "fees24hUsd": null, "manuallyAdded": false, "addedAt": null }, "totals": { "volumeUsd": 42150.75, "feesUsd": 105.38, "buyVolumeUsd": 23700.25, "sellVolumeUsd": 18450.5, "trades": 86 }, "data": [ { "startTime": "2026-07-14T04:00:00.000Z", "endTime": "2026-07-14T05:00:00.000Z", "complete": true, "volumeUsd": 3120.5, "feesUsd": 7.8, "buyVolumeUsd": 1870.25, "sellVolumeUsd": 1250.25, "trades": 7 } ] } ] } ``` 该接口只返回本服务捕获并持久化的链上成交,不代理上游榜单统计。`dex=orca` 使用当前 Orca RWA TOP 50 监控目录;`dex=meteora` 返回 `400`。即使某个当前监控池在窗口内没有成交,也会返回该池,并以零填充所有时间桶。数据库聚合在一次查询中完成,不会按池逐个查询。 ## 3. 按池 ID 查询成交量 ```http GET /api/public/pools/{poolId}/volume?dex={dex}&interval={interval}&limit={limit}&excludeMeme={boolean} ``` 查询当前监控池的成交量时间序列。 | 参数 | 必填 | 默认值 | 说明 | | --- | --- | --- | --- | | `poolId` | 是 | - | Solana 池地址,且必须位于当前监控目录 | | `dex` | 否 | `raydium` | 池来源:`raydium` 或 `orca` | | `interval` | 否 | `1h` | 聚合时间段:`1m`、`5m`、`1h`、`24h` | | `limit` | 否 | `24` | 返回最近多少个 `interval`,正整数 | | `excludeMeme` | 否 | `true` | 是否排除 Meme 池;`dex=orca` 时只保留发行方可识别的证券池 | 例如,以 5 分钟为一段,查询最近 12 段(约 1 小时): ```bash curl 'https://your-dashboard.example.com/api/public/pools/POOL_ADDRESS/volume?interval=5m&limit=12' curl 'https://your-dashboard.example.com/api/public/pools/ORCA_POOL_ADDRESS/volume?dex=orca&interval=1m&limit=60' curl 'https://your-dashboard.example.com/api/public/pools/MEME_POOL_ADDRESS/volume?interval=5m&limit=12&excludeMeme=false' ``` 响应示例: ```json { "pool": { "id": "POOL_ADDRESS", "dex": "raydium", "pair": "TOKEN/USDC", "baseToken": { "symbol": "TOKEN", "mint": "BASE_MINT_ADDRESS", "decimals": 6, "logoURI": "https://example.com/token-logo.png" }, "quoteToken": { "symbol": "USDC", "mint": "QUOTE_MINT_ADDRESS", "decimals": 6, "logoURI": "https://example.com/usdc-logo.png" }, "feeRate": 0.0025, "feeBps": 25, "tvlUsd": 1250000.5, "volume24hUsd": null, "fees24hUsd": null, "manuallyAdded": true, "addedAt": "2026-07-15T02:30:00.000Z" }, "query": { "dex": "raydium", "interval": "5m", "limit": 12, "excludeMeme": true, "windowStart": "2026-07-15T02:05:00.000Z", "windowEnd": "2026-07-15T03:03:20.000Z" }, "totals": { "volumeUsd": 42150.75, "feesUsd": 105.38, "buyVolumeUsd": 23700.25, "sellVolumeUsd": 18450.5, "trades": 86 }, "data": [ { "startTime": "2026-07-15T02:05:00.000Z", "endTime": "2026-07-15T02:10:00.000Z", "complete": true, "volumeUsd": 3120.5, "feesUsd": 7.8, "buyVolumeUsd": 1870.25, "sellVolumeUsd": 1250.25, "trades": 7 } ] } ``` ### 时间和数据口径 - 时间段按 UTC 自然边界对齐。例如 `5m` 使用 `10:00–10:05`、`10:05–10:10`;`24h` 使用 UTC 自然日。 - 返回数组固定包含 `limit` 个时间段,无成交的时间段补零。 - 最后一段通常仍在进行,此时 `complete` 为 `false`。`query.windowEnd` 是服务器实际查询时间。 - `volumeUsd` 来自监控服务捕获的 Swap,以 1 分钟蜡烛聚合;加池和撤池不计入成交量。 - `feesUsd` 是新捕获成交的链上实际手续费。Orca 使用 `Traded` 事件的 `lp_fee + protocol_fee`,动态费率池也可按实际值统计;历史 Raydium 蜡烛未回填实际手续费时可能为 `0`。 - `dex=raydium` 默认不返回被 Meme 规则排除的池,显式传 `excludeMeme=false` 后可查询;`dex=orca` 默认只返回 xStocks / Backpack Securities 证券池,传 `excludeMeme=false` 后可查询其他当前监控池。 - `dex=orca` 从服务启动后开始累积本地成交,不扫描上线前历史;退榜池返回 `404`。 - 服务默认保留 90 天蜡烛数据,可通过 `CANDLE_RETENTION_DAYS` 修改。单次最多返回 1000 段,并且不能超过当前保留期;超出时返回 `400`。 ## 错误响应 ```json { "error": { "message": "interval must be one of: 1m, 5m, 1h, 24h", "parameter": "interval" } } ``` | HTTP 状态码 | 含义 | | --- | --- | | `400` | 池地址或查询参数无效 | | `404` | 池不在当前监控目录,或被默认 Meme 过滤规则排除(可传 `excludeMeme=false` 查询) | | `500` | 服务内部错误 | | `502` | `dex=orca` / `dex=meteora` 上游数据服务不可用 | OpenAPI 3.1 机器可读定义见 [`docs/openapi.yaml`](/docs/openapi.yaml)。 ## 4. WebSocket:订阅 Raydium CLMM 新建池 ```text wss://your-dashboard.example.com/api/public/pool-creations/ws wss://your-dashboard.example.com/api/public/pool-creations/ws?after=0 ``` 这是标准 WebSocket(RFC 6455),不是 SSE 或 Solana `logsSubscribe`。与其他公共 API 一样,只读、无需鉴权,可从浏览器跨域连接。不要传 `ADMIN_PASSWORD`,此接口不需要签名密钥。 - 只发布本项目捕获的成功 **Raydium CLMM** 建池事件,不包含 CPMM、Orca、Meteora 或失败交易。 - 新池发现独立于 RWA / 手工池目录,包含普通代币新池;本接口不提供 `excludeMeme` 或 `dex` 参数。 - 发送时过滤已拉黑的池及已知黑名单创建者。已发送的事件不会因之后拉黑而撤回;创建者未知时为 `null`。 - 复用现有链上订阅及 SQLite 事件,不为每个客户端增加 RPC 或链上 WebSocket。元数据在服务端统一补充;失败最多尝试 3 次后发布可用字段,名称、创建者等可能为 `null`。 - 事件正式发布后分配持久化游标,后续不再更新同一事件的 payload。元数据补充较慢的池可能晚于其他池发布,因此游标顺序不等同于 slot 或建池时间顺序。 - 建池事件仅通过 WebSocket 分享。byreal-copy 使用已有 `RAYDIUM_DASHBOARD_API_BASE` 自动订阅此接口,首次 `after=0`,后续按持久化游标续传。 ### 连接参数与补发 | 参数 | 必填 | 默认 | 说明 | | --- | --- | --- | --- | | `after` | 否 | 连接时的最新游标 | 十进制非负整数,以字符串保存,最大 `9007199254740991`。从该游标**之后**开始补发,再持续推送新事件 | 不传 `after` 只接收之后发布的事件;`after=0` 从数据库保存的第一条可见事件开始补发。每个连接每秒最多扫描 100 条记录,积压会逐批追上。没有订阅指令,握手成功即开始推送;不要发送 JSON subscribe 消息。 游标与本实例的 SQLite 数据库绑定,重启后仍有效。持久化卷丢失、还原旧备份或改连另一份数据库时,应重新确定起点。游标大于数据库当前最大值会在握手阶段返回 `409`,不会静默忽略。 服务端目前不按时间清理建池 feed。黑名单会使游标出现跳号。客户端应在成功处理事件后保存游标,并按 `data.eventId` 幂等入库;重连补发可能重复已接收但未提交游标的事件。此机制只能重放已经捕获并持久化的事件,不能补齐链上 WS 断线期间从未收到的新池。 ### 服务端消息 **1. ready:** 握手后的第一条消息。 ```json {"type":"ready","version":1,"cursor":"120","latestCursor":"150","heartbeatSeconds":25} ``` `cursor` 是本次读取起点,`latestCursor` 是握手时的最新位置。请求 `after=120` 时仍需处理 121~150,**不能直接保存 `latestCursor` 跳过补发**。不传 `after` 时两者相同,可保存 `cursor` 作为初始重连起点。 **2. pool.created:** WebSocket 外层消息包裹 v1 建池事件。 ```json { "type": "pool.created", "cursor": "121", "data": { "version": 1, "type": "raydium.pool.created", "eventId": "SIGNATURE:pool:POOL_ADDRESS", "signature": "SIGNATURE", "slot": 350000000, "blockTime": null, "detectedAt": 1789084800000, "programId": "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK", "poolType": "clmm", "poolAddress": "POOL_ADDRESS", "creator": null, "ammConfig": null, "sqrtPriceX64": "18446744073709551616", "tick": 0, "tickSpacing": 60, "vaultA": "VAULT_A", "vaultB": "VAULT_B", "tokenA": {"mint":"MINT_A","symbol":"AAPLx","name":"Apple xStock","icon":null,"decimals":6}, "tokenB": {"mint":"MINT_B","symbol":"USDC","name":"USD Coin","icon":null,"decimals":6} } } ``` | 字段 | 类型 / 含义 | | --- | --- | | 外层 `cursor` | 持久化发布序号,十进制字符串,不是 slot | | `version` / `type` | 固定为 `1` / `raydium.pool.created` | | `eventId` | `${signature}:pool:${poolAddress}`;一笔交易多个池有不同 ID | | `signature` / `slot` | 交易签名与确认时所在 slot;commitment 为 confirmed | | `blockTime` | Unix **秒**或 `null`;实时日志没有精确区块时间,通常为 `null` | | `detectedAt` | 本项目发现事件的 Unix **毫秒**,不是池开盘时间 | | `programId` / `poolType` | Raydium CLMM 程序地址 / `clmm` | | `poolAddress` | 新池地址 | | `creator` / `ammConfig` | 从池账户补充的创建者 / 配置地址;不可用时为 `null` | | `sqrtPriceX64` | 创建时链上 Q64.64 平方根价格,u128 十进制字符串;不得转换为 JS Number 后再持久化 | | `tick` / `tickSpacing` | 初始 tick / 池 tick 间距 | | `vaultA` / `vaultB` | 两侧 token vault 地址 | | `tokenA` / `tokenB` | 保持链上 token0 / token1 顺序;含 mint、symbol、name、icon、decimals,除 mint 外均可为 `null` | 该事件不包含初始加池金额、TVL 或实际开仓:CLMM 创建池与添加流动性是不同操作。价格参数保持链上原始口径,未应用 Token-2022 ScaledUiAmount 倍率。 **3. checkpoint:** 一批扫描完成,可在前面的事件都处理成功后保存此游标。它也会跨过被黑名单隐藏的记录。 ```json {"type":"checkpoint","cursor":"150"} ``` ### JavaScript 重连示例 ```javascript let cursor = localStorage.getItem('raydium-pool-cursor'); let retryMs = 1000; function connect() { const url = new URL('wss://your-dashboard.example.com/api/public/pool-creations/ws'); if (cursor !== null) url.searchParams.set('after', cursor); const ws = new WebSocket(url); ws.onopen = () => { retryMs = 1000; }; ws.onmessage = ({ data }) => { const message = JSON.parse(data); if (message.type === 'pool.created') { // 示例同步处理;生产消费者按 eventId 幂等提交后才推进游标。 console.log(message.data); } if (['ready', 'pool.created', 'checkpoint'].includes(message.type)) { cursor = message.cursor; // ready 用 cursor,不能用 latestCursor localStorage.setItem('raydium-pool-cursor', cursor); } }; ws.onclose = () => { const wait = retryMs + Math.random() * 500; retryMs = Math.min(retryMs * 2, 30000); setTimeout(connect, wait); }; } connect(); ``` 如果处理是异步的,须按收到的顺序串行完成事件和 checkpoint,不能让后续 checkpoint 先提交。数据库被重置导致 `409` 时,人工选择清除本地游标(从实时开始)或改用 `after=0`(全部重放),不要无限重试旧游标。Node 客户端可使用 `ws` 包,浏览器使用内置 WebSocket。 ### 心跳、错误与部署 - 每 25 秒发送 WebSocket 协议 ping;浏览器和 `ws` 默认自动 pong,无须发送 JSON 心跳。下一次检查仍未 pong 时终止连接。 - 默认每个进程最多 200 个连接,可通过 `POOL_CREATION_WS_MAX_CONNECTIONS` 调整;超过上限返回 `503`。 - 握手 `400`:非法、重复或不支持的参数;`409`:游标超前;`503`:存储尚未准备好、mock 模式或连接已满。普通 HTTP GET 返回 `426 Upgrade Required`。 - 连接后关闭码 `1008`:发送了应用消息或数据库回退导致游标失效;`1011`:存储不可用。慢客户端发送缓冲超过 1 MiB 时终止连接,客户端通常看到 `1006`,应从最后成功处理的游标重连。 - 启动必须使用 `pnpm start`(`node server.mjs`),开发使用 `pnpm dev`。直接运行 `next start` 不提供此 WebSocket。Dockerfile 已同步复制入口及 transport,监听与网页相同的 3000 端口。 - EasyPanel / Nginx 必须转发 `Upgrade` 和 `Connection`;公网用 HTTPS / WSS。代理空闲超时建议大于 60 秒。多个实例必须各自使用一致的事件存储和游标范围;不要把本地 SQLite 游标用于另一份独立数据库。 Nginx 示例(按实际 upstream 调整): ```nginx location /api/public/pool-creations/ws { proxy_pass http://dashboard:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 75s; } ``` 部署方式参考 [Next.js Custom Server](https://nextjs.org/docs/app/guides/custom-server),协议处理使用 [ws](https://github.com/websockets/ws)。OpenAPI 文件描述握手及 `x-websocket` 消息协议;普通 OpenAPI HTTP 客户端生成器不会自动生成 WebSocket 订阅代码。 ## 5. WebSocket:按创建者地址订阅新建仓位与关仓 ```text wss://your-dashboard.example.com/api/public/position-openings/ws?creator=CREATOR_ADDRESS wss://your-dashboard.example.com/api/public/position-openings/ws?creator=CREATOR_ADDRESS&dex=orca&after=120 wss://your-dashboard.example.com/api/public/position-openings/ws?creator=ADDRESS_A&creator=ADDRESS_B&dex=all ``` 标准只读 WebSocket,无需密码;连接后自动订阅,不发送 subscribe 消息。支持 **Raydium CLMM / Orca Whirlpool**,范围为本服务已有监控池(Raydium RWA/手工池目录、Orca RWA TOP 50),不是该钱包的全链交易监听。增加客户端不会增加链上 RPC 或链上订阅。 | 参数 | 必填 | 默认 | 说明 | | --- | --- | --- | --- | | `creator` | 是 | — | 开仓时仓位持有人的 Solana 地址,不是出资人或手续费支付人。可重复传入 1–100 个参数,如 `creator=A&creator=B`;每个值必须是一个有效完整地址,不接受逗号分隔。重复地址去重,只推送一次;数量限制按去重前参数个数计算 | | `dex` | 否 | `all` | `all`、`raydium` 或 `orca` | | `after` | 否 | 连接时最新发布游标 | 非负十进制整数字符串,最大 `9007199254740991`;从此游标之后续传。`0` 补发仍保留的历史事件 | 一个连接合并所有订阅地址的开仓、加仓、减仓与关仓事件,按全局发布游标升序推送,`data.creator` 标明事件所属地址。`dex` 统一作用于整组地址。只有一条 `after` / checkpoint 游标,无需为组内各地址分别续传;单地址 URL 保持兼容。 **发布条件:** 已捕获创建事件、已首次注入非零代币数量、已知创建者,并且保存了原始 tick/金额。Raydium 直接使用开仓日志;Orca 等开仓交易中的持有人补齐后再发布,空仓创建和首次入金可以是两笔交易。后续加仓发布为 `position.increased`,不重复发布新开仓。创建者补齐失败、旧版记录缺少原始参数时不发布,不能从展示价格猜测 tick。 事件先与仓位一起持久化,再分配独立发布游标;同一开仓事件只发布一次。延迟补齐的 Orca 创建者获得新的发布游标,因此不会被已经推进的订阅游标漏掉。按当前池/创建者黑名单过滤。 ### 消息与跟单参数 第一条消息: ```json {"type":"ready","version":1,"creator":"CREATOR_ADDRESS","creators":["CREATOR_ADDRESS"],"dex":"all","scope":"monitored-pools","retentionDays":10,"cursor":"120","latestCursor":"125","heartbeatSeconds":25} ``` `ready.creators` 总是返回按字典序排序、去重后的完整地址数组;`ready.creator` 为兼容旧客户端保留,只有一个地址时为该地址,多地址时为 `null`。事件中的 `data.creator` 始终是该事件的单个原始持有人地址。 多地址 ready 示例: ```json {"type":"ready","version":1,"creator":null,"creators":["ADDRESS_A","ADDRESS_B"],"dex":"all","scope":"monitored-pools","retentionDays":10,"cursor":"120","latestCursor":"125","heartbeatSeconds":25} ``` `cursor` 是本次续传起点;不要把 `latestCursor` 保存为已处理游标。`after=0` 时 `cursor` 可能是已经清理的历史边界。 新开仓消息(以下地址为占位示例): ```json { "type": "position.opened", "cursor": "121", "data": { "version": 1, "type": "position.opened", "eventId": "orca:OPEN_SIGNATURE:open:0", "dex": "orca", "programId": "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc", "poolAddress": "POOL_ADDRESS", "creator": "CREATOR_ADDRESS", "signature": "OPEN_SIGNATURE", "eventIndex": 0, "slot": 454600000, "openedAt": 1791504000000, "fundingSignature": "FUNDING_SIGNATURE", "fundingSlot": 454600001, "fundedAt": 1791504000400, "publishedAt": 1791504002000, "commitment": "confirmed", "positionAddress": "POSITION_PDA", "nftMint": "POSITION_NFT_MINT", "tickLower": -120, "tickUpper": 240, "tickSpacing": 60, "tokenA": {"mint":"MINT_A","symbol":"TOKEN","decimals":6,"amountRaw":"1000000"}, "tokenB": {"mint":"MINT_B","symbol":"USDC","decimals":6,"amountRaw":"100000000"}, "amountUsd": 200, "display": {"baseMint":"MINT_A","quoteMint":"MINT_B","priceLower":0.9880723,"priceUpper":1.0242891} } } ``` | 字段 | 用途与口径 | | --- | --- | | `eventId` | `${dex}:${signature}:open:${eventIndex}`,幂等键;同一交易多个仓位分别发布 | | `dex` / `programId` / `poolAddress` | 选择 DEX SDK、程序和目标池 | | `creator` | 原始开仓持有人;Orca 组合仓位从该交易的组合 NFT token balance 识别,不把代理权限地址当成持有人 | | `tickLower` / `tickUpper` | **链上原始整数 tick**,保持 token A/B 方向;展示交易对反转也不改变这两个值,不从价格反推 | | `tickSpacing` | Orca 池目录中的 tick 间距;Raydium 当前未保存该元数据,返回 `null`,消费者可随池状态读取 | | `tokenA` / `tokenB` | **链上 token0/token1 或 Whirlpool A/B 顺序**,含 mint、symbol、decimals、首次注入的原始数量 `amountRaw` | | `amountRaw` | u64 最小单位十进制字符串,应使用 BigInt/BN;没有 ScaledUiAmount 展示倍率,也不是已含滑点的 maximum amount。转账费按对应 DEX/Token-2022 规则处理 | | `amountUsd` | 初次入金数量乘当时缓存价格的 USD 估值;缺少价格可能为 0,不是下单预算或保证成交金额 | | `display` | 仅供显示的 base/quote 价格区间,可能反向且已应用 ScaledUiAmount;不要用于还原链上 tick | | `signature` / `slot` | 创建仓位交易;`eventIndex` 为该 DEX 解码事件中的序号,不是 instruction index | | `fundingSignature` / `fundingSlot` | 首次入金交易;Raydium 与开仓一致,Orca 可能不同 | | `openedAt` / `fundedAt` | Unix 毫秒;实时路径使用本服务收到日志的时间,补漏路径使用区块时间,并非精确成交时间 | | `publishedAt` | 本服务正式发布的 Unix 毫秒,可能晚于开仓/首次入金 | | `positionAddress` / `nftMint` | 原始仓位身份,Raydium 日志缺失时为 `null`;跟单者创建自己的仓位/NFT,不复用原仓位身份 | | `commitment` | `confirmed`;事件不是 finalized 通知,也不发送链回滚撤销事件 | 消费者可以按原始数量或自定比例/预算跟单,使用返回的目标池与原始 tick 区间。执行前仍需通过对应 DEX SDK 读取当前池状态,计算当前所需数量、滑点上限、账户与 Token-2022 转账费;接口提供开仓信号和原始条件,不返回已签名交易,也不代替消费者下单。 ### 加仓与减仓消息 同一订阅连接新增两种消息:`position.increased`(后续增加流动性)和 `position.decreased`(减少流动性)。订阅地址、DEX 过滤、单一游标、幂等与十天回放规则不变。旧客户端需要处理这两种类型后再提交后续 checkpoint。 - 第一次入金记为 `position.opened`;Orca 的空仓创建后第一次入金不重复算加仓,同笔交易之后再次增加流动性才算加仓。 - 只有流动性变化量大于零才发布;纯领取手续费/奖励不是减仓。减到零但账户尚未关闭仍只是减仓,后续账户关闭单独发布 `position.closed`。同笔交易内同一仓位先减仓再关仓,只发布一次 `position.closed`,不再发布该仓位的 `position.decreased`;同笔交易中其他未关闭仓位的减仓仍正常推送。识别到关仓日志时,减仓会等待现有关仓交易查询确认对应 PDA,复用查询结果,不为此新增 RPC。 - `creator` 始终是原始开仓持有人,不是本次操作签名者;NFT 转手后仍按原持有人订阅。`openingEventId` 用于关联自己的跟单仓位,不能仅按地址/池匹配。 - 仅覆盖已有监控池内、能关联到仍保留开仓快照且已知创建者/原始参数的仓位;不会扫描全链或补造上线前缺失的开仓。未知创建者的 Orca 变动先持久化,待身份补齐后发布。 - 事件来自成功程序调用;失败交易和被上层回滚的 CPI 不发布。同笔交易多次变动各有不同 ID,WS 与补漏重放去重。金额保持事件原始整数,不改变开仓快照为“当前余额”,LP 变动也不计入 Swap 成交量。 ```json { "type": "position.decreased", "cursor": "123", "data": { "version": 1, "type": "position.decreased", "eventId": "orca:CHANGE_SIGNATURE:liquidity:0", "dex": "orca", "programId": "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc", "poolAddress": "POOL_ADDRESS", "creator": "CREATOR_ADDRESS", "positionAddress": "POSITION_PDA", "nftMint": "POSITION_NFT_MINT", "openingEventId": "orca:OPEN_SIGNATURE:open:0", "openingSignature": "OPEN_SIGNATURE", "signature": "CHANGE_SIGNATURE", "eventIndex": 0, "slot": 454600100, "occurredAt": 1791504100000, "publishedAt": 1791504101000, "commitment": "confirmed", "liquidityDelta": "12345678901234567890", "tickLower": -120, "tickUpper": 240, "tickSpacing": 60, "tokenA": {"mint":"MINT_A","symbol":"TOKEN","decimals":6,"amountRaw":"500000","feeAmountRaw":"0","transferFeeRaw":"0"}, "tokenB": {"mint":"MINT_B","symbol":"USDC","decimals":6,"amountRaw":"50000000","feeAmountRaw":"0","transferFeeRaw":"0"} } } ``` 加仓消息字段相同,内外层 `type` 均为 `position.increased`。 | 字段 | 含义 | | --- | --- | | `eventId` | `${dex}:${signature}:liquidity:${eventIndex}`,按事件幂等;不是开仓事件 ID | | `eventIndex` | 当前 DEX 交易日志中加/减流动性事件的序号,从 0 开始;Orca 的首次入金也占序号,但不作为加仓发布。不同于开仓序号与 instruction index | | `liquidityDelta` | u128 十进制字符串,非零、无符号的流动性变动绝对值;方向由消息类型决定。不是剩余流动性或增减仓比例,不能据此推断“已全部减仓” | | `tokenA/B.amountRaw` | 链上事件的本次流动性对应原始代币数量,u64 字符串。Raydium 减仓的独立手续费字段不并入该数量;不是滑点上限或保证净到账量 | | `tokenA/B.feeAmountRaw` | Raydium 减仓事件单独报告的已领取手续费;其他事件为 `"0"`。不包含奖励;Orca 独立领取手续费/奖励指令不在此事件内 | | `tokenA/B.transferFeeRaw` | 事件报告的 Token-2022 转账费,旧版 Raydium 日志没有该字段时为 `null`。Raydium 减仓转账费可能对应本金与手续费合并转出,不能直接当作本金净额扣除 | | `tickLower/Upper` | 原始链上 token A/B 方向;Orca 使用本次事件的 tick,Raydium 使用关联开仓快照的 tick | | `occurredAt` | Unix 毫秒;实时接收时间或补漏交易区块时间 | | `slot` | 变动事件所在 slot(实时日志 context 或完整交易 slot) | | `openingEventId` / `openingSignature` | 原开仓事件 ID 与签名;用于精确关联 | 跟单程序应读取当前目标仓位/池状态,自行计算加减仓比例、数量、滑点和转账费。`liquidityDelta`、原始数量不能当成当前仓位余额;不要重复使用 `openedAt`/开仓金额表示本次变动。本消息不提供保证成交报价或盈亏。 Orca 复用现有日志及已保存仓位;Raydium 变动日志只有 NFT 标识,未建立关联时会异步查询已识别的变动交易,先过滤监控池,再沿该 PDA 历史精确关联仍保留的开仓。关联和非监控池结果会缓存;重试最多 8 次,新增订阅客户端不增加 RPC。监控范围仍为现有池和十天仓位保留期。 事件字段依据 [Raydium 仓位事件定义](https://github.com/raydium-io/raydium-clmm/blob/master/programs/amm/src/states/personal_position.rs) 与 [Orca 事件定义](https://github.com/orca-so/whirlpools/blob/main/programs/whirlpool/src/events.rs)。 ### 关仓消息 同一连接推送 `position.opened`、`position.increased`、`position.decreased` 和 `position.closed`,使用同一条发布游标;接口路径与 `creator` / `dex` / `after` 参数不变。订阅程序通过 `openingEventId` 查找自己记录的跟单仓位映射。 ```json { "type": "position.closed", "cursor": "122", "data": { "version": 1, "type": "position.closed", "eventId": "orca:CLOSE_SIGNATURE:close:POSITION_PDA", "dex": "orca", "programId": "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc", "poolAddress": "POOL_ADDRESS", "creator": "CREATOR_ADDRESS", "signature": "CLOSE_SIGNATURE", "slot": 454601000, "closedAt": 1791504400000, "publishedAt": 1791504401500, "commitment": "confirmed", "positionAddress": "POSITION_PDA", "nftMint": "POSITION_NFT_MINT", "openingEventId": "orca:OPEN_SIGNATURE:open:0", "openingSignature": "OPEN_SIGNATURE" } } ``` - `eventId` 为 `${dex}:${关闭交易签名}:close:${仓位PDA}`,重试和断线重放按此幂等;同一交易关闭多个仓位分别发布。 - `openingEventId` 与开仓消息的 `eventId` 一致;`openingSignature` 是原开仓签名。旧版仅有 PDA 的快照无法还原开仓 ID 时,这两个字段为 `null`,可使用 `positionAddress` 匹配。不要只按创建者/池地址关掉所有跟单仓位。 - `creator` 保持原始开仓持有人,因此 NFT 后来转手、由其他地址关闭时,原订阅仍按同一地址接收;该字段不是关仓签名者。 - `signature` / `slot` 属于关仓交易;`closedAt` 为该交易区块时间的 Unix 毫秒,区块时间不可用时取本地处理时间。`publishedAt` 为发布时刻。 - 只对成功的 Raydium `ClosePosition`、Orca `ClosePosition` / `ClosePositionWithTokenExtensions` / `CloseBundledPosition` 指令发布。部分撤流动性、把流动性降为零但未关闭账户、本地保留期清理都不属于关仓消息。 - 复用现有关仓日志/补漏和持久化重试,不新增客户端专属 RPC。Raydium 通过原有 PDA 创建历史解析精确开仓 ID;Orca 按 PDA 和 slot 防止旧关仓误删后来复用的组合仓位。 - 关仓发布与删除原仓位在同一个数据库事务中完成,发布失败会保留仓位待重试。必须能关联本服务仍保留的仓位快照(开仓快照保留十天)且已知原创建者;不扫描全链历史、不补发升级前已删除的仓位,也不凭空猜测未知创建者。 - 关仓消息不返回撤回金额或盈亏;关闭账户指令本身不提供这些数量,不能把原开仓金额当成实际赎回金额。 ### 续传、保留与示例程序 ```json {"type":"checkpoint","cursor":"125"} ``` 只在前序事件处理成功后持久化 checkpoint。服务按创建者索引读取,每连接每秒最多处理整组地址合计 100 条候选记录(不是每个地址 100 条);checkpoint 可直接跨过其他地址、DEX 或黑名单记录,**不能用游标差判断漏单数量**。 - 发布记录保留十天(启动/每小时清理)。记录是历史开仓/关仓信号,关闭后保留原开仓消息并追加关仓消息;旧开仓消息不表示此刻仍持仓;消费者应检查事件新鲜度、需要时确认原仓位状态。 - 不传 `after` 从实时开始;`after=0` 会重放历史,消费者必须按 `eventId` 幂等,避免重连重复跟单。API 上线前缺少原始参数的仓位不回填。 - 游标与数据库及订阅条件绑定。游标必须绑定同一地址集合、`dex` 和部署实例;调整地址顺序或重复同一地址不改变订阅集合。增加、删除地址或更换 `dex` 时需重连,并使用新的游标文件/订阅状态:不传 `after` 从实时开始,或显式 `after=0` 重放仍保留的历史并按 `eventId` 去重。不能直接取多个旧单地址游标的最大值合并,否则会跳过其他地址尚未处理的事件。不支持连接内动态发送增删地址指令。 - `400`:地址无效、缺少地址、`creator` 参数超过 100 个、DEX/游标格式错误、重复 `dex` / `after` 或未知参数;`409`:游标超过当前数据库最大值;`410`:游标已超出保留期,需要调用者选择新起点;`426`:普通 HTTP GET;`503`:数据库未就绪、mock 模式或连接已满。 - 连接后游标回退/过期或收到应用消息关闭码 `1008`;存储异常 `1011`;慢客户端缓冲超过 1 MiB 会被终止。每 25 秒协议 ping,未回复 pong 的连接被清理。 - 默认最多 200 个开仓订阅连接,可通过 `POSITION_OPENING_WS_MAX_CONNECTIONS` 修改。使用既有 `server.mjs` / `pnpm start`,端口与网页相同;反向代理对该路径启用 WebSocket Upgrade,超时大于 60 秒。 仓库提供可运行示例 [`examples/subscribe-position-openings.mjs`](/docs/examples/subscribe-position-openings.mjs),串行处理事件、原子保存游标、断线退避重连,并区分订阅条件。示例只打印信号;将 `handleOpening` / `handleLiquidityChange` / `handleClose` 替换为按 `eventId` 幂等的持久化处理函数;先保存开仓事件到自有仓位的映射,关仓时按 `openingEventId` 查询该映射。 ```bash CURSOR_FILE=./my-wallet-orca-cursor.json node examples/subscribe-position-openings.mjs \ https://your-dashboard.example.com CREATOR_ADDRESS orca # 示例程序的命令行支持逗号分隔,内部会转换为重复 creator 参数 CURSOR_FILE=./my-wallets-cursor.json node examples/subscribe-position-openings.mjs \ https://your-dashboard.example.com ADDRESS_A,ADDRESS_B all ``` # OpenAPI 3.1 schema ```yaml openapi: 3.1.0 info: title: Raydium LP Dashboard Public API version: 1.8.0 description: Read-only API for monitored Raydium CLMM pools, Orca/Meteora 24h fee leaderboards, captured USD swap volume and fees, and resumable pool-creation and address-scoped position-opening WebSockets. servers: - url: https://your-dashboard.example.com description: Replace with the deployed dashboard origin paths: /api/public/pools: get: summary: List pools by DEX operationId: listMonitoredPools description: >- Defaults to the monitored Raydium RWA and manually registered pool directory. With dex=orca, returns the security-category TOP 50 source used by the volume monitor. With dex=meteora, returns that DEX's 24h fee leaderboard. parameters: - name: dex in: query required: false description: Pool source. Omit to keep the legacy Raydium-only behavior. schema: type: string enum: [raydium, orca, meteora] default: raydium - name: excludeMeme in: query required: false description: >- Whether to exclude Meme pools. Defaults to true and only applies to dex=raydium; dex=orca keeps recognizable xStocks/Backpack securities pools by default; Meteora results are never filtered. schema: type: boolean default: true responses: '200': description: Pool directory for the requested DEX content: application/json: schema: type: object required: [data, meta] properties: data: type: array items: $ref: '#/components/schemas/Pool' meta: type: object required: [count, generatedAt] properties: count: type: integer minimum: 0 generatedAt: type: string format: date-time '400': $ref: '#/components/responses/BadRequest' '502': description: Orca or Meteora upstream unavailable content: application/json: schema: $ref: '#/components/schemas/Error' /api/public/pools/volume: get: summary: Get volume and fees for all monitored Raydium or Orca pools operationId: getAllPoolVolumes description: >- Returns every currently monitored Raydium pool, or every current Orca security TOP 50 pool when dex=orca, including pools with no captured swaps in the requested window. Results are ordered by window volume descending. parameters: - name: dex in: query required: false description: Captured chain-volume source. Meteora currently has no volume monitor. schema: type: string enum: [raydium, orca] default: raydium - name: interval in: query required: false description: UTC-aligned aggregation interval schema: type: string enum: [1m, 5m, 1h, 24h] default: 1h - name: limit in: query required: false description: Number of most recent intervals per pool. The retention-based maximum can be lower than 1000. schema: type: integer minimum: 1 maximum: 1000 default: 24 - name: excludeMeme in: query required: false description: Whether to exclude Meme pools. Orca keeps only recognizable xStocks/Backpack securities pools by default. schema: type: boolean default: true responses: '200': description: All pool metadata, aggregate totals, per-pool totals, and oldest-to-newest volume series content: application/json: schema: $ref: '#/components/schemas/AllPoolsVolumeResponse' '400': $ref: '#/components/responses/BadRequest' /api/public/pools/{poolId}/volume: get: summary: Get volume and fees for a monitored pool operationId: getPoolVolume parameters: - name: poolId in: path required: true description: Solana address of a currently monitored pool schema: type: string pattern: '^[1-9A-HJ-NP-Za-km-z]{32,44}$' - name: dex in: query required: false description: Captured chain-volume source schema: type: string enum: [raydium, orca] default: raydium - name: interval in: query required: false description: UTC-aligned aggregation interval schema: type: string enum: [1m, 5m, 1h, 24h] default: 1h - name: limit in: query required: false description: Number of most recent intervals. The retention-based maximum can be lower than 1000. schema: type: integer minimum: 1 maximum: 1000 default: 24 - name: excludeMeme in: query required: false description: Whether to exclude Meme pools. Set false to query a filtered Raydium pool or a non-security Orca pool. schema: type: boolean default: true responses: '200': description: Pool metadata, totals, and an oldest-to-newest volume series content: application/json: schema: $ref: '#/components/schemas/PoolVolumeResponse' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' /api/public/pool-creations/ws: get: summary: Subscribe to CLMM pool creations using WebSocket operationId: subscribePoolCreations security: [] description: >- RFC 6455 upgrade on the same origin; use wss with HTTPS. Public read-only stream, not SSE or Solana JSON-RPC. No subscribe message is needed. Omit after for live-only; after=0 replays all saved visible events. Follows publication order, filtering blacklisted pools and known creators. Only captured successful Raydium CLMM events; no CPMM or chain gap guarantee. Requires node server.mjs (pnpm start), not next start. parameters: - name: after in: query required: false description: Exclusive database-local publication cursor; omit for latest. Maximum 9007199254740991. schema: type: string pattern: '^(0|[1-9][0-9]*)$' responses: '101': description: Switching protocols; subsequent text messages follow x-websocket. '400': description: Invalid, repeated or unsupported query parameter. '409': description: Cursor is ahead of the current database; consumer must select a new starting point. '426': description: Ordinary HTTP request requires a WebSocket upgrade. '503': description: Event storage unavailable, mock mode, or connection limit reached. x-websocket: path: /api/public/pool-creations/ws authentication: none clientMessages: none heartbeatSeconds: 25 maxConnectionsDefault: 200 replayBatchSize: 100 scanIntervalMilliseconds: 1000 serverMessages: oneOf: - $ref: '#/components/schemas/PoolStreamReady' - $ref: '#/components/schemas/PoolStreamEvent' - $ref: '#/components/schemas/PoolStreamCheckpoint' /api/public/position-openings/ws: get: summary: Subscribe to openings, liquidity increases/decreases and closes for one or multiple original owners operationId: subscribePositionOpenings description: >- Read-only WebSocket for current monitored Raydium CLMM and Orca RWA pools. A signal is published only after original ticks, raw amounts, first funding and creator are known. Subsequent positive liquidity changes append position.increased or position.decreased; initial funding remains position.opened only. Fee-only collections do not emit decreases. Changes require a retained opening with known original owner and raw conditions. All four types share cursors and filters. Confirmed account closes append position.closed linked by openingEventId. Partial liquidity withdrawals and local expiry do not emit closes. Close matching requires a retained source snapshot with a known original owner. Replay retains ten days of historical opening and closing signals. No query parameter expands the monitored directory. No per-subscriber RPC is performed. parameters: - name: creator in: query required: true description: >- Repeat creator=ADDRESS_A&creator=ADDRESS_B for one connection. Supply 1–100 parameters (before deduplication); each must be a valid Solana original position owner, not fee payer or funding sponsor. Duplicates are deduplicated; comma-separated values are not accepted. style: form explode: true schema: type: array minItems: 1 maxItems: 100 items: {type: string, minLength: 32, maxLength: 44} - name: dex in: query schema: {type: string, enum: [all, raydium, orca], default: all} - name: after in: query description: >- Exclusive publication cursor (max 9007199254740991). Omit for live-only; 0 replays retained history. All selected addresses share one ordered cursor. Bind cursors to the sorted unique address set/DEX/database; reordering does not change scope. Changing the set requires reconnecting with a new subscription state (omit after or explicitly replay with 0 and deduplicate). Never merge old individual cursors by taking the maximum. schema: {type: string, pattern: '^(0|[1-9][0-9]*)$'} responses: '101': description: Switching protocols; subsequent messages follow x-websocket. '400': description: Invalid/missing address, over 100 creator parameters, invalid DEX/cursor, duplicate dex/after or unsupported parameter. '409': description: Cursor ahead of this database. '410': description: Cursor expired; caller must explicitly choose a new starting point. '426': description: Ordinary HTTP GET requires WebSocket upgrade. '503': description: Storage unavailable, mock mode or connection limit reached. x-websocket: path: /api/public/position-openings/ws authentication: none clientMessages: none scope: monitored-pools retentionDays: 10 maxCreators: 100 addressEncoding: repeated-creator-query-parameter eventOrdering: publication-sequence-across-all-selected-addresses heartbeatSeconds: 25 maxConnectionsDefault: 200 replayBatchSize: 100 scanIntervalMilliseconds: 1000 serverMessages: oneOf: - $ref: '#/components/schemas/PositionStreamReady' - $ref: '#/components/schemas/PositionStreamEvent' - $ref: '#/components/schemas/PositionCloseStreamEvent' - $ref: '#/components/schemas/PositionLiquidityStreamEvent' - $ref: '#/components/schemas/PoolStreamCheckpoint' components: schemas: PositionStreamReady: allOf: - $ref: '#/components/schemas/PoolStreamReady' - type: object required: [creator, creators, dex, scope, retentionDays] properties: creator: type: [string, 'null'] description: Legacy compatibility field; sole address for a one-address set, otherwise null. creators: type: array minItems: 1 maxItems: 100 uniqueItems: true items: {type: string} description: Sorted unique original-owner addresses; data.creator identifies each event's owner. dex: {type: string, enum: [all, raydium, orca]} scope: {const: monitored-pools} retentionDays: {const: 10} PositionStreamEvent: type: object required: [type, cursor, data] properties: type: {const: position.opened} cursor: type: string description: Publication sequence, independent of slot and other feed cursors. data: $ref: '#/components/schemas/PositionOpeningEvent' PositionCloseStreamEvent: type: object required: [type, cursor, data] properties: type: {const: position.closed} cursor: {type: string, description: Shares the opening stream publication sequence.} data: $ref: '#/components/schemas/PositionClosedEvent' PositionLiquidityStreamEvent: type: object required: [type, cursor, data] properties: type: {type: string, enum: [position.increased, position.decreased]} cursor: {type: string, description: Shares the opening/close publication cursor.} data: $ref: '#/components/schemas/PositionLiquidityEvent' LiquidityChangeToken: allOf: - $ref: '#/components/schemas/OpeningToken' - type: object required: [feeAmountRaw, transferFeeRaw] properties: feeAmountRaw: type: string pattern: '^(0|[1-9][0-9]{0,19})$' description: Separate Raydium decrease collected fees, otherwise zero; excludes rewards. transferFeeRaw: type: [string, 'null'] pattern: '^(0|[1-9][0-9]{0,19})$' description: Event transfer fee; null on legacy Raydium logs. May cover principal plus collected fees. PositionLiquidityEvent: description: Same-transaction decreases for a closed position are omitted; only position.closed is published for that position. Decreases on other positions remain. type: object required: [version, type, eventId, dex, programId, creator, poolAddress, positionAddress, nftMint, openingEventId, openingSignature, signature, eventIndex, slot, occurredAt, publishedAt, commitment, liquidityDelta, tickLower, tickUpper, tickSpacing, tokenA, tokenB] description: >- Subsequent positive liquidity addition or removal on a retained monitored position. Initial funding and zero-liquidity fee collections are excluded. Amounts are unsigned event magnitudes, not balances, percentages, slippage limits or guaranteed net receipts. A decrease is not an account close. Inner type matches the outer message type. properties: version: {const: 1} type: {type: string, enum: [position.increased, position.decreased]} eventId: {type: string, description: 'dex:signature:liquidity:eventIndex; deduplicate by this key.'} dex: {type: string, enum: [raydium, orca]} programId: {type: string} creator: {type: string, description: Original opening owner, not necessarily current signer.} poolAddress: {type: string} positionAddress: {type: string} nftMint: {type: [string, 'null']} openingEventId: {type: string} openingSignature: {type: string} signature: {type: string} eventIndex: {type: integer, minimum: 0, description: Index among this DEX transaction's liquidity change events including skipped initial funding.} slot: {type: integer, minimum: 1} occurredAt: {type: integer, description: Unix milliseconds; log observation time or backfill block time.} publishedAt: {type: integer} commitment: {const: confirmed} liquidityDelta: type: string pattern: '^[1-9][0-9]{0,38}$' description: Positive u128 absolute liquidity change; direction from type. Not remaining liquidity. tickLower: {type: integer} tickUpper: {type: integer} tickSpacing: {type: [integer, 'null']} tokenA: $ref: '#/components/schemas/LiquidityChangeToken' tokenB: $ref: '#/components/schemas/LiquidityChangeToken' PositionClosedEvent: type: object description: >- Successful on-chain position account closure, linked to the retained source opening. Original creator is preserved across NFT transfers. This is not partial withdrawal or local expiry. No redeemed amounts or profit are inferred from opening deposits. required: [version, type, eventId, dex, programId, poolAddress, creator, signature, slot, closedAt, publishedAt, commitment, positionAddress, nftMint, openingEventId, openingSignature] properties: version: {const: 1} type: {const: position.closed} eventId: type: string description: 'Idempotency key: dex:closeSignature:close:positionAddress.' dex: {type: string, enum: [raydium, orca]} programId: type: string enum: [CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK, whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc] poolAddress: {type: string} creator: {type: string, description: Original position owner, not the close signer.} signature: {type: string, description: Confirmed closing transaction signature.} slot: {type: integer, minimum: 1} closedAt: type: integer description: Close block time in Unix milliseconds, falling back to local processing time. publishedAt: {type: integer, description: Publication time in Unix milliseconds.} commitment: {const: confirmed} positionAddress: {type: string} nftMint: {type: [string, 'null']} openingEventId: type: [string, 'null'] description: Matches original position.opened eventId; null for legacy PDA-only snapshots. openingSignature: type: [string, 'null'] description: Source opening signature, null if unavailable on legacy snapshots. OpeningToken: type: object required: [mint, symbol, decimals, amountRaw] properties: mint: {type: string} symbol: {type: string} decimals: {type: integer, minimum: 0, maximum: 255} amountRaw: type: string pattern: '^(0|[1-9][0-9]{0,19})$' description: Captured u64 raw token units; use BigInt/BN, no ScaledUiAmount multiplier or slippage allowance. PositionOpeningEvent: type: object description: >- Immutable historical opening signal at confirmed commitment, not current position state. Construct a new transaction using current DEX state/quotes and caller-selected budget/slippage. Token A/B and ticks preserve chain order regardless of display direction. required: [version, type, eventId, dex, programId, poolAddress, creator, signature, eventIndex, slot, openedAt, fundingSignature, fundingSlot, fundedAt, publishedAt, commitment, positionAddress, nftMint, tickLower, tickUpper, tickSpacing, tokenA, tokenB, amountUsd, display] properties: version: {const: 1} type: {const: position.opened} eventId: type: string description: 'Idempotency key: dex:signature:open:eventIndex.' dex: {type: string, enum: [raydium, orca]} programId: type: string enum: [CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK, whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc] poolAddress: {type: string} creator: {type: string, description: Original position owner.} signature: {type: string, description: Opening transaction signature.} eventIndex: {type: integer, minimum: 0, description: Decoded event index, not instruction index.} slot: {type: integer, minimum: 1} openedAt: type: integer description: Unix milliseconds; WS observation time or backfill block time. fundingSignature: type: string description: First funding transaction; may differ from opening for Orca. fundingSlot: {type: integer, minimum: 1} fundedAt: type: integer description: First funding in Unix milliseconds; WS observation time or backfill block time. publishedAt: {type: integer, description: Local publication time in Unix milliseconds.} commitment: {const: confirmed} positionAddress: type: [string, 'null'] description: Source position PDA; unavailable in Raydium opening logs. Do not reuse for your new position. nftMint: type: [string, 'null'] description: Source NFT; null when absent from captured metadata. tickLower: {type: integer, minimum: -443636, maximum: 443636} tickUpper: {type: integer, minimum: -443636, maximum: 443636} tickSpacing: type: [integer, 'null'] minimum: 1 description: Known Orca pool spacing; currently null for Raydium, read from pool when quoting. tokenA: $ref: '#/components/schemas/OpeningToken' tokenB: $ref: '#/components/schemas/OpeningToken' amountUsd: type: number minimum: 0 description: Cached-price estimate; may be zero when prices are unavailable. Not an execution budget. display: type: object required: [baseMint, quoteMint, priceLower, priceUpper] properties: baseMint: {type: string} quoteMint: {type: string} priceLower: {type: number} priceUpper: {type: number} description: Display-only quote/base prices, potentially reversed and UI-scaled. Never reconstruct chain ticks from these. PoolStreamReady: type: object required: [type, version, cursor, latestCursor, heartbeatSeconds] properties: type: const: ready version: const: 1 cursor: type: string description: Starting cursor; safe initial checkpoint. Never skip replay by saving latestCursor. latestCursor: type: string description: High-water mark at handshake, not a processed checkpoint. heartbeatSeconds: type: number const: 25 PoolStreamCheckpoint: type: object required: [type, cursor] properties: type: const: checkpoint cursor: type: string description: Save only after preceding events have been committed; may advance over hidden records. PoolStreamEvent: type: object required: [type, cursor, data] properties: type: const: pool.created cursor: type: string description: Persistent publication sequence, not slot. Use after to resume exclusively after it. data: $ref: '#/components/schemas/PoolCreatedEvent' PoolCreationToken: type: object required: [mint, symbol, name, icon, decimals] properties: mint: type: string symbol: type: [string, 'null'] name: type: [string, 'null'] icon: type: [string, 'null'] decimals: type: [integer, 'null'] minimum: 0 maximum: 255 PoolCreatedEvent: type: object description: Immutable v1 pool-creation WebSocket event. Chain token0/token1 order and raw Q64.64 price. required: [version, type, eventId, signature, slot, blockTime, detectedAt, programId, poolType, poolAddress, creator, ammConfig, sqrtPriceX64, tick, tickSpacing, vaultA, vaultB, tokenA, tokenB] properties: version: const: 1 type: const: raydium.pool.created eventId: type: string description: 'Idempotency key: signature:pool:poolAddress; multiple pools per transaction are distinct.' signature: type: string slot: type: integer minimum: 0 blockTime: type: [integer, 'null'] description: Unix seconds; usually null for live logs. detectedAt: type: integer description: Sender observation time in Unix milliseconds, not pool opening time. programId: const: CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK poolType: const: clmm poolAddress: type: string creator: type: [string, 'null'] ammConfig: type: [string, 'null'] sqrtPriceX64: type: string pattern: '^[1-9][0-9]{0,38}$' description: Initial u128 Q64.64 sqrt price. Preserve as string or BigInt. tick: type: integer minimum: -443636 maximum: 443636 tickSpacing: type: integer minimum: 1 maximum: 65535 vaultA: type: string vaultB: type: string tokenA: $ref: '#/components/schemas/PoolCreationToken' tokenB: $ref: '#/components/schemas/PoolCreationToken' Token: type: object required: [symbol, mint, decimals, logoURI] properties: symbol: type: string mint: type: string decimals: description: Token decimals from the DEX metadata, or null when the upstream does not provide it oneOf: - type: integer minimum: 0 - type: 'null' logoURI: description: Token logo URL from the DEX metadata, or null when unavailable oneOf: - type: string format: uri - type: 'null' Pool: type: object required: [id, dex, pair, baseToken, quoteToken, feeRate, feeBps, tvlUsd, volume24hUsd, fees24hUsd, manuallyAdded, addedAt] properties: id: type: string dex: type: string enum: [raydium, orca, meteora] pair: type: string baseToken: $ref: '#/components/schemas/Token' quoteToken: $ref: '#/components/schemas/Token' feeRate: type: number feeBps: type: number tvlUsd: type: number volume24hUsd: description: 24h window volume in USD; only provided for orca/meteora leaderboard entries oneOf: - type: number - type: 'null' fees24hUsd: description: 24h window fees in USD; only provided for orca/meteora leaderboard entries oneOf: - type: number - type: 'null' manuallyAdded: type: boolean addedAt: oneOf: - type: string format: date-time - type: 'null' VolumeBucket: type: object required: [startTime, endTime, complete, volumeUsd, feesUsd, buyVolumeUsd, sellVolumeUsd, trades] properties: startTime: type: string format: date-time endTime: type: string format: date-time complete: type: boolean volumeUsd: type: number minimum: 0 feesUsd: type: number minimum: 0 description: Actual captured swap fees. Historical Raydium candles without fee backfill can be 0. buyVolumeUsd: type: number minimum: 0 sellVolumeUsd: type: number minimum: 0 trades: type: integer minimum: 0 PoolVolumeResponse: type: object required: [pool, query, totals, data] properties: pool: $ref: '#/components/schemas/Pool' query: type: object required: [dex, interval, limit, excludeMeme, windowStart, windowEnd] properties: dex: type: string enum: [raydium, orca] interval: type: string enum: [1m, 5m, 1h, 24h] limit: type: integer excludeMeme: type: boolean windowStart: type: string format: date-time windowEnd: type: string format: date-time totals: type: object required: [volumeUsd, feesUsd, buyVolumeUsd, sellVolumeUsd, trades] properties: volumeUsd: type: number feesUsd: type: number buyVolumeUsd: type: number sellVolumeUsd: type: number trades: type: integer data: type: array items: $ref: '#/components/schemas/VolumeBucket' VolumeTotals: type: object required: [volumeUsd, feesUsd, buyVolumeUsd, sellVolumeUsd, trades] properties: volumeUsd: type: number minimum: 0 feesUsd: type: number minimum: 0 buyVolumeUsd: type: number minimum: 0 sellVolumeUsd: type: number minimum: 0 trades: type: integer minimum: 0 AllPoolsVolumeItem: type: object required: [pool, totals, data] properties: pool: $ref: '#/components/schemas/Pool' totals: $ref: '#/components/schemas/VolumeTotals' data: type: array items: $ref: '#/components/schemas/VolumeBucket' AllPoolsVolumeResponse: type: object required: [query, totals, data] properties: query: type: object required: [dex, interval, limit, excludeMeme, windowStart, windowEnd] properties: dex: type: string enum: [raydium, orca] interval: type: string enum: [1m, 5m, 1h, 24h] limit: type: integer excludeMeme: type: boolean windowStart: type: string format: date-time windowEnd: type: string format: date-time totals: allOf: - $ref: '#/components/schemas/VolumeTotals' - type: object required: [pools] properties: pools: type: integer minimum: 0 data: type: array items: $ref: '#/components/schemas/AllPoolsVolumeItem' Error: type: object required: [error] properties: error: type: object required: [message] additionalProperties: true properties: message: type: string responses: BadRequest: description: Invalid pool address or query parameter content: application/json: schema: $ref: '#/components/schemas/Error' NotFound: description: Pool is not currently monitored or is excluded by the default Meme filter content: application/json: schema: $ref: '#/components/schemas/Error' ``` # Runnable Node.js subscriber ```javascript // node examples/subscribe-position-openings.mjs https://dashboard.example ADDRESS_A,ADDRESS_B [all|raydium|orca] // Requires the project's `ws` dependency. This example prints signals; it does not place orders. import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs' import { WebSocket } from 'ws' const [, , origin, creatorList, dex = 'all'] = process.argv if (!origin || !creatorList) throw new Error('Usage: node examples/subscribe-position-openings.mjs DASHBOARD_ORIGIN ADDRESS_A[,ADDRESS_B,...] [all|raydium|orca]') const requestedCreators = creatorList.split(',').map(address => address.trim()) if (requestedCreators.length > 100 || requestedCreators.some(address => !address)) throw new Error('Provide 1–100 non-empty addresses') const creators = [...new Set(requestedCreators)].sort() const base = new URL('/api/public/position-openings/ws', origin) base.protocol = base.protocol === 'https:' || base.protocol === 'wss:' ? 'wss:' : 'ws:' for (const creator of creators) base.searchParams.append('creator', creator) base.searchParams.set('dex', dex) const scope = base.toString() const file = process.env.CURSOR_FILE || './position-opening-cursor.json' const saved = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : null if (saved && saved.scope !== scope) throw new Error('Use a different CURSOR_FILE for each origin/address-set/DEX subscription') let cursor = saved?.cursor // omitted on first connection: live events only let retryMs = 1000 let stopped = false let current async function handleOpening(event) { // Replace with your own durable, eventId-idempotent signal handler. // A replay is historical: check fundedAt freshness and source position state. // Use BigInt(event.tokenA.amountRaw) / BigInt(event.tokenB.amountRaw), chain ticks, // and a fresh DEX SDK quote to construct your own transaction and slippage limits. console.log(JSON.stringify(event)) } async function handleClose(event) { // Resolve your own copied position through event.openingEventId (or source PDA). // Never close a different position just because its creator/pool matches. // Commit handling idempotently by event.eventId before advancing the cursor. console.log(JSON.stringify(event)) } async function handleLiquidityChange(event) { // Match your copied position using openingEventId; deduplicate with eventId. // liquidityDelta and token amounts are unsigned magnitudes, not balances. // Requote with current DEX state; a decrease does not imply account closure. console.log(JSON.stringify(event)) } function checkpoint(next) { writeFileSync(file + '.tmp', JSON.stringify({ scope, cursor: next })) renameSync(file + '.tmp', file) cursor = next } function connect() { if (stopped) return const url = new URL(base) if (cursor !== undefined) url.searchParams.set('after', cursor) const ws = current = new WebSocket(url) let work = Promise.resolve() let failed = false ws.on('open', () => { retryMs = 1000 }) ws.on('message', raw => { work = work.then(async () => { if (failed || stopped) return const message = JSON.parse(raw.toString()) if (message.type === 'position.opened') await handleOpening(message.data) if (message.type === 'position.closed') await handleClose(message.data) if (['position.increased', 'position.decreased'].includes(message.type)) await handleLiquidityChange(message.data) if (['ready', 'position.opened', 'position.closed', 'position.increased', 'position.decreased', 'checkpoint'].includes(message.type)) checkpoint(message.cursor) }).catch(error => { failed = true; console.error(error); ws.terminate() }) }) ws.on('unexpected-response', (_, response) => { console.error(`WebSocket handshake HTTP ${response.statusCode}`) // Invalid scope or missing history needs an explicit choice of a new starting point. if ([400, 409, 410].includes(response.statusCode)) stopped = true response.resume(); ws.terminate() }) ws.on('error', error => console.error(error.message)) ws.on('close', async code => { await work // never advance a new connection ahead of unfinished old messages if (code === 1008) { stopped = true; console.error('Subscription cursor/policy reset required') } if (stopped) return const wait = retryMs + Math.random() * 500 retryMs = Math.min(30000, retryMs * 2) setTimeout(connect, wait) }) } for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => { stopped = true; current?.close() }) connect() ```