A Node.js/Express backend providing:
- Token Insight API — Fetches CoinGecko market data, builds a structured prompt, calls an AI model, and returns a validated JSON insight.
- HyperLiquid Wallet PnL API — Fetches fills, funding, and positions from HyperLiquid and returns daily realized/unrealized PnL, fees, and funding for any wallet.
cp .env.example .env # Edit AI_PROVIDER and keys as needed
docker-compose up --buildAPI is available at http://localhost:3000.
Requirements: Node.js 18+
npm install
cp .env.example .env # Configure your .env
npm start # Production
npm run dev # Development (nodemon)Copy .env.example to .env and fill in the values you need.
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
HTTP port |
AI_PROVIDER |
mock |
openai / huggingface / mock |
OPENAI_API_KEY |
— | Required when AI_PROVIDER=openai |
OPENAI_MODEL |
gpt-4o-mini |
OpenAI model ID |
HUGGINGFACE_API_KEY |
— | Required when AI_PROVIDER=huggingface |
HF_MODEL |
mistralai/Mistral-7B-Instruct-v0.2 |
HuggingFace Inference API model |
COINGECKO_API_KEY |
(empty) | Optional — leave empty to use the public API |
Never commit
.env— it is in.gitignore. Only.env.exampleis committed.
Works out of the box. Generates rule-based insights from the market data.
AI_PROVIDER=mock
Get a key at https://platform.openai.com/api-keys.
AI_PROVIDER=openai
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4o-mini
Get a free token at https://huggingface.co/settings/tokens.
AI_PROVIDER=huggingface
HUGGINGFACE_API_KEY=hf_...
HF_MODEL=mistralai/Mistral-7B-Instruct-v0.2
If the AI call fails for any reason the service automatically falls back to mock mode so the endpoint never hard-fails because of AI.
GET /health
{ "status": "ok", "timestamp": "2025-09-22T12:00:00.000Z" }This endpoint uses CoinGecko public endpoints by default, so a CoinGecko API key is not required for local development.
POST /api/token/:id/insight
Content-Type: application/json
Path param: :id — CoinGecko token ID (e.g. bitcoin, chainlink, ethereum)
Request body (all optional):
{
"vs_currency": "usd",
"history_days": 30
}Example:
curl -s -X POST http://localhost:3000/api/token/chainlink/insight \
-H "Content-Type: application/json" \
-d '{"vs_currency":"usd","history_days":30}' | jqResponse:
{
"source": "coingecko",
"token": {
"id": "chainlink",
"symbol": "link",
"name": "Chainlink",
"market_data": {
"current_price_usd": 7.23,
"market_cap_usd": 3500000000,
"total_volume_usd": 120000000,
"price_change_percentage_24h": -1.2,
"price_change_percentage_7d": 3.5,
"price_change_percentage_30d": -5.2,
"ath_usd": 52.88,
"atl_usd": 0.148183
}
},
"insight": {
"reasoning": "Chainlink is trading near $7.23 with a modest 24h decline...",
"sentiment": "Neutral",
"key_factors": ["24h price change: -1.20%", "Market cap: $3.50B"],
"risk_level": "Low"
},
"model": { "provider": "mock", "model": "rule-based-fallback" }
}GET /api/hyperliquid/:wallet/pnl?start=YYYY-MM-DD&end=YYYY-MM-DD
Path param: :wallet — Ethereum-style wallet address (0x + 40 hex chars)
Query params:
| Param | Required | Example |
|---|---|---|
start |
Yes | 2025-08-01 |
end |
Yes | 2025-08-03 |
Max range: 90 days.
Example:
curl -s "http://localhost:3000/api/hyperliquid/0xabc123.../pnl?start=2025-08-01&end=2025-08-03" | jqResponse:
{
"wallet": "0xabc123...",
"start": "2025-08-01",
"end": "2025-08-03",
"daily": [
{
"date": "2025-08-01",
"realized_pnl_usd": 120.5,
"unrealized_pnl_usd": -15.3,
"fees_usd": 2.1,
"funding_usd": -0.5,
"net_pnl_usd": 102.6,
"equity_usd": 10102.6
}
],
"summary": {
"total_realized_usd": 120.5,
"total_unrealized_usd": -25.3,
"total_fees_usd": 3.3,
"total_funding_usd": -0.8,
"net_pnl_usd": 91.1
},
"diagnostics": {
"data_source": "hyperliquid_api",
"last_api_call": "2025-09-22T12:00:00Z",
"fills_in_range": 5,
"notes": "Realized PnL and fees sourced from fills. Funding sourced from userFundingHistory. Unrealized PnL is estimated using current mid prices as mark-to-market."
}
}PnL Calculation:
| Field | Source |
|---|---|
realized_pnl_usd |
Sum of closedPnl from fills on that day |
fees_usd |
Sum of fee from fills on that day |
funding_usd |
Sum of usdc from userFundingHistory on that day |
unrealized_pnl_usd |
Open positions reconstructed from fills × current mid prices |
net_pnl_usd |
realized + unrealized − fees + funding |
equity_usd |
Running account equity anchored to current accountValue |
npm test # Run all tests with coverage
npm run test:watch # Watch modeTests use Jest + Supertest. External services (CoinGecko, HyperLiquid, AI) are fully mocked.
src/
app.js # Express app entry point
routes/
token.js # POST /api/token/:id/insight
hyperliquid.js # GET /api/hyperliquid/:wallet/pnl
services/
coingeckoService.js # CoinGecko API client
aiService.js # AI provider abstraction (OpenAI / HuggingFace / mock)
hyperliquidService.js # HyperLiquid API client + data assembly
utils/
pnlCalculator.js # Daily PnL aggregation + position reconstruction
validators.js # Input validation helpers
tests/
token.test.js
hyperliquid.test.js
pnlCalculator.test.js
All errors follow:
{ "error": "<human-readable message>" }| Status | Cause |
|---|---|
| 400 | Invalid wallet address / bad date format / range |
| 404 | Token not found on CoinGecko |
| 429 | CoinGecko rate limit exceeded |
| 500 | Unexpected server / upstream API error |