lpsignal 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/dist/client.d.ts +128 -0
- package/dist/client.js +195 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/stream.d.ts +146 -0
- package/dist/stream.js +310 -0
- package/dist/stream.js.map +1 -0
- package/dist/types.d.ts +344 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/webhook.d.ts +22 -0
- package/dist/webhook.js +50 -0
- package/dist/webhook.js.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LPSignal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# lpsignal (Node.js)
|
|
2
|
+
|
|
3
|
+
Official Node.js SDK for [LPSignal](https://lpsignal.app): net-of-IL APR signals for concentrated-liquidity pools.
|
|
4
|
+
ESM, typed, Node ≥ 20. One runtime dependency (`ws`).
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install lpsignal
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## REST
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { LPSignal, LPSignalError } from 'lpsignal';
|
|
14
|
+
|
|
15
|
+
const lps = new LPSignal({ apiKey: process.env.LPSIGNAL_API_KEY }); // key optional for public endpoints
|
|
16
|
+
|
|
17
|
+
const { pools } = await lps.pools({ chain: 'base', window: 168, minTvlUsd: 1e6 });
|
|
18
|
+
const detail = await lps.pool('base', pools[0].address); // every (window × range) metric
|
|
19
|
+
const bt = await lps.backtest('base', pools[0].address, { rangePct: 5, days: 7 });
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await lps.walletPositions('0x...');
|
|
23
|
+
} catch (e) {
|
|
24
|
+
if (e instanceof LPSignalError && e.code === 'pro_required') { /* upgrade */ }
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
| Method | Endpoint | Key |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `health()` · `chains()` | `/v1/health` · `/v1/chains` | – |
|
|
31
|
+
| `pools({ chain, class, window, minTvlUsd, limit, offset })` | `GET /v1/pools` | – |
|
|
32
|
+
| `pool(chain, address)` · `poolHours(chain, address, { hours })` | `GET /v1/pools/:chain/:address[/hours]` | – |
|
|
33
|
+
| `backtest(chain, address, { rangePct, days })` | `GET …/backtest` | – |
|
|
34
|
+
| `signals({ kind, limit, before })` · `signal(id)` | `GET /v1/signals[/:id]` | optional |
|
|
35
|
+
| `signalStats({ days })` | `GET /v1/signals/stats` — the public track record | – |
|
|
36
|
+
| `iterateSignals({ kind })` | every page, newest first | optional |
|
|
37
|
+
| `signalsAfter(id)` | everything newer than `id`, oldest first | optional |
|
|
38
|
+
| `smartLps({ windowDays, chain, limit })` | `GET /v1/smart-lps` | optional |
|
|
39
|
+
| `walletPositions(owner)` | `GET /v1/smart-lps/:owner/positions` | Pro |
|
|
40
|
+
| `follows()` · `follow(owner)` · `unfollow(owner)` | `/v1/me/follows` | Pro |
|
|
41
|
+
| `me()` · `setWebhook(url)` · `deleteWebhook()` · `telegramLink()` | `/v1/me…` | yes |
|
|
42
|
+
| `createApiKey()` | `POST /v1/me/api-key` — replaces the key, returned once | yes |
|
|
43
|
+
| `billing({ refresh })` · `checkout(tier)` · `billingPortal()` | `/v1/billing…` | yes |
|
|
44
|
+
|
|
45
|
+
Errors are `LPSignalError` with `status`, `code` (the API's `error` field), `body` and `requestId`. A `429` on a
|
|
46
|
+
GET, PUT or DELETE is retried after its `Retry-After` (`maxRetries`, default 2).
|
|
47
|
+
|
|
48
|
+
## Stream
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { LPSignal, SignalStream, FileLastIdStore } from 'lpsignal';
|
|
52
|
+
|
|
53
|
+
const stream = new SignalStream({
|
|
54
|
+
client: new LPSignal({ apiKey: process.env.LPSIGNAL_API_KEY }), // Basic or Pro
|
|
55
|
+
store: new FileLastIdStore('./lpsignal-state.json'),
|
|
56
|
+
onSignal: async (signal, { source }) => {
|
|
57
|
+
// called once per signal, in id order, never concurrently; source = 'rest' | 'replay' | 'live'
|
|
58
|
+
},
|
|
59
|
+
onEvent: (e) => { if (e.type === 'fatal') console.error(e.error.message); },
|
|
60
|
+
});
|
|
61
|
+
await stream.start();
|
|
62
|
+
// …
|
|
63
|
+
await stream.stop();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- **First start** with an empty store: begins after the newest signal that exists now (or after `since` if given).
|
|
67
|
+
- **Every (re)connect**: first fetches everything after the saved id over REST, repeating until a pass finds
|
|
68
|
+
nothing new, then connects with `?since=<id>`; anything at or below the saved id is dropped. No gap however long
|
|
69
|
+
you were away, and no duplicates while the process runs.
|
|
70
|
+
- **Your handler decides progress**: the id is saved only after `onSignal` resolves. If it throws, the connection
|
|
71
|
+
is dropped and the signal is offered again. A crash after the handler but before the save also offers it again
|
|
72
|
+
after the restart: delivery is **at-least-once**, so make the handler idempotent on `signal.id`.
|
|
73
|
+
- **No repeats even across crashes**: keep the last id in the same database transaction as your side effects
|
|
74
|
+
(write it inside `onSignal`), and pass a `store` that reads and writes that same row; its `save()` must only move
|
|
75
|
+
forward (e.g. `UPDATE … SET last_id = GREATEST(last_id, $1)`), since the stream also saves the starting point.
|
|
76
|
+
- `stop()` waits for the signal being handled; nothing new is handed over after it is called. Don't await `stop()`
|
|
77
|
+
from inside `onSignal` (it would wait for itself). After a `fatal` event, call `stop()` before `start()` again.
|
|
78
|
+
- **Fatal** (the stream stops): invalid key (401), a plan without the stream (402), or the plan expiring (4402).
|
|
79
|
+
Everything else (network drops, server restarts, 429 too many connections) reconnects with backoff.
|
|
80
|
+
|
|
81
|
+
## Webhooks
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import express from 'express';
|
|
85
|
+
import { verifyWebhook, WebhookVerificationError } from 'lpsignal';
|
|
86
|
+
|
|
87
|
+
app.post('/lpsignal', express.raw({ type: 'application/json' }), (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const event = verifyWebhook(req.body, req.headers, process.env.LPSIGNAL_WEBHOOK_SECRET!);
|
|
90
|
+
// deliveries are at-least-once: skip event.deliveryId if already handled
|
|
91
|
+
res.sendStatus(200);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
if (e instanceof WebhookVerificationError) return res.status(400).send(e.reason);
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Pass the raw body (a `Buffer` or string), never re-serialised JSON. Deliveries older than 5 minutes are rejected
|
|
100
|
+
(`toleranceSec`); every retry is signed afresh.
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { Backtest, BillingStatus, Chain, ChainStatus, Follow, Health, Leaderboard, Me, PairClass, PoolDetail, PoolHour, PoolsPage, Signal, SignalKind, SignalsPage, SignalStats, TelegramLink, WalletPositions, WebhookRegistration, WindowHours } from './types.js';
|
|
2
|
+
export declare const DEFAULT_BASE_URL = "https://api.lpsignal.app";
|
|
3
|
+
/** A non-2xx answer from the API. `code` is the API's stable `error` field (e.g. `rate_limited`, `pro_required`). */
|
|
4
|
+
export declare class LPSignalError extends Error {
|
|
5
|
+
readonly status: number;
|
|
6
|
+
readonly body: unknown;
|
|
7
|
+
/** `x-request-id` of the failed request: include it when contacting support */
|
|
8
|
+
readonly requestId: string | null;
|
|
9
|
+
readonly code: string | null;
|
|
10
|
+
constructor(status: number, body: unknown,
|
|
11
|
+
/** `x-request-id` of the failed request: include it when contacting support */
|
|
12
|
+
requestId?: string | null);
|
|
13
|
+
}
|
|
14
|
+
export interface LPSignalOptions {
|
|
15
|
+
/** `lps_...`. Optional: public endpoints work without one (opportunity signals then arrive 24h late). */
|
|
16
|
+
apiKey?: string;
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
/** per-request timeout, default 15 s */
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
/** how many times a 429 on a GET/PUT/DELETE is retried after its Retry-After, default 2 (0 = never) */
|
|
21
|
+
maxRetries?: number;
|
|
22
|
+
/** inject a fetch implementation (tests, proxies) */
|
|
23
|
+
fetch?: typeof fetch;
|
|
24
|
+
}
|
|
25
|
+
type Query = Record<string, string | number | undefined>;
|
|
26
|
+
export interface PoolsQuery {
|
|
27
|
+
chain?: Chain;
|
|
28
|
+
class?: PairClass;
|
|
29
|
+
/** default 168 (7 days) */
|
|
30
|
+
window?: WindowHours;
|
|
31
|
+
minTvlUsd?: number;
|
|
32
|
+
/** 1..100, default 50 */
|
|
33
|
+
limit?: number;
|
|
34
|
+
offset?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface SignalsQuery {
|
|
37
|
+
kind?: SignalKind;
|
|
38
|
+
/** 1..100, default 50 */
|
|
39
|
+
limit?: number;
|
|
40
|
+
/** only signals with a smaller id (the `next` of the previous page) */
|
|
41
|
+
before?: string;
|
|
42
|
+
}
|
|
43
|
+
/** REST client for the LPSignal API. */
|
|
44
|
+
export declare class LPSignal {
|
|
45
|
+
readonly apiKey: string | null;
|
|
46
|
+
readonly baseUrl: string;
|
|
47
|
+
private readonly timeoutMs;
|
|
48
|
+
private readonly maxRetries;
|
|
49
|
+
private readonly fetchImpl;
|
|
50
|
+
constructor(opts?: LPSignalOptions);
|
|
51
|
+
/** The stream URL derived from the base URL (https → wss). */
|
|
52
|
+
get streamUrl(): string;
|
|
53
|
+
request<T>(method: string, path: string, opts?: {
|
|
54
|
+
query?: Query;
|
|
55
|
+
body?: unknown;
|
|
56
|
+
}): Promise<T>;
|
|
57
|
+
health(): Promise<Health>;
|
|
58
|
+
/** Scan progress per chain. */
|
|
59
|
+
chains(): Promise<ChainStatus[]>;
|
|
60
|
+
/** Active pools ranked by their best range's net APR over `window` hours. */
|
|
61
|
+
pools(query?: PoolsQuery): Promise<PoolsPage>;
|
|
62
|
+
/** One pool with every (window, range) metric. `address` is the pool id for Uniswap v4. */
|
|
63
|
+
pool(chain: Chain, address: string): Promise<PoolDetail>;
|
|
64
|
+
/** Hourly aggregates, oldest first (at most 720 hours). */
|
|
65
|
+
poolHours(chain: Chain, address: string, opts?: {
|
|
66
|
+
hours?: number;
|
|
67
|
+
}): Promise<PoolHour[]>;
|
|
68
|
+
/** Backtest a symmetric range of ±`rangePct`% (0 = full range) over the last `days` (1..30, default 7). */
|
|
69
|
+
backtest(chain: Chain, address: string, opts: {
|
|
70
|
+
rangePct: number;
|
|
71
|
+
days?: number;
|
|
72
|
+
}): Promise<Backtest>;
|
|
73
|
+
/** One page of signals, newest first. Without a paid key, opportunities appear once they are 24h old. */
|
|
74
|
+
signals(query?: SignalsQuery): Promise<SignalsPage>;
|
|
75
|
+
/** The public track record over the last `days` (7..365, default 30). */
|
|
76
|
+
signalStats(opts?: {
|
|
77
|
+
days?: number;
|
|
78
|
+
}): Promise<SignalStats>;
|
|
79
|
+
signal(id: string): Promise<Signal>;
|
|
80
|
+
/** Every signal matching `query`, newest first, following the `next` cursor page by page. */
|
|
81
|
+
iterateSignals(query?: Omit<SignalsQuery, 'before'>): AsyncGenerator<Signal>;
|
|
82
|
+
/**
|
|
83
|
+
* Every signal visible to this key with an id greater than `afterId`, oldest first. This is how a consumer that was
|
|
84
|
+
* offline catches up (the stream itself only replays the last 24 hours).
|
|
85
|
+
*/
|
|
86
|
+
signalsAfter(afterId: string): Promise<Signal[]>;
|
|
87
|
+
/** Wallets ranked by LP pnl versus holding. Without Pro: the top 10 with masked addresses. */
|
|
88
|
+
smartLps(query?: {
|
|
89
|
+
windowDays?: 30 | 90;
|
|
90
|
+
chain?: Chain;
|
|
91
|
+
limit?: number;
|
|
92
|
+
}): Promise<Leaderboard>;
|
|
93
|
+
/** A wallet's open and closed positions (Pro). */
|
|
94
|
+
walletPositions(owner: string, opts?: {
|
|
95
|
+
limit?: number;
|
|
96
|
+
}): Promise<WalletPositions>;
|
|
97
|
+
/** Wallets you follow (Pro). */
|
|
98
|
+
follows(): Promise<Follow[]>;
|
|
99
|
+
/** Follow a wallet: its new positions of $10k+ become smart_lp signals for you (Pro, up to 50). */
|
|
100
|
+
follow(owner: string): Promise<{
|
|
101
|
+
owner: string;
|
|
102
|
+
following: true;
|
|
103
|
+
}>;
|
|
104
|
+
unfollow(owner: string): Promise<void>;
|
|
105
|
+
me(): Promise<Me>;
|
|
106
|
+
/** Set or replace the webhook. The signing secret is returned only here. */
|
|
107
|
+
setWebhook(url: string): Promise<WebhookRegistration>;
|
|
108
|
+
deleteWebhook(): Promise<void>;
|
|
109
|
+
/** Create a new API key for this account and return it (shown only here). The key used for this call stops working. */
|
|
110
|
+
createApiKey(): Promise<{
|
|
111
|
+
apiKey: string;
|
|
112
|
+
}>;
|
|
113
|
+
/** A one-time code: send `/start <code>` to the LPSignal Telegram bot. */
|
|
114
|
+
telegramLink(): Promise<TelegramLink>;
|
|
115
|
+
/** Current plan. `refresh: true` re-reads Stripe first (use it right after a checkout). */
|
|
116
|
+
billing(opts?: {
|
|
117
|
+
refresh?: boolean;
|
|
118
|
+
}): Promise<BillingStatus>;
|
|
119
|
+
/** A Stripe Checkout URL for a monthly plan. */
|
|
120
|
+
checkout(tier: 'basic' | 'pro'): Promise<{
|
|
121
|
+
url: string;
|
|
122
|
+
}>;
|
|
123
|
+
/** A Stripe Customer Portal URL (change plan, cancel, invoices). */
|
|
124
|
+
billingPortal(): Promise<{
|
|
125
|
+
url: string;
|
|
126
|
+
}>;
|
|
127
|
+
}
|
|
128
|
+
export {};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
export const DEFAULT_BASE_URL = 'https://api.lpsignal.app';
|
|
2
|
+
/** A non-2xx answer from the API. `code` is the API's stable `error` field (e.g. `rate_limited`, `pro_required`). */
|
|
3
|
+
export class LPSignalError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
body;
|
|
6
|
+
requestId;
|
|
7
|
+
code;
|
|
8
|
+
constructor(status, body,
|
|
9
|
+
/** `x-request-id` of the failed request: include it when contacting support */
|
|
10
|
+
requestId = null) {
|
|
11
|
+
const code = body && typeof body === 'object' && typeof body.error === 'string' ? body.error : null;
|
|
12
|
+
super(`LPSignal API ${status}${code ? ` ${code}` : ''}${requestId ? ` (request ${requestId})` : ''}: ${typeof body === 'string' ? body : JSON.stringify(body)}`);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.body = body;
|
|
15
|
+
this.requestId = requestId;
|
|
16
|
+
this.name = 'LPSignalError';
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const enc = encodeURIComponent;
|
|
21
|
+
const IDEMPOTENT = new Set(['GET', 'PUT', 'DELETE']);
|
|
22
|
+
const MAX_RETRY_WAIT_MS = 60_000;
|
|
23
|
+
/** REST client for the LPSignal API. */
|
|
24
|
+
export class LPSignal {
|
|
25
|
+
apiKey;
|
|
26
|
+
baseUrl;
|
|
27
|
+
timeoutMs;
|
|
28
|
+
maxRetries;
|
|
29
|
+
fetchImpl;
|
|
30
|
+
constructor(opts = {}) {
|
|
31
|
+
if (opts.apiKey !== undefined && !opts.apiKey.startsWith('lps_'))
|
|
32
|
+
throw new Error('LPSignal: apiKey must start with "lps_"');
|
|
33
|
+
this.apiKey = opts.apiKey ?? null;
|
|
34
|
+
this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
35
|
+
this.timeoutMs = opts.timeoutMs ?? 15_000;
|
|
36
|
+
this.maxRetries = opts.maxRetries ?? 2;
|
|
37
|
+
this.fetchImpl = opts.fetch ?? fetch;
|
|
38
|
+
}
|
|
39
|
+
/** The stream URL derived from the base URL (https → wss). */
|
|
40
|
+
get streamUrl() {
|
|
41
|
+
return `${this.baseUrl.replace(/^http/, 'ws')}/v1/stream`;
|
|
42
|
+
}
|
|
43
|
+
async request(method, path, opts = {}) {
|
|
44
|
+
const url = new URL(this.baseUrl + path);
|
|
45
|
+
for (const [k, v] of Object.entries(opts.query ?? {}))
|
|
46
|
+
if (v !== undefined)
|
|
47
|
+
url.searchParams.set(k, String(v));
|
|
48
|
+
const headers = { accept: 'application/json' };
|
|
49
|
+
if (this.apiKey)
|
|
50
|
+
headers.authorization = `Bearer ${this.apiKey}`;
|
|
51
|
+
if (opts.body !== undefined)
|
|
52
|
+
headers['content-type'] = 'application/json';
|
|
53
|
+
for (let attempt = 0;; attempt++) {
|
|
54
|
+
const res = await this.fetchImpl(url, {
|
|
55
|
+
method,
|
|
56
|
+
headers,
|
|
57
|
+
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
|
|
58
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
59
|
+
});
|
|
60
|
+
const text = await res.text();
|
|
61
|
+
let parsed = text;
|
|
62
|
+
if (text) {
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(text);
|
|
65
|
+
}
|
|
66
|
+
catch { /* keep the raw text */ }
|
|
67
|
+
}
|
|
68
|
+
if (res.ok)
|
|
69
|
+
return (text ? parsed : undefined);
|
|
70
|
+
if (res.status === 429 && IDEMPOTENT.has(method) && attempt < this.maxRetries) {
|
|
71
|
+
const after = Number(res.headers.get('retry-after') ?? parsed?.retryAfterSec);
|
|
72
|
+
const waitMs = Math.min(MAX_RETRY_WAIT_MS, Number.isFinite(after) && after > 0 ? after * 1000 : 1000);
|
|
73
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
throw new LPSignalError(res.status, parsed, res.headers.get('x-request-id'));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ── status ───────────────────────────────────────────────────────────────
|
|
80
|
+
health() {
|
|
81
|
+
return this.request('GET', '/v1/health');
|
|
82
|
+
}
|
|
83
|
+
/** Scan progress per chain. */
|
|
84
|
+
async chains() {
|
|
85
|
+
return (await this.request('GET', '/v1/chains')).chains;
|
|
86
|
+
}
|
|
87
|
+
// ── pools ────────────────────────────────────────────────────────────────
|
|
88
|
+
/** Active pools ranked by their best range's net APR over `window` hours. */
|
|
89
|
+
pools(query = {}) {
|
|
90
|
+
return this.request('GET', '/v1/pools', { query: { ...query } });
|
|
91
|
+
}
|
|
92
|
+
/** One pool with every (window, range) metric. `address` is the pool id for Uniswap v4. */
|
|
93
|
+
pool(chain, address) {
|
|
94
|
+
return this.request('GET', `/v1/pools/${enc(chain)}/${enc(address)}`);
|
|
95
|
+
}
|
|
96
|
+
/** Hourly aggregates, oldest first (at most 720 hours). */
|
|
97
|
+
async poolHours(chain, address, opts = {}) {
|
|
98
|
+
return (await this.request('GET', `/v1/pools/${enc(chain)}/${enc(address)}/hours`, { query: opts })).hours;
|
|
99
|
+
}
|
|
100
|
+
/** Backtest a symmetric range of ±`rangePct`% (0 = full range) over the last `days` (1..30, default 7). */
|
|
101
|
+
backtest(chain, address, opts) {
|
|
102
|
+
return this.request('GET', `/v1/pools/${enc(chain)}/${enc(address)}/backtest`, { query: opts });
|
|
103
|
+
}
|
|
104
|
+
// ── signals ──────────────────────────────────────────────────────────────
|
|
105
|
+
/** One page of signals, newest first. Without a paid key, opportunities appear once they are 24h old. */
|
|
106
|
+
signals(query = {}) {
|
|
107
|
+
return this.request('GET', '/v1/signals', { query: { ...query } });
|
|
108
|
+
}
|
|
109
|
+
/** The public track record over the last `days` (7..365, default 30). */
|
|
110
|
+
signalStats(opts = {}) {
|
|
111
|
+
return this.request('GET', '/v1/signals/stats', { query: opts });
|
|
112
|
+
}
|
|
113
|
+
signal(id) {
|
|
114
|
+
return this.request('GET', `/v1/signals/${enc(id)}`);
|
|
115
|
+
}
|
|
116
|
+
/** Every signal matching `query`, newest first, following the `next` cursor page by page. */
|
|
117
|
+
async *iterateSignals(query = {}) {
|
|
118
|
+
let before;
|
|
119
|
+
for (;;) {
|
|
120
|
+
const page = await this.signals({ ...query, limit: query.limit ?? 100, ...(before ? { before } : {}) });
|
|
121
|
+
for (const s of page.signals)
|
|
122
|
+
yield s;
|
|
123
|
+
if (!page.next)
|
|
124
|
+
return;
|
|
125
|
+
before = page.next;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Every signal visible to this key with an id greater than `afterId`, oldest first. This is how a consumer that was
|
|
130
|
+
* offline catches up (the stream itself only replays the last 24 hours).
|
|
131
|
+
*/
|
|
132
|
+
async signalsAfter(afterId) {
|
|
133
|
+
const after = BigInt(afterId);
|
|
134
|
+
const newer = [];
|
|
135
|
+
for await (const s of this.iterateSignals()) {
|
|
136
|
+
if (BigInt(s.id) <= after)
|
|
137
|
+
break;
|
|
138
|
+
newer.push(s);
|
|
139
|
+
}
|
|
140
|
+
return newer.reverse();
|
|
141
|
+
}
|
|
142
|
+
// ── smart LPs ────────────────────────────────────────────────────────────
|
|
143
|
+
/** Wallets ranked by LP pnl versus holding. Without Pro: the top 10 with masked addresses. */
|
|
144
|
+
smartLps(query = {}) {
|
|
145
|
+
return this.request('GET', '/v1/smart-lps', { query: { ...query } });
|
|
146
|
+
}
|
|
147
|
+
/** A wallet's open and closed positions (Pro). */
|
|
148
|
+
walletPositions(owner, opts = {}) {
|
|
149
|
+
return this.request('GET', `/v1/smart-lps/${enc(owner)}/positions`, { query: opts });
|
|
150
|
+
}
|
|
151
|
+
/** Wallets you follow (Pro). */
|
|
152
|
+
async follows() {
|
|
153
|
+
return (await this.request('GET', '/v1/me/follows')).follows;
|
|
154
|
+
}
|
|
155
|
+
/** Follow a wallet: its new positions of $10k+ become smart_lp signals for you (Pro, up to 50). */
|
|
156
|
+
follow(owner) {
|
|
157
|
+
return this.request('PUT', `/v1/me/follows/${enc(owner)}`);
|
|
158
|
+
}
|
|
159
|
+
async unfollow(owner) {
|
|
160
|
+
await this.request('DELETE', `/v1/me/follows/${enc(owner)}`);
|
|
161
|
+
}
|
|
162
|
+
// ── account ──────────────────────────────────────────────────────────────
|
|
163
|
+
me() {
|
|
164
|
+
return this.request('GET', '/v1/me');
|
|
165
|
+
}
|
|
166
|
+
/** Set or replace the webhook. The signing secret is returned only here. */
|
|
167
|
+
setWebhook(url) {
|
|
168
|
+
return this.request('PUT', '/v1/me/webhook', { body: { url } });
|
|
169
|
+
}
|
|
170
|
+
async deleteWebhook() {
|
|
171
|
+
await this.request('DELETE', '/v1/me/webhook');
|
|
172
|
+
}
|
|
173
|
+
/** Create a new API key for this account and return it (shown only here). The key used for this call stops working. */
|
|
174
|
+
createApiKey() {
|
|
175
|
+
return this.request('POST', '/v1/me/api-key');
|
|
176
|
+
}
|
|
177
|
+
/** A one-time code: send `/start <code>` to the LPSignal Telegram bot. */
|
|
178
|
+
telegramLink() {
|
|
179
|
+
return this.request('POST', '/v1/me/telegram-link');
|
|
180
|
+
}
|
|
181
|
+
// ── billing ──────────────────────────────────────────────────────────────
|
|
182
|
+
/** Current plan. `refresh: true` re-reads Stripe first (use it right after a checkout). */
|
|
183
|
+
billing(opts = {}) {
|
|
184
|
+
return this.request('GET', '/v1/billing', { query: opts.refresh ? { refresh: 1 } : {} });
|
|
185
|
+
}
|
|
186
|
+
/** A Stripe Checkout URL for a monthly plan. */
|
|
187
|
+
checkout(tier) {
|
|
188
|
+
return this.request('POST', '/v1/billing/checkout', { body: { tier } });
|
|
189
|
+
}
|
|
190
|
+
/** A Stripe Customer Portal URL (change plan, cancel, invoices). */
|
|
191
|
+
billingPortal() {
|
|
192
|
+
return this.request('POST', '/v1/billing/portal');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { LPSignal, LPSignalError, DEFAULT_BASE_URL } from './client.js';
|
|
2
|
+
export type { LPSignalOptions, PoolsQuery, SignalsQuery } from './client.js';
|
|
3
|
+
export { SignalStream, MemoryLastIdStore, FileLastIdStore } from './stream.js';
|
|
4
|
+
export type { LastIdStore, SignalMeta, SignalStreamOptions, StreamEvent, SocketFactory, SocketLike } from './stream.js';
|
|
5
|
+
export { verifyWebhook, WebhookVerificationError } from './webhook.js';
|
|
6
|
+
export type { HeadersLike, WebhookFailure } from './webhook.js';
|
|
7
|
+
export type * from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAExE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE/E,OAAO,EAAE,aAAa,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import WebSocket, { type ClientOptions } from 'ws';
|
|
2
|
+
import { type LPSignal } from './client.js';
|
|
3
|
+
import type { Signal } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Where the stream is: the id of the last signal handled. Persist it to resume after a restart.
|
|
6
|
+
*
|
|
7
|
+
* To make a crash unable to repeat a signal's side effects, keep the id in the same database transaction as those
|
|
8
|
+
* effects (write it inside onSignal) and give the stream a store over that same row. Its save() must only move
|
|
9
|
+
* forward (e.g. `SET last_id = GREATEST(last_id, $1)`): the stream also saves the starting point with it.
|
|
10
|
+
*/
|
|
11
|
+
export interface LastIdStore {
|
|
12
|
+
load(): Promise<string | null>;
|
|
13
|
+
save(lastId: string): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare class MemoryLastIdStore implements LastIdStore {
|
|
16
|
+
private id;
|
|
17
|
+
load(): Promise<string | null>;
|
|
18
|
+
save(lastId: string): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/** A small JSON file, replaced atomically (temp file + rename) so a crash never leaves half a file. */
|
|
21
|
+
export declare class FileLastIdStore implements LastIdStore {
|
|
22
|
+
readonly path: string;
|
|
23
|
+
constructor(path: string);
|
|
24
|
+
load(): Promise<string | null>;
|
|
25
|
+
save(lastId: string): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/** How a signal reached you: REST catch-up after downtime, the stream's own replay, or live. */
|
|
28
|
+
export interface SignalMeta {
|
|
29
|
+
source: 'rest' | 'replay' | 'live';
|
|
30
|
+
}
|
|
31
|
+
export type StreamEvent =
|
|
32
|
+
/** no saved position: starting after the newest signal that exists now */
|
|
33
|
+
{
|
|
34
|
+
type: 'anchored';
|
|
35
|
+
lastId: string;
|
|
36
|
+
}
|
|
37
|
+
/** missed signals were fetched over REST before connecting */
|
|
38
|
+
| {
|
|
39
|
+
type: 'caught_up';
|
|
40
|
+
delivered: number;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'connecting';
|
|
43
|
+
url: string;
|
|
44
|
+
} | {
|
|
45
|
+
type: 'connected';
|
|
46
|
+
}
|
|
47
|
+
/** the stream's replay finished: everything from here on is live */
|
|
48
|
+
| {
|
|
49
|
+
type: 'live';
|
|
50
|
+
} | {
|
|
51
|
+
type: 'disconnected';
|
|
52
|
+
code: number;
|
|
53
|
+
reason: string;
|
|
54
|
+
} | {
|
|
55
|
+
type: 'error';
|
|
56
|
+
error: Error;
|
|
57
|
+
}
|
|
58
|
+
/** unrecoverable (bad key, plan without the stream, plan expired): the stream has stopped */
|
|
59
|
+
| {
|
|
60
|
+
type: 'fatal';
|
|
61
|
+
error: Error;
|
|
62
|
+
};
|
|
63
|
+
/** The subset of the `ws` WebSocket the stream uses; injectable for tests. */
|
|
64
|
+
export interface SocketLike {
|
|
65
|
+
on(event: 'open', cb: () => void): unknown;
|
|
66
|
+
on(event: 'message', cb: (data: WebSocket.RawData) => void): unknown;
|
|
67
|
+
on(event: 'close', cb: (code: number, reason: Buffer) => void): unknown;
|
|
68
|
+
on(event: 'error', cb: (err: Error) => void): unknown;
|
|
69
|
+
on(event: 'pong', cb: () => void): unknown;
|
|
70
|
+
on(event: 'unexpected-response', cb: (req: unknown, res: {
|
|
71
|
+
statusCode?: number;
|
|
72
|
+
}) => void): unknown;
|
|
73
|
+
ping(): void;
|
|
74
|
+
close(code?: number): void;
|
|
75
|
+
terminate(): void;
|
|
76
|
+
}
|
|
77
|
+
export type SocketFactory = (url: string, headers: Record<string, string>) => SocketLike;
|
|
78
|
+
export interface SignalStreamOptions {
|
|
79
|
+
/** a client with a paid API key */
|
|
80
|
+
client: LPSignal;
|
|
81
|
+
/**
|
|
82
|
+
* Called for each signal in ascending id order, never concurrently. The position is saved only after this returns:
|
|
83
|
+
* if it throws, the connection is dropped and the signal is offered again after reconnecting. A crash between this
|
|
84
|
+
* returning and the position being saved also offers it again after the restart, so delivery is at-least-once:
|
|
85
|
+
* make the handler idempotent on `signal.id`, or keep the position in your own database (see LastIdStore).
|
|
86
|
+
*/
|
|
87
|
+
onSignal: (signal: Signal, meta: SignalMeta) => void | Promise<void>;
|
|
88
|
+
onEvent?: (event: StreamEvent) => void;
|
|
89
|
+
/** default: in memory (a restart starts from "now") */
|
|
90
|
+
store?: LastIdStore;
|
|
91
|
+
/** start after this signal id when the store is empty (default: after the newest signal at start) */
|
|
92
|
+
since?: string;
|
|
93
|
+
pingIntervalMs?: number;
|
|
94
|
+
/** give up on a connection that has not opened after this long, default 15 s */
|
|
95
|
+
openTimeoutMs?: number;
|
|
96
|
+
minBackoffMs?: number;
|
|
97
|
+
maxBackoffMs?: number;
|
|
98
|
+
socketFactory?: SocketFactory;
|
|
99
|
+
/** extra options for the `ws` client, e.g. `{ agent }` for a proxy */
|
|
100
|
+
wsOptions?: ClientOptions;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Live signals in id order, with no gaps across disconnects, outages and restarts.
|
|
104
|
+
*
|
|
105
|
+
* The server replays the last 24 hours on `?since=<id>`. Before every connection this stream first fetches everything
|
|
106
|
+
* newer than its position over REST, repeating until a pass finds nothing new, so an outage of any length is filled
|
|
107
|
+
* and the socket opens seconds after the last fetch. Anything at or below the position is dropped, so within one
|
|
108
|
+
* running process each signal reaches the handler once; across a crash the last one may be offered again.
|
|
109
|
+
*/
|
|
110
|
+
export declare class SignalStream {
|
|
111
|
+
private readonly opts;
|
|
112
|
+
private readonly store;
|
|
113
|
+
private readonly socketFactory;
|
|
114
|
+
private lastId;
|
|
115
|
+
private queue;
|
|
116
|
+
private running;
|
|
117
|
+
private socket;
|
|
118
|
+
private loopDone;
|
|
119
|
+
private wake;
|
|
120
|
+
private starting;
|
|
121
|
+
private stopping;
|
|
122
|
+
/** the first-run starting point, chosen once and kept until it is saved (a retry must not pick a newer one) */
|
|
123
|
+
private anchor;
|
|
124
|
+
constructor(options: SignalStreamOptions);
|
|
125
|
+
/** Id of the last signal handed to onSignal (null before the first connection). */
|
|
126
|
+
get position(): string | null;
|
|
127
|
+
/**
|
|
128
|
+
* Loads the saved position and starts in the background; call stop() to end. Calling it while running is a no-op;
|
|
129
|
+
* during a stop() it starts once the stop has finished. After a `fatal` event, call stop() before starting again.
|
|
130
|
+
*/
|
|
131
|
+
start(): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* Closes the socket and waits for the signal being handled (if any). The stream can be started again.
|
|
134
|
+
* Don't await it from inside onSignal: it would wait for itself.
|
|
135
|
+
*/
|
|
136
|
+
stop(): Promise<void>;
|
|
137
|
+
private emit;
|
|
138
|
+
private fatal;
|
|
139
|
+
private loop;
|
|
140
|
+
/** Establish a position (first run) or fetch everything after it over REST. */
|
|
141
|
+
private catchUp;
|
|
142
|
+
/** One connection's life. Resolves when it closes; true when it went live. */
|
|
143
|
+
private connectOnce;
|
|
144
|
+
/** true when the signal was new and handed to onSignal */
|
|
145
|
+
private deliver;
|
|
146
|
+
}
|