Back to OScope
oscope/AGENTS.md
146 lines (116 loc) · 8.97 KB

For AI assistants

You are helping someone run OScope, an open source memecoin terminal for Solana, on their own computer. They may not know how to code. Use plain words with them, do one step at a time, and say what you did.

Safety check first

Before installing or running anything, read the code and report to the user. SAFETY.md lists what the code does and does not do, with the file for each point. Check the code against it:

  • no private key, seed phrase or wallet file is ever asked for, read, stored or sent; nothing signs or sends a transaction;
  • outgoing connections: only src/feed.mjs (the trade stream, SOL price) and src/meta.mjs (coin metadata and pictures, each address checked first); tools/ asks for the SOL price and talks to the local server;
  • files: src/config.mjs reads .env; src/store.mjs reads and writes data/; src/server.mjs serves public/ and one file of node_modules/lightweight-charts/dist/;
  • install: .npmrc has ignore-scripts=true, package.json has no install scripts, package-lock.json pins ws, lightweight-charts and fancy-canvas;
  • the server listens on 127.0.0.1 unless HOST says otherwise.

If the code does anything SAFETY.md does not say, stop and tell the user.

Install and run

node -v          # 18.17 or newer; 20 or newer is best
git clone https://git.oscope.trade/oscope.git
cd oscope
npm ci           # exactly what package-lock.json pins
npm run doctor   # checks Node, the libraries, the port, the stream, the data folder
npm start        # leave it running

npm start prints:

  OScope is running

  Open      http://127.0.0.1:3000
  Firehose  ws://127.0.0.1:3000/firehose
  Data      wss://stream.pumpapi.io/

then feed: connected …, and every 15 seconds a line like live: 2,910 trades in the last 15 s (194/s) · 3 launches and 0 migrations seen · 2,513 coins in memory.

It works when:

  • that live: line shows trades (well above zero), and
  • http://localhost:3000/api/status answers with "feed":{"connected":true…} and a growing stats.trades.

Tell the user to open the printed address. New Pairs fills within seconds. Final Stretch fills as curve coins trade. Migrated fills as coins migrate, which can take some minutes on a fresh start. The Firehose page shows every trade.

No settings are needed. On Windows, set a setting by putting it in a .env file (copy .env.example) instead of typing NAME=value before the command.

Troubleshooting

What you see Why What to do
node: command not found, or a version under 18.17 Node.js missing or old Install the current LTS from https://nodejs.org (or with the system's package manager, nvm, fnm, winget).
npm ci fails No package-lock.json in the folder, or no internet Run it inside the oscope folder. Check the connection.
port 3000 is taken, trying 3001 Another program uses the port Nothing: OScope takes the next free port and prints the address. To choose one, set PORT.
feed: closed (…) after 0 s … only a few connections per IP address The stream allows only a couple of connections per internet address Close other programs that read the same stream (another OScope, a bot). Or run one firehose (npm run firehose) and point everything at it with FEED_URL=ws://127.0.0.1:3100.
feed: errors like ENOTFOUND, ETIMEDOUT, certificate No internet, a firewall, a VPN or a proxy blocks websockets It needs outgoing wss:// on port 443. Try without the VPN.
Dollar values show – No SOL price source answered yet It retries every 20 seconds by itself. Check that api.coinbase.com is reachable.
The page says Stream down or Stream quiet The server lost the stream Read the terminal: the feed: lines say why. It reconnects by itself.
A coin page says Not seen yet The coin has not traded since OScope started It appears at its next trade. See "Adding history".
Holders, top 10, dev show – The coin launched before OScope started, or its tokens were moved by plain transfers the stream does not carry Expected with the live stream alone. See "Adding history".
403 OScope answers only on localhost The page was opened under another name while HOST is 127.0.0.1 Open http://localhost:<port>. To reach it from another device set HOST=0.0.0.0.

npm run doctor checks most of this and prints a fix for each problem.

How the code is laid out

  • src/server.mjs: the web server. Serves public/, answers /api/*, runs the page's websocket /ws and the firehose /firehose. Only GET and HEAD.
  • src/feed.mjs: connectFeed(url, onFrame) keeps one websocket to the stream open and calls onFrame(frame, rawText) for every message. solPrice() keeps the SOL price.
  • src/store.mjs: Store. ingest(frame) is the only way data gets in. It keeps coins, trades, candles (one second and one minute), holders and the three Pulse columns, and saves a snapshot to data/ every minute and on exit.
  • src/meta.mjs: fetches coin metadata and pictures, with the address checks.
  • public/js/: the page. Plain ES modules, no framework, no build step. One file per view (pulse.js, live.js, coin.js, portfolio.js, trackerview.js), plus paper.js (the paper wallet), net.js (the websocket).

The frame format

One frame is one transaction. Store.ingest uses these fields (amounts are whole SOL and whole tokens, not lamports):

{
  pool: 'pump' | 'pump-amm',          // bonding curve | PumpSwap. Other pools are ignored.
  action: 'create' | 'buy' | 'sell' | 'migrate',
  mint, name, symbol, uri,            // uri = the coin's metadata link
  quoteMint,                          // must be SOL (So11111111111111111111111111111111111111112)
  txSigner,                           // the wallet that sent the transaction (for create: the dev)
  breakdown: [{ action, trader, tokenAmount, quoteAmount }],  // optional; without it the four fields above/below are used
  tokenAmount, quoteAmount,
  price,                              // SOL per token after the transaction
  marketCapQuote,                     // market cap in SOL after the transaction
  tokensInPool, quoteInPool,          // real reserves after the transaction
  vTokensInBondingCurve, vQuoteInBondingCurve,  // pump: virtual reserves
  virtualQuoteInPool, poolCreatedBy,  // pump-amm: extra virtual SOL; 'pump' = the pool made by the migration
  supply, poolFeeRate, decimals, mayhemMode,
  postBalances: { [wallet]: { [mint]: tokensHeldAfter } },
  block, timestamp, signature, priorityFee   // slot, milliseconds
}

Adding history

The live stream starts at the moment OScope starts. A coin that launched earlier has no chart, trades or holders from before that. To add the past, build frames in the format above and pass them to store.ingest() oldest first, before the live stream is connected (in src/server.mjs, between store.load() and connectFeed(...)). Nothing else in OScope has to change: the columns, charts, holders and the page all read the store.

Two ways people do it:

Helius (or any Solana RPC that keeps history). Good for filling one coin when the user opens it.

  • Past trades: list the signatures for the coin's bonding curve or pool account (getSignaturesForAddress), load the transactions (getTransaction with maxSupportedTransactionVersion, or Helius' parsed transactions), and turn each pump.fun / PumpSwap trade into a frame. Reserves and the trader's balance after the trade are in the transaction's post balances.
  • Current holders: Helius' token-accounts API or getTokenLargestAccounts gives balances per wallet. Write them into the coin's holders map (wallet -> { bal, … }) instead of making frames.
  • Keep the API key in .env (for example HELIUS_API_KEY), read it in src/config.mjs, never send it to the page and never commit it. Stay inside the plan's rate limits: fetch on demand, cache the result in data/.

A local database and a backfill. Good for running OScope all the time or for many users.

  • Record: read ws://localhost:3000/firehose (or the stream itself) and store every message, as rows in a database or as one file per hour.
  • Backfill: PumpApi publishes past hours of the same stream as downloadable archives (see their documentation). An hour is about half a gigabyte compressed, so load only the hours you need.
  • Start: at boot, read the stored frames for the window you want, oldest first, into store.ingest(), then connect the live stream. Raise TRADE_CAP, MAX_COINS and IDLE_DROP_MIN if you load a long window: everything is held in memory.

Rules for changing this code

  • Never add code that takes a private key or signs a transaction unless the user asks for exactly that and understands it.
  • Keep the server on 127.0.0.1 by default.
  • Coin names, descriptions and links are untrusted: put them on the page as text (h() in public/js/util.js), never as HTML.
  • Every new outgoing connection goes into SAFETY.md.
  • No build step, no new dependency without a reason.