pump-analyzer-solana

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PumpAnalyzer — Solana Token Monitoring Platform

PumpAnalyzer — Solana代币监控平台

Skill by ara.so — Daily 2026 Skills collection.
PumpAnalyzer is a static front-end platform (pure HTML/CSS/JS, no build tools) that provides real-time monitoring, analytics, and alerts for tokens launched on Pump.fun on the Solana blockchain. It connects to Pump.fun's WebSocket API for sub-second updates, displays price/volume charts, supports custom alert criteria, and includes non-custodial Solana wallet connection.

来自ara.so的技能 — 2026每日技能合集。
PumpAnalyzer是一个静态前端平台(纯HTML/CSS/JS,无需构建工具),为Solana区块链上Pump.fun平台发行的代币提供实时监控、分析与告警功能。它通过连接Pump.fun的WebSocket API实现亚秒级更新,展示价格/交易量图表,支持自定义告警规则,还集成了非托管Solana钱包连接功能。

Installation

安装

bash
git clone https://github.com/happyboy4ty25/pump-analyzer.git
cd pump-analyzer
open index.html   # or use a local dev server
No npm, no bundler, no dependencies — open
index.html
directly in a browser or serve with any static file server:
bash
undefined
bash
git clone https://github.com/happyboy4ty25/pump-analyzer.git
cd pump-analyzer
open index.html   # 或使用本地开发服务器
无需npm、无需打包工具、无依赖项 — 直接在浏览器中打开
index.html
,或使用任何静态文件服务器运行:
bash
undefined

Python

Python

python3 -m http.server 8080
python3 -m http.server 8080

Node (npx)

Node (npx)

npx serve .
npx serve .

VS Code

VS Code

Use the "Live Server" extension

使用「Live Server」扩展


---

---

Project Structure

项目结构

pump-analyzer/
├── index.html          # Main landing page & app shell
├── css/
│   └── style.css       # All styles, animations, responsive layout
├── js/
│   ├── main.js         # App init, UI interactions, animations
│   ├── websocket.js    # Pump.fun WebSocket connection & event handling
│   ├── wallet.js       # Solana wallet adapter (Phantom, Solflare, etc.)
│   ├── alerts.js       # Custom alert criteria logic
│   └── charts.js       # Price/volume chart rendering
└── assets/
    └── ...             # Icons, images

pump-analyzer/
├── index.html          # 主页面与应用外壳
├── css/
│   └── style.css       # 所有样式、动画、响应式布局
├── js/
│   ├── main.js         # 应用初始化、UI交互、动画
│   ├── websocket.js    # Pump.fun WebSocket连接与事件处理
│   ├── wallet.js       # Solana钱包适配器(Phantom、Solflare等)
│   ├── alerts.js       # 自定义告警规则逻辑
│   └── charts.js       # 价格/交易量图表渲染
└── assets/
    └── ...             # 图标、图片

Key Concepts & Architecture

核心概念与架构

1. Pump.fun WebSocket Connection

1. Pump.fun WebSocket连接

PumpAnalyzer subscribes to Pump.fun's real-time data stream. The core pattern:
javascript
// js/websocket.js

const PUMP_FUN_WS_URL = 'wss://pumpportal.fun/api/data';

class PumpWebSocket {
  constructor(onToken, onTrade) {
    this.onToken = onToken;  // callback for new token launches
    this.onTrade = onTrade;  // callback for trade events
    this.ws = null;
    this.reconnectDelay = 1000;
  }

  connect() {
    this.ws = new WebSocket(PUMP_FUN_WS_URL);

    this.ws.addEventListener('open', () => {
      console.log('[PumpWS] Connected');
      this.reconnectDelay = 1000;

      // Subscribe to new token creation events
      this.ws.send(JSON.stringify({
        method: 'subscribeNewToken'
      }));

      // Subscribe to all trades on new tokens
      this.ws.send(JSON.stringify({
        method: 'subscribeTokenTrade',
        keys: []  // empty = all tokens
      }));
    });

    this.ws.addEventListener('message', (event) => {
      const data = JSON.parse(event.data);
      if (data.txType === 'create') {
        this.onToken(data);
      } else if (data.txType === 'buy' || data.txType === 'sell') {
        this.onTrade(data);
      }
    });

    this.ws.addEventListener('close', () => {
      console.warn('[PumpWS] Disconnected — reconnecting in', this.reconnectDelay, 'ms');
      setTimeout(() => this.connect(), this.reconnectDelay);
      this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
    });

    this.ws.addEventListener('error', (err) => {
      console.error('[PumpWS] Error:', err);
      this.ws.close();
    });
  }

  // Subscribe to trades for a specific token mint
  subscribeToken(mintAddress) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({
        method: 'subscribeTokenTrade',
        keys: [mintAddress]
      }));
    }
  }

  disconnect() {
    this.ws?.close();
  }
}

export default PumpWebSocket;
PumpAnalyzer订阅Pump.fun的实时数据流,核心实现模式如下:
javascript
// js/websocket.js

const PUMP_FUN_WS_URL = 'wss://pumpportal.fun/api/data';

class PumpWebSocket {
  constructor(onToken, onTrade) {
    this.onToken = onToken;  // 新代币发行的回调函数
    this.onTrade = onTrade;  // 交易事件的回调函数
    this.ws = null;
    this.reconnectDelay = 1000;
  }

  connect() {
    this.ws = new WebSocket(PUMP_FUN_WS_URL);

    this.ws.addEventListener('open', () => {
      console.log('[PumpWS] Connected');
      this.reconnectDelay = 1000;

      // 订阅新代币创建事件
      this.ws.send(JSON.stringify({
        method: 'subscribeNewToken'
      }));

      // 订阅所有新代币的交易事件
      this.ws.send(JSON.stringify({
        method: 'subscribeTokenTrade',
        keys: []  // 空数组表示订阅所有代币
      }));
    });

    this.ws.addEventListener('message', (event) => {
      const data = JSON.parse(event.data);
      if (data.txType === 'create') {
        this.onToken(data);
      } else if (data.txType === 'buy' || data.txType === 'sell') {
        this.onTrade(data);
      }
    });

    this.ws.addEventListener('close', () => {
      console.warn('[PumpWS] Disconnected — reconnecting in', this.reconnectDelay, 'ms');
      setTimeout(() => this.connect(), this.reconnectDelay);
      this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
    });

    this.ws.addEventListener('error', (err) => {
      console.error('[PumpWS] Error:', err);
      this.ws.close();
    });
  }

  // 订阅特定代币铸币地址的交易
  subscribeToken(mintAddress) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({
        method: 'subscribeTokenTrade',
        keys: [mintAddress]
      }));
    }
  }

  disconnect() {
    this.ws?.close();
  }
}

export default PumpWebSocket;

2. Handling New Token Events

2. 处理新代币事件

javascript
// js/main.js

import PumpWebSocket from './websocket.js';

const tokenList = [];

function onNewToken(tokenData) {
  // tokenData shape from Pump.fun:
  // {
  //   signature: string,
  //   mint: string,          // token mint address
  //   traderPublicKey: string,
  //   txType: 'create',
  //   name: string,
  //   symbol: string,
  //   description: string,
  //   imageUri: string,
  //   initialBuy: number,    // SOL amount
  //   marketCapSol: number,
  //   uri: string,
  //   timestamp: number
  // }

  tokenList.unshift(tokenData);
  renderTokenCard(tokenData);
  checkAlerts(tokenData);
}

function onTrade(tradeData) {
  // tradeData shape:
  // {
  //   signature: string,
  //   mint: string,
  //   traderPublicKey: string,
  //   txType: 'buy' | 'sell',
  //   tokenAmount: number,
  //   solAmount: number,
  //   newTokenBalance: number,
  //   bondingCurveKey: string,
  //   vTokensInBondingCurve: number,
  //   vSolInBondingCurve: number,
  //   marketCapSol: number,
  //   timestamp: number
  // }

  updateTokenMetrics(tradeData.mint, tradeData);
}

const pumpWS = new PumpWebSocket(onNewToken, onTrade);
pumpWS.connect();
javascript
// js/main.js

import PumpWebSocket from './websocket.js';

const tokenList = [];

function onNewToken(tokenData) {
  // Pump.fun返回的tokenData结构:
  // {
  //   signature: string,
  //   mint: string,          // 代币铸币地址
  //   traderPublicKey: string,
  //   txType: 'create',
  //   name: string,
  //   symbol: string,
  //   description: string,
  //   imageUri: string,
  //   initialBuy: number,    // SOL数量
  //   marketCapSol: number,
  //   uri: string,
  //   timestamp: number
  // }

  tokenList.unshift(tokenData);
  renderTokenCard(tokenData);
  checkAlerts(tokenData);
}

function onTrade(tradeData) {
  // tradeData结构:
  // {
  //   signature: string,
  //   mint: string,
  //   traderPublicKey: string,
  //   txType: 'buy' | 'sell',
  //   tokenAmount: number,
  //   solAmount: number,
  //   newTokenBalance: number,
  //   bondingCurveKey: string,
  //   vTokensInBondingCurve: number,
  //   vSolInBondingCurve: number,
  //   marketCapSol: number,
  //   timestamp: number
  // }

  updateTokenMetrics(tradeData.mint, tradeData);
}

const pumpWS = new PumpWebSocket(onNewToken, onTrade);
pumpWS.connect();

3. Rendering Token Cards

3. 渲染代币卡片

javascript
// js/main.js

function renderTokenCard(token) {
  const container = document.getElementById('token-feed');

  const card = document.createElement('div');
  card.className = 'token-card';
  card.dataset.mint = token.mint;

  card.innerHTML = `
    <div class="token-header">
      <img src="${token.imageUri || 'assets/placeholder.png'}" 
           alt="${token.symbol}" 
           class="token-image"
           onerror="this.src='assets/placeholder.png'">
      <div class="token-info">
        <span class="token-name">${escapeHtml(token.name)}</span>
        <span class="token-symbol">$${escapeHtml(token.symbol)}</span>
      </div>
      <span class="token-time">${formatTimestamp(token.timestamp)}</span>
    </div>
    <div class="token-metrics">
      <div class="metric">
        <label>Market Cap</label>
        <span class="market-cap">${formatSol(token.marketCapSol)} SOL</span>
      </div>
      <div class="metric">
        <label>Initial Buy</label>
        <span>${formatSol(token.initialBuy)} SOL</span>
      </div>
    </div>
    <div class="token-actions">
      <a href="https://pump.fun/${token.mint}" target="_blank" rel="noopener" 
         class="btn btn-small">View on Pump.fun</a>
      <button class="btn btn-small btn-outline" 
              onclick="setAlert('${token.mint}')">Set Alert</button>
    </div>
  `;

  // Animate in
  card.style.opacity = '0';
  card.style.transform = 'translateY(-10px)';
  container.prepend(card);
  requestAnimationFrame(() => {
    card.style.transition = 'opacity 0.3s, transform 0.3s';
    card.style.opacity = '1';
    card.style.transform = 'translateY(0)';
  });

  // Cap the list at 50 cards
  while (container.children.length > 50) {
    container.removeChild(container.lastChild);
  }
}

function escapeHtml(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

function formatSol(amount) {
  return amount ? Number(amount).toFixed(2) : '0.00';
}

function formatTimestamp(ts) {
  return new Date(ts * 1000).toLocaleTimeString();
}
javascript
// js/main.js

function renderTokenCard(token) {
  const container = document.getElementById('token-feed');

  const card = document.createElement('div');
  card.className = 'token-card';
  card.dataset.mint = token.mint;

  card.innerHTML = `
    <div class="token-header">
      <img src="${token.imageUri || 'assets/placeholder.png'}" 
           alt="${token.symbol}" 
           class="token-image"
           onerror="this.src='assets/placeholder.png'">
      <div class="token-info">
        <span class="token-name">${escapeHtml(token.name)}</span>
        <span class="token-symbol">$${escapeHtml(token.symbol)}</span>
      </div>
      <span class="token-time">${formatTimestamp(token.timestamp)}</span>
    </div>
    <div class="token-metrics">
      <div class="metric">
        <label>市值</label>
        <span class="market-cap">${formatSol(token.marketCapSol)} SOL</span>
      </div>
      <div class="metric">
        <label>初始买入额</label>
        <span>${formatSol(token.initialBuy)} SOL</span>
      </div>
    </div>
    <div class="token-actions">
      <a href="https://pump.fun/${token.mint}" target="_blank" rel="noopener" 
         class="btn btn-small">在Pump.fun查看</a>
      <button class="btn btn-small btn-outline" 
              onclick="setAlert('${token.mint}')">设置告警</button>
    </div>
  `;

  // 入场动画
  card.style.opacity = '0';
  card.style.transform = 'translateY(-10px)';
  container.prepend(card);
  requestAnimationFrame(() => {
    card.style.transition = 'opacity 0.3s, transform 0.3s';
    card.style.opacity = '1';
    card.style.transform = 'translateY(0)';
  });

  // 限制列表最多显示50张卡片
  while (container.children.length > 50) {
    container.removeChild(container.lastChild);
  }
}

function escapeHtml(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

function formatSol(amount) {
  return amount ? Number(amount).toFixed(2) : '0.00';
}

function formatTimestamp(ts) {
  return new Date(ts * 1000).toLocaleTimeString();
}

4. Custom Alerts System

4. 自定义告警系统

javascript
// js/alerts.js

const MAX_FREE_ALERTS = 5;

class AlertManager {
  constructor() {
    this.alerts = JSON.parse(localStorage.getItem('pump_alerts') || '[]');
    this.dailyCount = parseInt(localStorage.getItem('pump_alert_count') || '0');
    this.plan = localStorage.getItem('pump_plan') || 'free';
  }

  canAddAlert() {
    if (this.plan !== 'free') return true;
    return this.dailyCount < MAX_FREE_ALERTS;
  }

  addAlert({ mint, criteria }) {
    // criteria: { minMarketCap, maxMarketCap, minVolume, keywords }
    if (!this.canAddAlert()) {
      showUpgradeModal('You've reached the free plan limit of 5 alerts/day.');
      return false;
    }

    const alert = { id: Date.now(), mint, criteria, active: true };
    this.alerts.push(alert);
    this._save();
    return alert;
  }

  checkToken(tokenData) {
    for (const alert of this.alerts) {
      if (!alert.active) continue;
      if (this._matches(tokenData, alert.criteria)) {
        this._trigger(alert, tokenData);
      }
    }
  }

  _matches(token, criteria) {
    if (criteria.minMarketCap && token.marketCapSol < criteria.minMarketCap) return false;
    if (criteria.maxMarketCap && token.marketCapSol > criteria.maxMarketCap) return false;
    if (criteria.keywords?.length) {
      const text = `${token.name} ${token.symbol} ${token.description}`.toLowerCase();
      if (!criteria.keywords.some(k => text.includes(k.toLowerCase()))) return false;
    }
    return true;
  }

  _trigger(alert, token) {
    // Browser notification
    if (Notification.permission === 'granted') {
      new Notification(`🚨 Alert: ${token.name} ($${token.symbol})`, {
        body: `Market cap: ${token.marketCapSol.toFixed(2)} SOL`,
        icon: token.imageUri || 'assets/icon.png'
      });
    }

    // In-app notification
    showInAppAlert(token);

    this.dailyCount++;
    localStorage.setItem('pump_alert_count', this.dailyCount);
  }

  _save() {
    localStorage.setItem('pump_alerts', JSON.stringify(this.alerts));
  }
}

export const alertManager = new AlertManager();

// Request notification permission on load
if ('Notification' in window && Notification.permission === 'default') {
  Notification.requestPermission();
}
javascript
// js/alerts.js

const MAX_FREE_ALERTS = 5;

class AlertManager {
  constructor() {
    this.alerts = JSON.parse(localStorage.getItem('pump_alerts') || '[]');
    this.dailyCount = parseInt(localStorage.getItem('pump_alert_count') || '0');
    this.plan = localStorage.getItem('pump_plan') || 'free';
  }

  canAddAlert() {
    if (this.plan !== 'free') return true;
    return this.dailyCount < MAX_FREE_ALERTS;
  }

  addAlert({ mint, criteria }) {
    // criteria: { minMarketCap, maxMarketCap, minVolume, keywords }
    if (!this.canAddAlert()) {
      showUpgradeModal('您已达到免费计划每日5条告警的上限。');
      return false;
    }

    const alert = { id: Date.now(), mint, criteria, active: true };
    this.alerts.push(alert);
    this._save();
    return alert;
  }

  checkToken(tokenData) {
    for (const alert of this.alerts) {
      if (!alert.active) continue;
      if (this._matches(tokenData, alert.criteria)) {
        this._trigger(alert, tokenData);
      }
    }
  }

  _matches(token, criteria) {
    if (criteria.minMarketCap && token.marketCapSol < criteria.minMarketCap) return false;
    if (criteria.maxMarketCap && token.marketCapSol > criteria.maxMarketCap) return false;
    if (criteria.keywords?.length) {
      const text = `${token.name} ${token.symbol} ${token.description}`.toLowerCase();
      if (!criteria.keywords.some(k => text.includes(k.toLowerCase()))) return false;
    }
    return true;
  }

  _trigger(alert, token) {
    // 浏览器通知
    if (Notification.permission === 'granted') {
      new Notification(`🚨 告警: ${token.name} ($${token.symbol})`, {
        body: `市值: ${token.marketCapSol.toFixed(2)} SOL`,
        icon: token.imageUri || 'assets/icon.png'
      });
    }

    // 应用内通知
    showInAppAlert(token);

    this.dailyCount++;
    localStorage.setItem('pump_alert_count', this.dailyCount);
  }

  _save() {
    localStorage.setItem('pump_alerts', JSON.stringify(this.alerts));
  }
}

export const alertManager = new AlertManager();

// 页面加载时请求通知权限
if ('Notification' in window && Notification.permission === 'default') {
  Notification.requestPermission();
}

5. Solana Wallet Connection (Non-Custodial)

5. Solana非托管钱包连接

javascript
// js/wallet.js

class SolanaWalletConnect {
  constructor() {
    this.publicKey = null;
    this.provider = null;
  }

  getProvider() {
    // Phantom
    if ('phantom' in window && window.phantom?.solana?.isPhantom) {
      return window.phantom.solana;
    }
    // Solflare
    if ('solflare' in window && window.solflare?.isSolflare) {
      return window.solflare;
    }
    return null;
  }

  async connect() {
    this.provider = this.getProvider();

    if (!this.provider) {
      window.open('https://phantom.app/', '_blank');
      throw new Error('No Solana wallet found. Please install Phantom.');
    }

    try {
      const resp = await this.provider.connect();
      this.publicKey = resp.publicKey.toString();
      this._onConnected();
      return this.publicKey;
    } catch (err) {
      if (err.code === 4001) {
        throw new Error('Connection rejected by user.');
      }
      throw err;
    }
  }

  async disconnect() {
    await this.provider?.disconnect();
    this.publicKey = null;
    this._onDisconnected();
  }

  _onConnected() {
    const btn = document.getElementById('wallet-btn');
    if (btn) {
      btn.textContent = `${this.publicKey.slice(0, 4)}...${this.publicKey.slice(-4)}`;
      btn.classList.add('connected');
    }

    // Unlock plan features based on on-chain subscription (check via RPC)
    this.checkSubscription();
  }

  _onDisconnected() {
    const btn = document.getElementById('wallet-btn');
    if (btn) {
      btn.textContent = 'Connect Wallet';
      btn.classList.remove('connected');
    }
  }

  async checkSubscription() {
    // Query your backend or on-chain program to verify subscription tier
    const RPC = 'https://api.mainnet-beta.solana.com';
    // ... implement based on your subscription contract
  }
}

export const wallet = new SolanaWalletConnect();

// Wire up button
document.getElementById('wallet-btn')?.addEventListener('click', async () => {
  try {
    if (wallet.publicKey) {
      await wallet.disconnect();
    } else {
      await wallet.connect();
    }
  } catch (err) {
    console.error('Wallet error:', err.message);
    showToast(err.message, 'error');
  }
});
javascript
// js/wallet.js

class SolanaWalletConnect {
  constructor() {
    this.publicKey = null;
    this.provider = null;
  }

  getProvider() {
    // Phantom钱包
    if ('phantom' in window && window.phantom?.solana?.isPhantom) {
      return window.phantom.solana;
    }
    // Solflare钱包
    if ('solflare' in window && window.solflare?.isSolflare) {
      return window.solflare;
    }
    return null;
  }

  async connect() {
    this.provider = this.getProvider();

    if (!this.provider) {
      window.open('https://phantom.app/', '_blank');
      throw new Error('未找到Solana钱包,请安装Phantom。');
    }

    try {
      const resp = await this.provider.connect();
      this.publicKey = resp.publicKey.toString();
      this._onConnected();
      return this.publicKey;
    } catch (err) {
      if (err.code === 4001) {
        throw new Error('用户拒绝连接。');
      }
      throw err;
    }
  }

  async disconnect() {
    await this.provider?.disconnect();
    this.publicKey = null;
    this._onDisconnected();
  }

  _onConnected() {
    const btn = document.getElementById('wallet-btn');
    if (btn) {
      btn.textContent = `${this.publicKey.slice(0, 4)}...${this.publicKey.slice(-4)}`;
      btn.classList.add('connected');
    }

    // 根据链上订阅解锁高级功能(通过RPC查询)
    this.checkSubscription();
  }

  _onDisconnected() {
    const btn = document.getElementById('wallet-btn');
    if (btn) {
      btn.textContent = '连接钱包';
      btn.classList.remove('connected');
    }
  }

  async checkSubscription() {
    // 查询后端或链上程序以验证订阅等级
    const RPC = 'https://api.mainnet-beta.solana.com';
    // ... 根据您的订阅合约实现逻辑
  }
}

export const wallet = new SolanaWalletConnect();

// 绑定按钮事件
document.getElementById('wallet-btn')?.addEventListener('click', async () => {
  try {
    if (wallet.publicKey) {
      await wallet.disconnect();
    } else {
      await wallet.connect();
    }
  } catch (err) {
    console.error('Wallet error:', err.message);
    showToast(err.message, 'error');
  }
});

6. Simple Price Chart (Canvas API)

6. 简易价格图表(Canvas API)

javascript
// js/charts.js

class PriceChart {
  constructor(canvasId) {
    this.canvas = document.getElementById(canvasId);
    this.ctx = this.canvas.getContext('2d');
    this.dataPoints = [];
    this.maxPoints = 60;
  }

  addPoint(marketCapSol, timestamp) {
    this.dataPoints.push({ value: marketCapSol, time: timestamp });
    if (this.dataPoints.length > this.maxPoints) {
      this.dataPoints.shift();
    }
    this.render();
  }

  render() {
    const { ctx, canvas, dataPoints } = this;
    const { width, height } = canvas;

    ctx.clearRect(0, 0, width, height);

    if (dataPoints.length < 2) return;

    const values = dataPoints.map(p => p.value);
    const min = Math.min(...values);
    const max = Math.max(...values);
    const range = max - min || 1;

    const xStep = width / (dataPoints.length - 1);

    // Draw gradient fill
    const gradient = ctx.createLinearGradient(0, 0, 0, height);
    gradient.addColorStop(0, 'rgba(20, 241, 149, 0.3)');
    gradient.addColorStop(1, 'rgba(20, 241, 149, 0)');

    ctx.beginPath();
    ctx.moveTo(0, height - ((dataPoints[0].value - min) / range) * height);

    dataPoints.forEach((point, i) => {
      const x = i * xStep;
      const y = height - ((point.value - min) / range) * height;
      ctx.lineTo(x, y);
    });

    ctx.lineTo(width, height);
    ctx.lineTo(0, height);
    ctx.closePath();
    ctx.fillStyle = gradient;
    ctx.fill();

    // Draw line
    ctx.beginPath();
    ctx.strokeStyle = '#14F195';
    ctx.lineWidth = 2;
    dataPoints.forEach((point, i) => {
      const x = i * xStep;
      const y = height - ((point.value - min) / range) * height;
      i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
    });
    ctx.stroke();
  }
}

export default PriceChart;

javascript
// js/charts.js

class PriceChart {
  constructor(canvasId) {
    this.canvas = document.getElementById(canvasId);
    this.ctx = this.canvas.getContext('2d');
    this.dataPoints = [];
    this.maxPoints = 60;
  }

  addPoint(marketCapSol, timestamp) {
    this.dataPoints.push({ value: marketCapSol, time: timestamp });
    if (this.dataPoints.length > this.maxPoints) {
      this.dataPoints.shift();
    }
    this.render();
  }

  render() {
    const { ctx, canvas, dataPoints } = this;
    const { width, height } = canvas;

    ctx.clearRect(0, 0, width, height);

    if (dataPoints.length < 2) return;

    const values = dataPoints.map(p => p.value);
    const min = Math.min(...values);
    const max = Math.max(...values);
    const range = max - min || 1;

    const xStep = width / (dataPoints.length - 1);

    // 绘制渐变填充
    const gradient = ctx.createLinearGradient(0, 0, 0, height);
    gradient.addColorStop(0, 'rgba(20, 241, 149, 0.3)');
    gradient.addColorStop(1, 'rgba(20, 241, 149, 0)');

    ctx.beginPath();
    ctx.moveTo(0, height - ((dataPoints[0].value - min) / range) * height);

    dataPoints.forEach((point, i) => {
      const x = i * xStep;
      const y = height - ((point.value - min) / range) * height;
      ctx.lineTo(x, y);
    });

    ctx.lineTo(width, height);
    ctx.lineTo(0, height);
    ctx.closePath();
    ctx.fillStyle = gradient;
    ctx.fill();

    // 绘制线条
    ctx.beginPath();
    ctx.strokeStyle = '#14F195';
    ctx.lineWidth = 2;
    dataPoints.forEach((point, i) => {
      const x = i * xStep;
      const y = height - ((point.value - min) / range) * height;
      i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
    });
    ctx.stroke();
  }
}

export default PriceChart;

Configuration

配置

All configuration is done via constants at the top of each JS file. No
.env
file needed for the front-end — but if you add a backend:
javascript
// js/config.js
const CONFIG = {
  WS_URL: 'wss://pumpportal.fun/api/data',
  RPC_URL: process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com',
  API_BASE: process.env.API_BASE_URL || 'https://pump-analyzer.com/api',
  PLANS: {
    free:  { alertsPerDay: 5,         price: 0 },
    pro:   { alertsPerDay: Infinity,   price: 29,  sol: 0.5 },
    elite: { alertsPerDay: Infinity,   price: 99,  sol: 1.5 }
  }
};

所有配置通过每个JS文件顶部的常量完成。前端无需
.env
文件 — 若添加后端则可使用:
javascript
// js/config.js
const CONFIG = {
  WS_URL: 'wss://pumpportal.fun/api/data',
  RPC_URL: process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com',
  API_BASE: process.env.API_BASE_URL || 'https://pump-analyzer.com/api',
  PLANS: {
    free:  { alertsPerDay: 5,         price: 0 },
    pro:   { alertsPerDay: Infinity,   price: 29,  sol: 0.5 },
    elite: { alertsPerDay: Infinity,   price: 99,  sol: 1.5 }
  }
};

Common Patterns

常用模式

Filter tokens by keyword on arrival

按关键词过滤新代币

javascript
function onNewToken(token) {
  const keyword = document.getElementById('filter-input').value.toLowerCase();
  if (keyword && !`${token.name} ${token.symbol}`.toLowerCase().includes(keyword)) return;
  renderTokenCard(token);
}
javascript
function onNewToken(token) {
  const keyword = document.getElementById('filter-input').value.toLowerCase();
  if (keyword && !`${token.name} ${token.symbol}`.toLowerCase().includes(keyword)) return;
  renderTokenCard(token);
}

Debounce rapid trade updates

防抖处理频繁交易更新

javascript
const updateQueue = new Map();

function onTrade(trade) {
  clearTimeout(updateQueue.get(trade.mint));
  updateQueue.set(trade.mint, setTimeout(() => {
    updateTokenMetrics(trade.mint, trade);
    updateQueue.delete(trade.mint);
  }, 200));
}
javascript
const updateQueue = new Map();

function onTrade(trade) {
  clearTimeout(updateQueue.get(trade.mint));
  updateQueue.set(trade.mint, setTimeout(() => {
    updateTokenMetrics(trade.mint, trade);
    updateQueue.delete(trade.mint);
  }, 200));
}

Show upgrade modal for free plan limits

免费计划限额时展示升级弹窗

javascript
function showUpgradeModal(reason) {
  document.getElementById('upgrade-reason').textContent = reason;
  document.getElementById('upgrade-modal').classList.add('visible');
}

javascript
function showUpgradeModal(reason) {
  document.getElementById('upgrade-reason').textContent = reason;
  document.getElementById('upgrade-modal').classList.add('visible');
}

Troubleshooting

故障排查

IssueCauseFix
WebSocket won't connectBrowser blocks WSS or wrong URLCheck
wss://pumpportal.fun/api/data
is reachable; use DevTools Network tab
No tokens appearingSubscription message not sent on
open
Ensure
subscribeNewToken
is sent inside
ws.addEventListener('open', ...)
Wallet button does nothingWallet extension not installedDetect
window.phantom
before calling
.connect()
Notifications not firingPermission not grantedCall
Notification.requestPermission()
after a user gesture
Cards not updating market cap
mint
mismatch between token and trade events
Normalize mint addresses to strings before comparison
Page flickers on new tokenDOM prepend causes reflowUse
requestAnimationFrame
+ CSS transitions for card entry

问题原因解决方法
WebSocket无法连接浏览器阻止WSS或URL错误检查
wss://pumpportal.fun/api/data
是否可访问;使用开发者工具网络面板排查
无代币显示连接成功时未发送订阅消息确保
subscribeNewToken
ws.addEventListener('open', ...)
内部发送
钱包按钮无响应未安装钱包扩展调用
.connect()
前先检测
window.phantom
通知不触发未获取权限在用户交互后调用
Notification.requestPermission()
卡片未更新市值代币与交易事件的
mint
不匹配
比较前将铸币地址统一转换为字符串
新代币出现时页面闪烁DOM前置操作导致重排使用
requestAnimationFrame
+ CSS过渡实现卡片入场动画

Pricing / Plan Gating Pattern

定价/计划权限控制模式

javascript
// Check plan before unlocking features
function requirePlan(minimumPlan, action) {
  const planRank = { free: 0, pro: 1, elite: 2 };
  const userPlan = localStorage.getItem('pump_plan') || 'free';

  if (planRank[userPlan] >= planRank[minimumPlan]) {
    action();
  } else {
    showUpgradeModal(`This feature requires the ${minimumPlan} plan.`);
  }
}

// Usage
requirePlan('pro', () => enableUnlimitedAlerts());
requirePlan('elite', () => enableAIInsights());

javascript
// 解锁功能前检查用户计划
function requirePlan(minimumPlan, action) {
  const planRank = { free: 0, pro: 1, elite: 2 };
  const userPlan = localStorage.getItem('pump_plan') || 'free';

  if (planRank[userPlan] >= planRank[minimumPlan]) {
    action();
  } else {
    showUpgradeModal(`此功能需要${minimumPlan}计划。`);
  }
}

// 使用示例
requirePlan('pro', () => enableUnlimitedAlerts());
requirePlan('elite', () => enableAIInsights());

Resources

资源