Horizon Pulse
Pay-per-call crypto market pulse, signals, funding, yield rankings, portfolio risk, gas, plus public URL fetch → clean text, universal http proxy, and extract → structured fields for AI agents, monetized with the x402 protocol on Base mainnet and Solana mainnet (USDC, exact, same price on both).
This repo is a Next.js App Router service ready to deploy (e.g. Vercel) and push to:
https://github.com/horizon-pulse/horizon-pulse.git
Public host
| Role | URL |
|---|
| Canonical | https://horizonpulse.dev |
| Backup (Vercel) | https://horizon-pulse-seven.vercel.app |
Prefer horizonpulse.dev in agent docs, OpenAPI, and clients. The *.vercel.app URL remains a working fallback.
What it is
- Agents hit live paid HTTP endpoints (crypto routes frozen;
/api/fetch + /api/http + /api/extract + /api/x402-check + /api/bazaar-check + /api/market/search + /api/market/quote + /api/screenshot + /api/search + /api/pdf non-crypto LIVE — see below).
- Unpaid requests receive HTTP 402 with x402 v2 requirements: canonical wire is the
PAYMENT-REQUIRED header, with one accepts entry per network: Base (CAIP-2 eip155:8453, Base USDC, the treasury below) and Solana (CAIP-2 solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp, USDC mint, the Solana payTo below). See docs/solana-rail.md.
- Retry with an x402 v2
PAYMENT-SIGNATURE header. The facilitator (Coinbase CDP on Base, PayAI on Solana) verifies + settles, then the route returns live data.
- No stubbed prices or fake APYs: Coinbase Exchange (CoinGecko fallback) for spot; CoinGecko OHLC (Coinbase candles fallback) for signals; OKX for perpetual funding (Binance/Bybit are often geo-blocked on Vercel); DefiLlama for yield pools; public RPC
balanceOf for portfolio (real balances only); eth_feeHistory / eth_gasPrice for gas (real fees only); /api/fetch returns best-effort cleaned text from a requested public URL (SSRF-safe, size/time capped); /api/http is a raw universal proxy (filtered headers, text|base64 body, SSRF-safe) priced $0.01 for volume; /api/extract returns best-effort structured page fields from a URL or HTML (SSRF-safe, size/time capped) priced $0.015.
- Never invent metrics; never advertise routes that 404; never cite a cached balance —
/status reads live USDC balanceOf on payTo. Honest: current payTo is interim Coinbase-custodial (not Safe/multisig).
Catalog status (post first settlement)
| Fact | Value |
|---|
| Live paid routes | See the table below — the frozen crypto routes + /api/fetch + /api/http + /api/extract + /api/x402-check + /api/bazaar-check + /api/market/search + /api/market/quote + /api/screenshot + /api/search + /api/pdf |
| First settle | /api/pulse $0.005 — tx 0xedbd1a51… |
| Crypto policy | Frozen — six crypto routes: no price changes |
| Fetch | Non-crypto LIVE: clean-text ($0.02) |
| Http | Non-crypto LIVE: universal proxy ($0.01 volume price) |
| Extract | Non-crypto LIVE: structured HTML ($0.015) |
Coming-soon / placeholder / 404 routes are not listed on /, /status, or this README.
Agent how-to (x402 v2)
GET a paid route with no payment → HTTP 402.
- Read
PAYMENT-REQUIRED (v2) and pick one accepts entry. Confirm network (eip155:8453 for Base or solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp for Solana), USDC on that network, exact atomic amount, and that network's payTo.
- Sign and retry the same
GET with PAYMENT-SIGNATURE (v2 primary; do not rely on legacy X-PAYMENT as the settle path).
- On success: the facilitator settles on the network you paid on; response body is the live JSON for that route.
Optional: OPTIONS on a paid route returns discovery + the same PAYMENT-REQUIRED challenge without charging.
Agent integration
- Skill file:
/skill.md, plain markdown an agent can follow end to end (generated from public/openapi.json by lib/agent-skill.ts).
- Guide:
/agents: pay flow, skill and MCP setup.
- Local MCP server:
mcp/, stdio, every live route as a tool, pays the x402 challenge from a buyer wallet you configure, with per-call and per-session caps (quote-only without a key). Not published to npm; install from this repo.
- Hosted MCP:
POST https://horizonpulse.dev/mcp (streamable HTTP) for MCP clients that can pay x402 themselves.
Treasury (payTo)
| Field | Value |
|---|
| payTo | 0x5b32c973596078a967562ca652761404f19be0e9 |
| Basename | horizonpulsebase.base.eth (resolves to the payTo above; a label only, the 402 carries the hex address) |
| Custody | Interim Coinbase-custodial Base address (Michael-controlled). Not a Safe or multisig. |
| Upgrade path | Move later to a non-custodial Safe / multisig when ready — same catalog and prices. |
| USDC (Base) | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| Network | base (CAIP-2 eip155:8453) |
| Facilitator | https://api.cdp.coinbase.com/platform/v2/x402 |
| Solana payTo | BjY98A6dS3GGLZdz2zHy8wK7XAwnQgNhCc66mfmBTRPz (operator-controlled; pinned in lib/solana-config.ts; payments credit its USDC token account) |
| USDC (Solana) | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
| Solana network | CAIP-2 solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp (exact only) |
| Solana facilitator | PayAI https://facilitator.payai.network |
Do not use the retired address 0xe16A1b12404cB2EbC6e783beCA6E2A9253c3dC7E.
PAY_TO may be overridden via env, but defaults to the address above. Agents should treat current payTo as the live settlement destination; custody shape may change later without changing route prices.
Endpoints
Paid (frozen crypto routes + fetch + http + extract + x402-check + bazaar-check + market/search + market/quote + screenshot + search + pdf):
| Route | Price | Auth |
|---|
GET /api/pulse | $0.005 USDC (5000 atomic) | x402 v2 |
GET /api/signals | $0.015 USDC (15000 atomic) | x402 v2 |
GET /api/yield | $0.02 USDC (20000 atomic) | x402 v2 |
GET /api/portfolio?address=0x… | $0.04 USDC (40000 atomic) | x402 v2 |
GET /api/gas | $0.01 USDC (10000 atomic) | x402 v2 |
GET /api/funding | $0.01 USDC (10000 atomic) | x402 v2 |
GET /api/fetch?url=https://… | $0.02 USDC (20000 atomic) | x402 v2 |
GET / POST /api/http | $0.01 USDC (10000 atomic) | x402 v2 |
GET / POST /api/extract | $0.015 USDC (15000 atomic) | x402 v2 |
GET /api/x402-check?url=https://… | $0.01 USDC (10000 atomic) | x402 v2 |
GET /api/bazaar-check?url=… | $0.01 USDC (10000 atomic) | x402 v2 |
GET /api/market/search?q=… | $0.005 USDC (5000 atomic), untested price | x402 v2 |
GET /api/market/quote?url=https://… | $0.005 USDC (5000 atomic), untested price | x402 v2 |
GET /api/screenshot?url=https://… | $0.02 USDC (20000 atomic) | x402 v2 |
GET /api/search?q=…&n=3 | $0.03 USDC (30000 atomic) | x402 v2 |
GET /api/pdf?url=https://… | $0.02 USDC (20000 atomic) | x402 v2 |
Free (not paid APIs):
| Route | Auth |
|---|
GET /status | public HTML — live USDC balanceOf on payTo (honest RPC error if fetch fails) |
GET / | landing — live paid catalog + agent how-to |
GET /api/pulse
Real spot prices for BTC / ETH / SOL (Coinbase Exchange, CoinGecko fallback) with per-asset momentum, plus overall momentum / sentiment / direction (up/down/flat, a neutral description of the average 24h move; renamed from signal, see CHANGELOG.md).
runtime = 'nodejs'
- Discovery: Bazaar extension +
outputSchema.input.discoverable: true in payment requirements
GET /api/signals
RSI(14), MACD(12,26,9), Bollinger bands from CoinGecko OHLC closes, plus OKX funding rates. Includes a methodology field.
runtime = 'nodejs'
- Discoverable for agents
GET /api/yield
Ranked DeFi yield pools from the public DefiLlama yields API (https://yields.llama.fi/pools).
- Hard filter: TVL ≥ $10M; exclude non-finite APY and DefiLlama
outlier pools
- Preference: keep stablecoin and/or single-asset (
exposure === "single") pools
- Transparent ranking:
preferenceTier DESC (2 = stablecoin+single, 1 = either), then APY DESC, then TVL DESC
- Every response includes a
methodology object (also on error bodies)
- Price: $0.02 USDC (
20000 atomic)
runtime = 'nodejs', dynamic = 'force-dynamic'
- Unpaid GET → 402 +
PAYMENT-REQUIRED (v2, eip155:8453) · payTo 0x5b32c973596078a967562ca652761404f19be0e9
GET /api/portfolio
On-chain portfolio snapshot for one EVM address (?address=0x…, required).
- Networks: Base + Ethereum mainnet (public RPCs; optional
BASE_RPC_URL / ETH_RPC_URL with failover)
- Tokens: native ETH; USDC, WETH, DAI on both chains; WBTC on Ethereum; cbBTC on Base (honest substitute — Base has no BitGo WBTC)
- USD marks via Coinbase public exchange rates (CoinGecko for the rest or on Coinbase failure); real balances only — if an RPC fails, that network is marked failed in
networks[] / warnings
- Rule-based risk score (0–100) and rule findings (largest asset weight vs 35%/50%, stablecoin share vs 15%/85%, largest chain share vs 85%, low native ETH gas balance; returned in the
suggestions array as plain findings, no instructions) with transparent formulas in methodology
- Price: $0.04 USDC (
40000 atomic)
runtime = 'nodejs', dynamic = 'force-dynamic'
- Unpaid GET → 402 +
PAYMENT-REQUIRED (v2, eip155:8453) · payTo 0x5b32c973596078a967562ca652761404f19be0e9
GET /api/gas
Live gas snapshot for Base and Ethereum from public RPCs.
- Prefers
eth_feeHistory (last 20 blocks, reward percentiles 10/50/90): baseFeeGwei, priorityFeeGwei (p50), suggestedMaxFeeGwei (= 2×baseFee + priority)
- Falls back to
eth_gasPrice when feeHistory is unavailable
- Transparent
timingHint: cheap / normal / expensive from percentile rank of the latest confirmed baseFee within the feeHistory window (<33 / 33–67 / >67); documented in methodology — not a forecast
- Optional ETH USD (Coinbase Exchange, CoinGecko fallback) for a simple 21k-gas transfer cost estimate (
simpleTransfer.costUsd)
- Price: $0.01 USDC (
10000 atomic)
runtime = 'nodejs', dynamic = 'force-dynamic'
- Unpaid GET → 402 +
PAYMENT-REQUIRED (v2, eip155:8453) · payTo 0x5b32c973596078a967562ca652761404f19be0e9
GET /api/funding
Live perpetual funding rates for BTC / ETH / SOL from OKX (same source as /api/signals; Vercel-friendly — not Binance/Bybit).
- Reuses
lib/okx.ts (fetchAllFunding)
- Transparent
methodology on every response (including errors)
- Optional crowding hint from funding sign + magnitude only (rules: quiet / mild / elevated / extreme; side longs/shorts/neutral) — not a forecast
- Honesty: real OKX data only
- Price: $0.01 USDC (
10000 atomic) — cheap poll product for agents
runtime = 'nodejs', dynamic = 'force-dynamic'
- Unpaid GET → 402 +
PAYMENT-REQUIRED (v2, eip155:8453) · payTo 0x5b32c973596078a967562ca652761404f19be0e9
GET /api/fetch
Fetch a public http(s) URL (?url=https://…, required) and return best-effort clean text/markdown.
- SSRF-safe: blocks localhost, private, link-local, and metadata IPs; re-checks each redirect hop
- Caps: ~200KB raw body, 8s timeout, max 3 redirects
- Preferred Content-Types:
text/html, text/plain, application/json, text/markdown
- Cleaning is best-effort (strip script/style/chrome, collapse whitespace) — not a full readability engine
- Price: $0.02 USDC (
20000 atomic)
runtime = 'nodejs', dynamic = 'force-dynamic'
- Unpaid GET → 402 +
PAYMENT-REQUIRED (v2, eip155:8453) · payTo 0x5b32c973596078a967562ca652761404f19be0e9
GET / POST /api/http
Universal agent HTTP proxy — call a public http(s) URL with optional method / headers / body; get back upstream status, filtered headers, and body (text or base64) plus contentType and elapsedMs.
- Price choice: $0.01 USDC (
10000 atomic) for volume. /api/fetch stays $0.02 (specialized clean-text). Prefer /api/http for raw proxy traffic.
- Inputs:
url (required); method (GET default; GET|POST|HEAD|PUT|PATCH|DELETE); optional allowlisted headers (no Cookie / hop-by-hop); optional body for POST/PUT/PATCH (size-capped)
- GET query:
?url=&method=&headers=<json>; POST JSON body for full control including upstream body
- SSRF-safe (shared guards with
/api/fetch): blocks private/link-local/metadata; http/https only; re-checks redirects
- Caps: ~384KB response body, 64KB request body, 12s timeout, max 3 redirects
- Honesty: upstream 4xx/5xx returned in
status when the hop succeeded; proxy failures use honest error codes
runtime = 'nodejs', dynamic = 'force-dynamic'
- Unpaid GET/POST → 402 +
PAYMENT-REQUIRED (v2, eip155:8453) · payTo 0x5b32c973596078a967562ca652761404f19be0e9
- Bazaar discovery advertises GET; POST is paid identically
URL or HTML → structured page fields for agents (title, description, canonical, language, links, images, headings, json-ld, text sample).
- Price choice: $0.015 USDC (
15000 atomic) — between /api/http ($0.01) and /api/fetch ($0.02). Same payTo + Base USDC stack.
- Inputs:
url (optional if html provided) and/or html (size-capped). Prefer fetching url with the same SSRF-safe assertSafePublicUrl as fetch/http.
- GET query:
?url=&fields=<url-encoded json>; POST JSON { url?, html?, fields? } for html payloads
- Optional
fields: up to 20 named CSS selectors, e.g. {"title":"h1","links":{"selector":"a.story","attr":"href","all":true,"limit":5}}. attr is text (default), html, or any attribute (href/src resolved to absolute URLs). Unmatched fields return null plus fieldErrors[name] (no_match, bad_selector, attr_missing, time_budget, document_too_deep, eval_failed). If none match: 422 no_fields_matched, not charged. Hard 2s selector budget; :has() at most once per selector; pages nested >256 levels are refused for fields. Deterministic selectors, no AI.
- Lightweight regex parse (no cheerio). Empty fields omitted. Not a full browser DOM.
- Caps: ~200KB HTML, 8s timeout, max 3 redirects (same band as
/api/fetch)
- No cookie jar. Private/localhost/link-local/metadata blocked.
runtime = 'nodejs', dynamic = 'force-dynamic'
- Unpaid GET/POST → 402 +
PAYMENT-REQUIRED (v2, eip155:8453) · payTo 0x5b32c973596078a967562ca652761404f19be0e9
- Bazaar discovery advertises GET; POST is paid identically
GET /status
Public page showing the live USDC balance of the payTo address on Base (via public RPC / BASE_RPC_URL balanceOf) plus the live paid catalog and agent how-to.
- On RPC failure the page shows an honest error — it does not fall back to a cached balance. Custody note: interim Coinbase-custodial
payTo (not Safe/multisig); upgrade path later.
- First settle noted as
/api/pulse $0.005 (0xedbd1a51…); six crypto routes frozen; /api/fetch + /api/http + /api/extract deliberately LIVE.
Local development
Requires Node.js 20+ (tested on 20.19).
cd horizon-pulse
cp .env.example .env.local
npm install
npm run dev
npm run build && npm start
Environment variables (Vercel / production)
Copy from .env.example:
| Variable | Required | Notes |
|---|
X402_NETWORK | recommended | base |
PAY_TO | optional | defaults to 0x5b32c973596078a967562ca652761404f19be0e9 |
CDP_API_KEY_ID | for settle | Coinbase Developer Platform |
CDP_API_KEY_SECRET | for settle | PKCS8 PEM (store safely; never commit) |
BASE_RPC_URL | optional | overrides default Base RPC for /status + /api/portfolio + /api/gas |
ETH_RPC_URL | optional | overrides default Ethereum RPC for /api/portfolio + /api/gas |
Without CDP keys:
- Unpaid GET and OPTIONS still return correct 402 / discovery with v2
PAYMENT-REQUIRED (payTo, eip155:8453, USDC amount) — no CDP required for discovery.
- Requests that include
PAYMENT-SIGNATURE (or legacy X-PAYMENT) receive 503 until CDP_API_KEY_ID + CDP_API_KEY_SECRET are set on Vercel (settlement path).
- Do not expect live GET to 500 when CDP is missing; that was a prior bug fixed by gating
withX402 behind payment + CDP credentials.
Stack
- Next.js 16 (App Router) + TypeScript
@x402/next + @x402/core + @x402/evm + @x402/extensions
@coinbase/x402 for CDP facilitator auth headers
viem for treasury USDC balanceOf on Base, portfolio balances, and gas feeHistory on Base + Ethereum
Deploy notes
- Create the GitHub repo / remote
https://github.com/horizon-pulse/horizon-pulse.git.
- Set Vercel env vars (above). Prefer PEM secret as a single line with
\n escapes if the UI is single-line.
- Deploy from
main. Ensure functions use Node.js runtime (routes already set export const runtime = 'nodejs').
- Smoke-test:
curl -i https://horizonpulse.dev/api/pulse (or /api/yield, /api/portfolio, /api/gas, /api/funding, /api/fetch?url=https://example.com, /api/http?url=https://example.com, /api/extract?url=https://example.com) should return 402 with payment requirements pointing at the new payTo.
- Confirm
/status shows the treasury balance for 0x5b32…e0e9.
Push (from this workspace)
This scaffold does not push for you (no credentials). From a machine with GitHub auth:
cd /workspace/horizon-pulse
git init
git add .
git commit -m "feat: Horizon Pulse x402 API with new Base treasury"
git branch -M main
git remote add origin https://github.com/horizon-pulse/horizon-pulse.git
git push -u origin main
If the remote already has history:
git remote add origin https://github.com/horizon-pulse/horizon-pulse.git
git fetch origin
git pull origin main --rebase
git push -u origin main
Reference agent (examples/)
Unpaid 402 discovery for the live paid routes listed in the script, plus optional paid single-route when SMOKE_PRIVATE_KEY is set:
cd horizon-pulse
node examples/reference-agent.mjs
SMOKE_PRIVATE_KEY=0x... SMOKE_ROUTE=/api/pulse node examples/reference-agent.mjs
See examples/README.md. Uses @x402/core + @x402/evm from this package. Never commit secrets.
Paid smoke test (/api/pulse)
End-to-end check against production (or SMOKE_BASE_URL): unpaid 402 → sign EIP-3009 / x402 with a local key → paid 200 + on-chain USDC toward payTo.
cd horizon-pulse
npm install
SMOKE_PRIVATE_KEY=0x... node scripts/smoke-pulse.mjs
Optional:
SMOKE_BASE_URL=https://horizonpulse.dev
BASE_RPC_URL=https://mainnet.base.org
Never paste your private key in chat, commits, or screenshots. Export it only in your local shell (e.g. Mac mini). Without SMOKE_PRIVATE_KEY, the script exits with usage help (safe dry-run).
License
Private / as designated by the horizon-pulse org.