# Everted Hand Swarm — Technical Report

**A real-time Solana memecoin analytics dashboard built on PublicNode Solana RPC/WebSocket and the DexScreener API**

Prepared: 2026-10-16 · Audience: developers planning to build or bid on the project

---

## 0. How to read this report

Each substantive statement is tagged with one of these labels:

| Tag | Meaning |
|---|---|
| **[FACT]** | Stated in a primary source linked inline, or seen directly in a live probe described in §0.1. |
| **[OBSERVED]** | Seen in our own short live probe on 2026-10-16 (§0.1). It is real, but it comes from one sample, so treat it as a strong hint rather than a guarantee. |
| **[INFERENCE]** | Our engineering judgement drawn from the facts. It is reasoned, but no source states it. |
| **[UNCERTAIN]** | We could not confirm it. Verify before you commit to it in a bid. |
| **[OPEN]** | A question only the requester or further testing can answer (collected in §13). |

### 0.1 Live probes performed (2026-10-16, ~01:35 UTC)

We ran all probes from one machine with `curl` and the Node.js 24 built-in `WebSocket`. We did not use API keys.

| # | Probe | Result |
|---|---|---|
| P1 | `getVersion` → `https://solana-rpc.publicnode.com` | HTTP 200, `{"solana-core":"4.3.0"}`. Served behind Cloudflare. No rate-limit headers in the response. |
| P2 | 40 parallel `getSlot` requests | All 40 returned HTTP 200, so no throttling at that burst size. |
| P3 | `getSignaturesForAddress(JUP6Lkb…, limit 2)` | Worked. Returned `blockTime`, `slot`, `confirmationStatus`, `err`. |
| P4 | `getBlock(1)` | Error `-32001`: *"Block 1 cleaned up, does not exist on node. First available block: 457064937"* (current slot was ~457456067). |
| P5 | `getTransaction(…, jsonParsed, maxSupportedTransactionVersion:0)` | Worked. `meta` contained `preTokenBalances`/`postTokenBalances`, `preBalances`/`postBalances`, `fee`, `innerInstructions`, `logMessages`. A pump.fun token (`…pump` mint) showed `programId: TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb` (Token-2022). |
| P6 | WebSocket `wss://solana-rpc.publicnode.com`: `slotSubscribe`, `logsSubscribe("all")`, `logsSubscribe({mentions:[Jupiter v6]})`, `logsSubscribe({mentions:[pump.fun program]})` (confirmed and processed), `programSubscribe(pump.fun)` | All subscriptions were accepted and returned IDs. **15 s counts:** slot = 68, logs "all" = **5**, Jupiter mentions = **0**, pump.fun mentions = **0**, programSubscribe pump.fun = 13. |
| P7 | `logsSubscribe` with **two** addresses in `mentions` | No success response arrived in 10 s. The docs say this returns `Invalid params`. |
| P8 | `GET https://api.dexscreener.com/tokens/v1/solana/So111…112` | Worked. Fields included `pairAddress, baseToken, quoteToken, priceNative, priceUsd, txns{m5,h1,h6,h24}{buys,sells}, volume{…}, liquidity, marketCap, pairCreatedAt, priceChange`. Header `cache-control: public, max-age=30`, `cf-cache-status: HIT`. |

The most important finding is P6. Mainnet Solana processes thousands of transactions per second, and Jupiter and pump.fun are among the busiest programs on it. Five "all" log notifications and zero program-mention notifications in 15 s means the free PublicNode WebSocket did **not** deliver a complete firehose to us. **[INFERENCE]** The cause could be sampling, filtering, per-IP fair-use limits, or a lagging node. We cannot tell which from outside. **Design consequence:** never treat the free WebSocket as a complete source of trades. Use it as a *trigger* and reconcile over HTTP (§2.4).

---

## 1. Executive summary

- **[FACT]** PublicNode advertises Solana at `https://solana-rpc.publicnode.com` as a "free" and "privacy first" endpoint, with a WS RPC tab and optional "Archive data available" access ([solana.publicnode.com](https://solana.publicnode.com/), [publicnode.com](https://www.publicnode.com/)). Neither page publishes rate limits or an SLA.
- **[FACT]** DexScreener publishes rate limits: **300 req/min** for `/latest/dex/pairs`, `/tokens/v1`, `/token-pairs/v1`, `/latest/dex/search`, and **60 req/min** for profiles, boosts, orders, and metas. `/tokens/v1` accepts **up to 30 comma-separated addresses** ([DexScreener API reference](https://docs.dexscreener.com/api/reference); [OpenAPI spec](https://openapi.gitbook.com/o/F3EJQhITOlKtqJoq3nND/spec/dex-screener-api.yaml)).
- **[FACT]** DexScreener gives **aggregate** pair statistics: price, buy/sell *counts*, volume, and liquidity. It gives **no per-wallet trade list**, so wallet discovery and per-trade data must come from Solana RPC.
- **[INFERENCE]** The feasible free-tier design is **wallet-centric polling plus a WebSocket trigger**. It is *not* a whole-chain indexer. Monitoring 10 wallets on free services is realistic. 50 is feasible with careful batching. 100 is at the edge and needs either a paid RPC or degraded latency (§3).
- **[INFERENCE]** You can compute P&L precisely from on-chain balance deltas using integer arithmetic. "Profit" is only as accurate as (a) the history you can still fetch (P4 shows the free node prunes old blocks) and (b) the USD price source. Show both caveats in the UI.

---

## 2. Auto-discovery of wallets and real buy/sell transactions

### 2.1 What "real" means here

A buy or sell is a **confirmed, successful** Solana transaction (`err == null`) that, for a given owner wallet:
- decreases one asset and increases another. For example, SOL/WSOL/USDC goes down and memecoin X goes up (a **buy**), or the reverse (a **sell**);
- with the amounts taken from `meta.preTokenBalances`/`postTokenBalances` (SPL/Token-2022 tokens) and `meta.preBalances`/`postBalances` (lamports). **[FACT]** These fields are documented in [getTransaction](https://solana.com/docs/rpc/http/gettransaction) and seen in P5.

Do not infer trades from log text alone. Logs can be truncated, and program log formats change. **[INFERENCE]**

### 2.2 Discovery sources (all real, no synthetic data)

| Source | What it gives | Cost and limits |
|---|---|---|
| DexScreener `/token-profiles/latest/v1`, `/token-boosts/latest/v1`, `/token-boosts/top/v1` | Newly profiled or boosted Solana tokens (`chainId`, `tokenAddress`) | 60 req/min **[FACT]**. Boosts are *paid promotion*, so they show a token's marketing spend, not its quality. Label them as such in the UI. **[INFERENCE]** |
| DexScreener `/latest/dex/search?q=` and `/token-pairs/v1/solana/{token}` | Pair addresses (pool accounts) for a token | 300 req/min **[FACT]** |
| RPC `getSignaturesForAddress(<pairAddress or token mint>)` | Recent signatures that touched the pool. Each one is a candidate trade. | Paginated, `limit` ≤ 1000 per call ([docs](https://solana.com/docs/rpc/http/getsignaturesforaddress)). Limited to the history the node retains (P4). |
| RPC `getTransaction` on each signature | Fee payer plus balance deltas → the trader wallet and the trade side and size | One call per transaction. This is the main cost driver. |
| WS `logsSubscribe({mentions:[pool]})` | Push trigger when a pool is touched | **[FACT]** Exactly one address per subscription ([logsSubscribe](https://solana.com/docs/rpc/websocket/logssubscribe)). P6/P7 suggest delivery is incomplete on the free endpoint. |

**Wallet discovery algorithm** **[INFERENCE]**:
1. Pick candidate tokens from DexScreener (trending, newly profiled, or user-entered), filtered by `liquidity.usd`, `volume.h24`, and `pairCreatedAt` thresholds.
2. For each token's top pair, page `getSignaturesForAddress(pair)` across a bounded window, such as the last N = 200 signatures.
3. Fetch `getTransaction` for each signature and decode trades (§2.3). Aggregate per owner: realised P&L, number of trades, hold time, and first-buy timing compared with `pairCreatedAt`.
4. Score wallets, for example on realised P&L, win rate, and early entry. Exclude obvious non-humans: the pool's own vault, program-derived accounts, and known aggregator or router accounts. Also flag likely bots, such as many trades in the same slot or buy-and-sell in the same block.
5. Add the top-scored wallets to the **auto watchlist** (§4) and record *why* each one was added, so a user can audit the reason.

### 2.3 Decoding a trade from a parsed transaction

```
owner      = fee payer (accountKeys[0]) unless the user wallet is a different signer
for each mint m in pre/postTokenBalances where entry.owner == owner:
    delta_m = BigInt(post.amount) - BigInt(pre.amount)     // raw integer units
solDelta   = postBalances[i] - preBalances[i] + (owner is fee payer ? fee : 0)   // lamports, excl. fee
wsolDelta  = delta for mint So11111111111111111111111111111111111111112
quoteDelta = solDelta + wsolDelta   (or USDC/USDT delta if quote is a stablecoin)
if memecoin delta > 0 and quoteDelta < 0 -> BUY  ; price = |quoteDelta| / delta
if memecoin delta < 0 and quoteDelta > 0 -> SELL ; price = quoteDelta / |delta|
```

Notes:
- **[FACT]** `uiTokenAmount.amount` is a base-10 *integer string*. `uiAmount` is deprecated in favour of `uiAmountString` ([getTransaction](https://solana.com/docs/rpc/http/gettransaction)). Always compute from `amount` and `decimals` with BigInt or a decimal library. Never use JS floats.
- **[FACT]** Omitting `maxSupportedTransactionVersion` makes versioned transactions error out. Always pass `0` (the docs now also list `1`). `getTransaction` rejects `processed` commitment ([same page](https://solana.com/docs/rpc/http/gettransaction)).
- **[OBSERVED]** pump.fun mints use the Token-2022 program (P5). The decoder and any `getTokenAccountsByOwner` calls must query **both** `Tokenkeg…` and `TokenzQd…` program IDs.
- Multi-hop routes (Jupiter) can touch several intermediate mints. Use **net** owner deltas, not per-instruction amounts. **[INFERENCE]**
- Transfers, airdrops, and LP add/remove change balances without being trades. Classify a row as a trade only when one side is a recognised quote asset and the opposite side is the token. Label everything else as "transfer/other". **[INFERENCE]**

### 2.4 Live pipeline (trigger and reconcile)

```
WS (logsSubscribe per watched wallet, commitment=confirmed)
      │  signature hint
      ▼
Ingest queue (dedupe by signature) ◄── poller: getSignaturesForAddress(wallet, until=lastSeenSig) every T s
      │
      ▼
getTransaction(confirmed, jsonParsed, v0) → decoder → trades table (unique signature+owner+mint)
      │
      ▼
price enrichment (DexScreener /tokens/v1 batch ≤30) → P&L engine → pub/sub → UI
```

The poller guarantees completeness within the node's retention window. The WebSocket only lowers latency. Because P6 showed missed notifications, the poller is **mandatory**. **[OBSERVED + INFERENCE]**

---

## 3. Monitoring 10, 50 or 100 wallets

### 3.1 Request budget model **[INFERENCE]**

Let *W* be the number of wallets, *T* the poll interval in seconds, and *k* the average number of new transactions per wallet per minute.

- Polling cost: *W × 60/T* `getSignaturesForAddress` calls per minute.
- Decode cost: *W × k* `getTransaction` calls per minute.
- WebSocket: *W* `logsSubscribe` subscriptions, because each subscription takes only one address.

| Tier | Poll interval *T* | Signature polls/min | Decodes/min at k = 2 | WS subscriptions | Free-tier verdict |
|---|---|---|---|---|---|
| 10 wallets | 10 s | 60 | 20 | 10 | Comfortable. 40-request bursts were unthrottled (P2). |
| 50 wallets | 20 s | 150 | 100 | 50 | Feasible. Needs a token-bucket limiter and backoff. |
| 100 wallets | 30 s | 200 | 200 | 100 | Edge case. Latency rises and throttling risk is unknown. Offer a "bring your own RPC URL" setting. |

**[UNCERTAIN]** PublicNode publishes no numeric limits, so the actual ceiling is unknown. Measure it during a soak test (§11) and make *T* adaptive: back off on HTTP 429/5xx or JSON-RPC errors and speed up when error-free.

### 3.2 Implementation

- Rotate wallets through a priority scheduler. Wallets with recent activity get polled more often (*T*/2). Dormant wallets get polled less (2*T*).
- Use the `until` and `before` cursors of `getSignaturesForAddress` so you never re-fetch known signatures.
- Persist `last_seen_signature` per wallet so restarts resume without gaps (bounded by node retention).
- Make the tier selector (10/50/100) a real setting that changes scheduler parameters. Show the *effective* refresh latency in the UI, so users know what they are getting.

---

## 4. Watchlist management

| Feature | Behaviour |
|---|---|
| **Auto-add** | Discovery job (§2.2) inserts wallets with `source='auto'`, `reason` (JSON: score, tokens, P&L), and `added_at`. Respects the tier cap. When full, it evicts the lowest-scored *auto* entry and never a manual one. |
| **Manual add** | Paste a base-58 address. Validate it (32-byte decode, `PublicKey.isOnCurve` not required because PDAs are valid owners). Optional label. Backfill history immediately within retention. |
| **Remove / pin / mute** | Pin protects an entry from auto-eviction. Mute stops alerts but keeps tracking. |
| **Import / export** | CSV/JSON of addresses and labels. Never include private data. |
| **Audit trail** | `watchlist_events` table (add, remove, pin, with actor = user or auto). |

Wallet addresses are public data. Labels a user attaches may still be sensitive, so keep them per-user. **[INFERENCE]**

---

## 5. Live transaction feed and P&L

### 5.1 Feed row fields

`time (blockTime) · slot · signature (link to explorer) · wallet · side · token mint (CA) · token symbol · token amount · quote amount (SOL/USDC) · execution price (quote per token, derived on-chain) · USD value · DexScreener price at ingest time · realised P&L for this sell · position after trade · status (confirmed/finalized)`.

- **Execution price** comes from on-chain deltas (§2.3). It is exact to the transaction, apart from fees and slippage, which are already embedded in the deltas.
- **USD value** = quote amount × SOL/USD. This needs an external price. DexScreener's SOL pair `priceUsd` is a string ([spec](https://openapi.gitbook.com/o/F3EJQhITOlKtqJoq3nND/spec/dex-screener-api.yaml)) and is cached up to 30 s (P8). Store **which** price and **when** with every USD figure. **[FACT + INFERENCE]**
- **Volume** per token in the feed header comes from DexScreener `volume.{m5,h1,h6,h24}`. It covers *all traders*, not only watched wallets. Label it that way.

### 5.2 P&L method **[INFERENCE — standard accounting]**

- Track positions per (wallet, mint) in **raw integer units**, with cost basis in lamports (or USDC micro-units). Choose **average-cost** (simple and common) or **FIFO** (needed for tax-style lots) and display the method.
- **Realised P&L** on a sell = proceeds − (avg_cost × qty_sold). Fees count as part of cost and proceeds because they are already in the SOL deltas. Show them separately for transparency.
- **Unrealised P&L** = qty_held × current price − remaining cost basis. Current price comes from DexScreener `priceNative`/`priceUsd`.
- **Precision:** use BigInt for raw amounts and `decimal.js` (or PostgreSQL `NUMERIC`) for ratios. Never store money in `float8` or JS `number`.
- **Incomplete history:** if the first observed event for a (wallet, mint) is a sell, or a balance exists without a fetched buy, mark cost basis **unknown** and show "P&L n/a (history before retention window)" instead of guessing. P4 shows why: the free node had pruned blocks older than roughly 391k slots, about 2 days at ~0.4 s per slot **[OBSERVED + INFERENCE]**.

---

## 6. Leaderboards and whale alerts

| Board | Definition | Data caveat |
|---|---|---|
| **Top profit** | Sum of realised P&L (SOL and USD) per wallet over 24 h, 7 d, or all-tracked windows. Unrealised P&L in a separate column. | Only covers watched or discovered wallets and their retained history. Not "the whole market". |
| **Most-held tokens** | Count of watched wallets currently holding mint X (from position table, optionally verified with `getTokenAccountsByOwner` for both token programs), plus total USD value. | Positions acquired before tracking began are found only by the verification call. |
| **Whale accumulation alert** | A trigger fires when any of these holds: (a) one wallet's net buys of X over a rolling 15 min exceed $N or Y % of pool `liquidity.usd`; (b) ≥ M watched wallets buy X within a window (cluster); (c) a single buy exceeds the threshold. | Thresholds are user-configurable **[OPEN]**. Liquidity comes from DexScreener and may be ~30 s stale. |

Deduplicate alerts per (rule, wallet, mint, window) and record the triggering signatures, so every alert links to on-chain proof.

---

## 7. UI design guidelines (dark cyberpunk, responsive, live)

- **Palette:** near-black background (`#0a0a12`), panel `#12121f`, neon accents: cyan `#00f0ff` (info/buy), magenta `#ff2bd6` (highlights), lime `#39ff14` (profit), red `#ff3b5c` (loss/sell). Never rely on colour alone. Pair it with ▲/▼ icons and the BUY/SELL text, and check WCAG AA contrast (4.5:1 for body text).
- **Typography:** monospace for addresses and numbers (JetBrains Mono / IBM Plex Mono) with tabular figures. Show addresses shortened (`AxWz…fcvH`) with copy and explorer buttons.
- **Layout:** CSS grid with 3 columns on desktop (watchlist | live feed | leaderboards/alerts), 2 on tablet, and a single column with bottom tabs on mobile. Virtualise the feed list (e.g. TanStack Virtual) to keep 60 fps with thousands of rows.
- **Live notifications:** toast stack plus an optional browser Notification API (opt-in) plus an optional sound. Show a persistent **connection status pill** (Live / Reconnecting / Polling-only / Degraded), and the age of the last price update, so users can see how stale the data is.
- **Motion:** brief glow on new rows. Respect `prefers-reduced-motion`.
- **Honesty in UI:** show "n/a" for unknown values, a "data source and limitations" panel, and never use placeholder or demo rows in production builds.

---

## 8. Project audit, removing simulation code, and working controls

For an existing codebase, or as acceptance gates for a new one:

1. **Inventory:** list every button, toggle, and route. Map each one to a handler and a backend endpoint. Any control without a working path is a defect.
2. **Find simulated data:** grep for `Math.random`, `faker`, `mock`, `dummy`, `setInterval(...push(` and hard-coded addresses or prices in production code paths. Move fixtures to `test/fixtures` only. Add a CI check that fails if mock modules are imported from `src/`.
3. **Replace each simulation** with the real pipeline (§2.4). Where real data is unavailable, render an explicit empty or error state, never invented values.
4. **End-to-end test every control** (Playwright): add/remove wallet, tier switch, pin/mute, leaderboard window switch, alert threshold edit, export, and reconnect banner. Each test asserts both the UI change and the backend side effect.
5. **Data-correctness audit:** for a sample of decoded trades, compare amounts and side with a block explorer (Solscan/Solana Explorer) and record the result in an audit log.
6. **Dependency and config audit:** run `npm audit` / `pnpm audit`, pin versions, and confirm no secrets are in the client bundle.

---

## 9. Free services, security, reconnects and precision

### 9.1 Recommended stack **[INFERENCE]**

| Layer | Choice | Reason |
|---|---|---|
| Backend | Node.js (TypeScript) with `@solana/kit` (or `@solana/web3.js` v1), Fastify, `ws` | First-class Solana SDKs. One language end to end. |
| Workers | Same codebase. Separate processes for poller, decoder, pricer, and alerting. BullMQ queues on Redis. | Isolates rate-limited I/O and supports retries. |
| Database | **PostgreSQL** (Supabase free tier or self-hosted). Optional TimescaleDB extension for trade hypertables. | `NUMERIC` precision, unique constraints for idempotency, window functions for leaderboards. |
| Cache / pub-sub | Redis (Upstash free tier or self-hosted) | Price cache (TTL ≥ 30 s to match DexScreener `max-age`), fan-out to clients. |
| Frontend | React/Next.js with Tailwind, server-sent events or WebSocket from **your** backend | Browsers never connect to RPC directly: this centralises rate limiting and avoids leaking the user's IP to third parties. |

Free-tier quotas for Supabase, Upstash, and similar hosts change often and were **not** verified for this report **[UNCERTAIN]**. Check them at bid time.

### 9.2 Automatic reconnects (WebSocket) **[INFERENCE]**

- Use exponential backoff with full jitter (base 1 s, cap 30 s). Reset after 60 s of stable connection.
- Send a heartbeat ping every 20–30 s. Treat no `slotNotification` (or no pong) within 2× the interval as a dead socket and reconnect.
- On reconnect, **re-subscribe everything** (subscription IDs are per connection) and run an immediate **gap backfill** with `getSignaturesForAddress(wallet, until=lastSeenSig)` for every wallet.
- Make ingestion idempotent with a `UNIQUE(signature, owner, mint)` constraint, so replays never double-count P&L.
- After N failed reconnects, switch to polling-only mode and show that in the status pill.

### 9.3 Precision rules

Use integer raw amounts (`amount` string → BigInt), store as `NUMERIC(78,0)`, keep `decimals` per mint, compute prices with `decimal.js` at ≥ 30 significant digits, and round only for display. Count only `confirmed` (or `finalized`) transactions with `err == null`. Mark rows `confirmed` → `finalized` when they finalise. **[FACT for fields and commitment; INFERENCE for the policy]**

---

## 10. Limitations of free Solana RPC (PublicNode) — honest assessment

| # | Limitation | Evidence |
|---|---|---|
| L1 | **No published rate limits or SLA.** Throughput can change without notice. | **[FACT]** None stated on [solana.publicnode.com](https://solana.publicnode.com/) or [publicnode.com](https://www.publicnode.com/). |
| L2 | **Limited history.** Old blocks are pruned. In our probe the oldest available block was ~391k slots behind the tip. "Archive" access is a separate request. | **[OBSERVED]** P4. **[FACT]** "Archive data available / Get Archive Access" on the PublicNode page. Terms unknown **[UNCERTAIN]**. |
| L3 | **WebSocket streams appeared heavily incomplete**: 5 `logs(all)` and 0 Jupiter or pump.fun mention notifications in 15 s. | **[OBSERVED]** P6. Single sample; the cause is unknown. |
| L4 | **One address per `logsSubscribe`**, so N wallets need N subscriptions. The per-connection subscription cap is unpublished. | **[FACT]** [logsSubscribe docs](https://solana.com/docs/rpc/websocket/logssubscribe). Cap **[UNCERTAIN]**. |
| L5 | **Shared, anonymous infrastructure** behind Cloudflare. Latency and availability depend on other users. | **[OBSERVED]** `server: cloudflare` header (P1). |
| L6 | **No enhanced APIs**: no parsed-swap or wallet-history endpoints such as those some paid providers offer. All decoding is your code. | **[INFERENCE]** Only standard JSON-RPC was advertised. Yellowstone gRPC "requires a free API token" **[FACT]**, but we did not test it. |
| L7 | **`getProgramAccounts` and full-chain scans** are impractical on any shared free endpoint. Do not design around them. | **[INFERENCE]** |
| L8 | **DexScreener is aggregate-only, cached ~30 s, and rate-limited (60/300 per minute).** Boosts are paid promotion. | **[FACT]** spec plus P8 header. |

**Mitigations:** a configurable RPC URL list with failover (keep PublicNode as default, and let the owner add a paid key), polling as the source of truth, visible staleness indicators, and an optional PublicNode Yellowstone gRPC path once a free token is obtained **[OPEN]**.

---

## 11. Testing methods

| Level | What | How |
|---|---|---|
| Unit | Trade decoder, P&L engine, BigInt and decimal maths | Golden fixtures = **real** `getTransaction` JSON saved from mainnet (buy, sell, multi-hop, Token-2022, failed tx, transfer). Assert side, amounts, and price exactly. Property tests (fast-check): buy-then-sell of the same quantity at the same price gives P&L = −fees. |
| Contract | DexScreener and RPC response shapes | Validate with zod schemas from the published OpenAPI spec. Fail loudly when a schema drifts. |
| Integration | Poller, queue, DB idempotency | Replay the same signatures twice and assert there are no duplicate rows and no P&L drift. Inject 429/5xx and dropped sockets with a fault proxy (toxiproxy) and assert backoff and backfill. |
| E2E | Every UI control (§8) | Playwright against a staging stack pointed at mainnet with a small real watchlist. Snapshot tests for mobile, tablet, and desktop. |
| Soak | Free-tier ceilings | Run tiers 10, 50, and 100 for 24 h. Record error rate, p95 ingest latency (blockTime → UI), and missed transactions (poller found it but WS did not). Publish these numbers in the bid. |
| Accuracy audit | Spot checks | Compare 50 random trades per week with an explorer. Target 100 % match on amounts and side. |

---

## 12. Security best practices

- **Read-only by design.** The dashboard never needs private keys or signing. Refuse any "connect wallet to trade" scope creep unless it is separately specified.
- **Backend proxy for all third-party calls**, with a server-side rate limiter. Expose no RPC keys to the browser. Keep secrets in environment variables or a secret manager, never in the repo.
- **Input validation:** base-58 address decode (32 bytes) for every user-entered address, and zod validation on every API body. Use parameterised SQL only.
- **Output safety:** token names and symbols are attacker-controlled (anyone can mint a token named `<script>`). Escape all output, use a strict CSP, and avoid `dangerouslySetInnerHTML`. Treat DexScreener `info.websites`/`socials` links as untrusted: add `rel="noopener noreferrer"` and show a warning on outbound links.
- **Phishing and spam tokens:** flag tokens with lookalike symbols, zero liquidity, or freeze or mint authority still set (check via `getAccountInfo` on the mint) **[INFERENCE]**.
- **AuthN/Z** for multi-user deployments: per-user watchlists, session cookies (`HttpOnly`, `Secure`, `SameSite`), rate limits per user on your own API, and audit logs.
- **Supply chain:** use a lockfile, `npm audit` in CI, Dependabot or Renovate, and pin `@solana/*` packages. The [`@solana/web3.js` 1.95.6–1.95.7 compromise in Dec 2024](https://github.com/solana-labs/solana-web3.js/security/advisories/GHSA-jcxm-7wvp-g6p5) is a precedent **[FACT per that advisory; verify current status]**.
- **Not financial advice:** show this disclaimer. Leaderboards describe past on-chain behaviour only.

---

## 13. Open questions for the requester

1. **[OPEN]** Single-user or multi-user deployment (affects auth and DB design)?
2. **[OPEN]** Is a paid RPC fallback acceptable if the 100-wallet tier misses latency targets? What latency counts as "real-time" (e.g. < 5 s, < 30 s)?
3. **[OPEN]** Which P&L method (average-cost or FIFO), and which quote assets (SOL only, or also USDC/USDT)?
4. **[OPEN]** Whale-alert thresholds (USD amount, % of liquidity, cluster size) and notification channels (in-app only, or Telegram/Discord/email)?
5. **[OPEN]** May the team request a PublicNode Yellowstone gRPC token or archive access, and accept those terms?
6. **[OPEN]** Hosting target and budget (the "free" tiers of database and cache hosts have quotas).
7. **[OPEN]** Does an existing codebase exist for the §8 audit, or is this greenfield?

## 14. Sources

- PublicNode Solana endpoint page — https://solana.publicnode.com/ (accessed 2026-10-16)
- PublicNode home — https://www.publicnode.com/ (accessed 2026-10-16)
- DexScreener API reference — https://docs.dexscreener.com/api/reference
- DexScreener OpenAPI spec — https://openapi.gitbook.com/o/F3EJQhITOlKtqJoq3nND/spec/dex-screener-api.yaml
- Solana RPC `getTransaction` — https://solana.com/docs/rpc/http/gettransaction
- Solana RPC `getSignaturesForAddress` — https://solana.com/docs/rpc/http/getsignaturesforaddress (the `limit` ≤ 1000 figure is from this page, which we did not re-fetch this session **[UNCERTAIN — re-check]**)
- Solana WebSocket `logsSubscribe` — https://solana.com/docs/rpc/websocket/logssubscribe
- @solana/web3.js security advisory GHSA-jcxm-7wvp-g6p5 — https://github.com/solana-labs/solana-web3.js/security/advisories/GHSA-jcxm-7wvp-g6p5 (cited from prior knowledge, not re-fetched **[UNCERTAIN — re-check]**)
- Live probes P1–P8 (§0.1), run by the report author on 2026-10-16.

*Scope limits of this report: no code was built. The probes were short, single-location samples. No paid services were tested. Free-tier quotas of hosting providers were not verified.*
