Build a sniper
A sniper does four things: see a launch, decide, buy, exit. This guide builds one in about a hundred lines with the launch feed, Lightspeed for the buy and the trade stream for the exits, then shows the one change that moves the launch signal a block earlier with Ultra Fast Stream.
What you need
- Node.js 22 or newer (WebSocket is built in),
npm install socket.io-client. - A Lightspeed account, funded with SOL. Its key signs the buys.
- Run it next to the API, in Frankfurt (Regions).
The script
import { io } from "socket.io-client";
// ─── change these ─────────────────────────────────────
const LS_KEY = "ls_…"; // your Lightspeed account key
const BUY_SOL = 0.05; // per launch
const SLIPPAGE = 20; // percent; launches move fast
const TIP = 0.001; // SOL per provider copy, paid once by the one that lands
const TAKE_PROFIT = 2.0; // sell at 2x
const STOP_LOSS = 0.6; // ... or at -40%
const MAX_HOLD_S = 120; // ... or after two minutes
const MAX_OPEN = 3;
const MIN_DEV_BUY = 0.5; // skip launches whose creator bought less than this (SOL)
const DRY_RUN = true;
// ──────────────────────────────────────────────────────
const API = "https://sol.shrine.trade";
const open = new Map(); // mint -> { entry, since, pool }
async function lightspeed(body) {
if (DRY_RUN) { console.log(" dry-run", body); return { success: true }; }
const r = await (await fetch(`${API}/api/lightspeed`, {
method: "POST", headers: { "content-type": "application/json", "x-api-key": LS_KEY }, body: JSON.stringify(body),
})).json();
if (!r.success) throw new Error(`${r.error}: ${r.message}`);
return r;
}
const socket = io(API, { transports: ["websocket"] });
socket.on("connect", () => {
socket.emit("subscribe_new_tokens", { protocols: ["PUMPFUN"] });
console.log("watching launches");
});
// 1. A launch. Filter, then buy.
socket.on("new_token", async (t) => {
if (open.size >= MAX_OPEN || open.has(t.mint)) return;
const devBuy = t.initialBuy?.quoteAmount ?? 0;
if (devBuy < MIN_DEV_BUY) return; // the creator did not back it
if (t.mintAuthority || t.freezeAuthority) return; // can still mint or freeze: skip
console.log(`launch ${t.symbol} ${t.mint} dev buy ${devBuy} SOL`);
try {
const r = await lightspeed({ action: "buy", mint: t.mint, amount: BUY_SOL, slippage: SLIPPAGE, tip: TIP });
open.set(t.mint, { entry: null, since: Date.now(), pool: t.pool });
socket.emit("subscribe_trades", { pool: t.pool }); // 2. follow its price for the exits
console.log(` bought ${t.symbol}: ${r.signature ?? "dry"}`);
} catch (e) {
console.error(` buy failed: ${e.message}`);
}
});
// 3. Exits from the live trades: first price seen after the buy is the entry.
socket.on("trade", async (tr) => {
const pos = open.get(tr.mint);
if (!pos) return;
if (pos.entry === null) { pos.entry = tr.price; return; }
const x = tr.price / pos.entry;
const age = (Date.now() - pos.since) / 1000;
const why = x >= TAKE_PROFIT ? "take profit" : x <= STOP_LOSS ? "stop loss" : age >= MAX_HOLD_S ? "timer" : null;
if (!why) return;
open.delete(tr.mint);
socket.emit("unsubscribe_trades", { pool: pos.pool });
try {
await lightspeed({ action: "sell", mint: tr.mint, amount: "100%", slippage: SLIPPAGE, tip: TIP });
console.log(` sold ${tr.mint} (${why}, ${x.toFixed(2)}x)`);
} catch (e) {
console.error(` sell failed: ${e.message}`);
open.set(tr.mint, pos); // keep it; try again on the next trade
}
});
Run it with DRY_RUN = true first; it prints every launch it would buy and every exit it would take.
What each part does
- The filter is where the edge is. The launch event carries the creator's own buy (
initialBuy), the authorities and token extensions, the name and URI; a creator who bought nothing, or kept a mint authority, is a launch to skip. Add your own: a minimum name length, a URI that resolves, a creator you have seen before. - The buy is one request. Lightspeed builds and signs from its own cache of every curve, so nothing is read from the chain on the way, and the copies race every landing provider at once; only one fills.
tipis what buys priority in a crowded slot. - The exits come from the pool's trades: the first price after your buy is your entry, every trade after it a check against the target, the stop and the clock. One sell request closes the position,
"100%"whatever the fill was.
The fast version: Ultra Fast Stream
The launch feed above is an executed feed: the create has run and a node has served it. Ultra Fast Stream delivers the same create the moment its shreds arrive, before the block executes, and it carries the mint already. Replace the launch source and keep everything else:
const uf = new WebSocket(`wss://sol.shrine.trade/api/ultrafast/stream?key=${UF_KEY}&creates=1`);
uf.onmessage = async (m) => {
const e = JSON.parse(m.data);
if (e.type !== "create") return;
// e.mint, e.symbol, e.name, e.creator, e.bondingCurve are known before the block exists.
// The dev buy follows as a trade with createBuy: true on this same socket.
// ... same filter and the same lightspeed({ action: "buy", mint: e.mint, ... }) as above
};
The buy then leaves more than 100 ms before a bot on PumpPortal has seen the launch at all, and 6 ms before one on our own executed stream in the same region. The exits stay on the executed stream, where prices are real.
Where the time goes
| Step | Time |
|---|---|
| launch on Ultra Fast Stream to your bot in Frankfurt | about 1 ms |
| your filter | yours |
| Lightspeed build and sign | about 1 ms |
| first provider acknowledgement | 10 to 20 ms |
| landed | the next slot, usually |
A bot elsewhere in Europe adds 10 to 30 ms each way; one in the US should use the New York region instead of crossing the Atlantic. That is the difference between the first slot and the third.