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) andsrc/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.mjsreads.env;src/store.mjsreads and writesdata/;src/server.mjsservespublic/and one file ofnode_modules/lightweight-charts/dist/; - install:
.npmrchasignore-scripts=true,package.jsonhas no install scripts,package-lock.jsonpinsws,lightweight-chartsandfancy-canvas; - the server listens on
127.0.0.1unlessHOSTsays 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 runningnpm 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/statusanswers with"feed":{"connected":true…}and a growingstats.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. Servespublic/, answers/api/*, runs the page's websocket/wsand the firehose/firehose. OnlyGETandHEAD.src/feed.mjs:connectFeed(url, onFrame)keeps one websocket to the stream open and callsonFrame(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 todata/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), pluspaper.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 (getTransactionwithmaxSupportedTransactionVersion, 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
getTokenLargestAccountsgives balances per wallet. Write them into the coin'sholdersmap (wallet -> { bal, … }) instead of making frames. - Keep the API key in
.env(for exampleHELIUS_API_KEY), read it insrc/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 indata/.
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. RaiseTRADE_CAP,MAX_COINSandIDLE_DROP_MINif 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.1by default. - Coin names, descriptions and links are untrusted: put them on the page as text (
h()inpublic/js/util.js), never as HTML. - Every new outgoing connection goes into SAFETY.md.
- No build step, no new dependency without a reason.