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
| 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.
- Agents hit fourteen live paid HTTP endpoints (six crypto frozen;
/api/fetch+/api/http+/api/extract+/api/x402-check+/api/bazaar-check+/api/screenshot+/api/search+/api/pdfnon-crypto LIVE — see below). - Unpaid requests receive HTTP 402 with x402 v2 requirements: canonical wire is the
PAYMENT-REQUIREDheader, with oneacceptsentry per network: Base (CAIP-2eip155:8453, Base USDC, the treasury below) and Solana (CAIP-2solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp, USDC mint, the Solana payTo below). Seedocs/solana-rail.md. - Retry with an x402 v2
PAYMENT-SIGNATUREheader. 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
balanceOffor portfolio (real balances only);eth_feeHistory/eth_gasPricefor gas (real fees only);/api/fetchreturns best-effort cleaned text from a requested public URL (SSRF-safe, size/time capped);/api/httpis a raw universal proxy (filtered headers, text|base64 body, SSRF-safe) priced $0.01 for volume;/api/extractreturns 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 —
/statusreads live USDCbalanceOfonpayTo. Honest: currentpayTois interim Coinbase-custodial (not Safe/multisig).
| Fact | Value |
|---|---|
| Live paid routes | 14 (table below) — six crypto + /api/fetch + /api/http + /api/extract + /api/x402-check + /api/bazaar-check + /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.
GETa paid route with no payment → HTTP 402.- Read
PAYMENT-REQUIRED(v2) and pick oneacceptsentry. Confirmnetwork(eip155:8453for Base orsolana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpfor Solana), USDC on that network, exact atomicamount, and that network'spayTo. - Sign and retry the same
GETwithPAYMENT-SIGNATURE(v2 primary; do not rely on legacyX-PAYMENTas 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.
- Skill file:
/skill.md, plain markdown an agent can follow end to end (generated frompublic/openapi.jsonbylib/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.
| 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.
Paid (14 — six crypto frozen + fetch + http + extract + x402-check + bazaar-check + 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/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 |
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: truein payment requirements
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
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
outlierpools - Preference: keep stablecoin and/or single-asset (
exposure === "single") pools - Transparent ranking:
preferenceTierDESC (2 = stablecoin+single, 1 = either), then APY DESC, then TVL DESC - Every response includes a
methodologyobject (also on error bodies) - Price: $0.02 USDC (
20000atomic) runtime = 'nodejs',dynamic = 'force-dynamic'- Unpaid GET → 402 +
PAYMENT-REQUIRED(v2,eip155:8453) ·payTo0x5b32c973596078a967562ca652761404f19be0e9
On-chain portfolio snapshot for one EVM address (?address=0x…, required).
- Networks: Base + Ethereum mainnet (public RPCs; optional
BASE_RPC_URL/ETH_RPC_URLwith 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
suggestionsarray as plain findings, no instructions) with transparent formulas inmethodology - Price: $0.04 USDC (
40000atomic) runtime = 'nodejs',dynamic = 'force-dynamic'- Unpaid GET → 402 +
PAYMENT-REQUIRED(v2,eip155:8453) ·payTo0x5b32c973596078a967562ca652761404f19be0e9
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_gasPricewhen 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 inmethodology— not a forecast - Optional ETH USD (Coinbase Exchange, CoinGecko fallback) for a simple 21k-gas transfer cost estimate (
simpleTransfer.costUsd) - Price: $0.01 USDC (
10000atomic) runtime = 'nodejs',dynamic = 'force-dynamic'- Unpaid GET → 402 +
PAYMENT-REQUIRED(v2,eip155:8453) ·payTo0x5b32c973596078a967562ca652761404f19be0e9
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
methodologyon 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 (
10000atomic) — cheap poll product for agents runtime = 'nodejs',dynamic = 'force-dynamic'- Unpaid GET → 402 +
PAYMENT-REQUIRED(v2,eip155:8453) ·payTo0x5b32c973596078a967562ca652761404f19be0e9
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 (
20000atomic) runtime = 'nodejs',dynamic = 'force-dynamic'- Unpaid GET → 402 +
PAYMENT-REQUIRED(v2,eip155:8453) ·payTo0x5b32c973596078a967562ca652761404f19be0e9
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 (
10000atomic) for volume./api/fetchstays $0.02 (specialized clean-text). Prefer/api/httpfor raw proxy traffic. - Inputs:
url(required);method(GETdefault;GET|POST|HEAD|PUT|PATCH|DELETE); optional allowlistedheaders(no Cookie / hop-by-hop); optionalbodyfor 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
statuswhen the hop succeeded; proxy failures use honest error codes runtime = 'nodejs',dynamic = 'force-dynamic'- Unpaid GET/POST → 402 +
PAYMENT-REQUIRED(v2,eip155:8453) ·payTo0x5b32c973596078a967562ca652761404f19be0e9 - 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 (
15000atomic) — between/api/http($0.01) and/api/fetch($0.02). Same payTo + Base USDC stack. - Inputs:
url(optional ifhtmlprovided) and/orhtml(size-capped). Prefer fetchingurlwith the same SSRF-safeassertSafePublicUrlas 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}}.attristext(default),html, or any attribute (href/src resolved to absolute URLs). Unmatched fields returnnullplusfieldErrors[name](no_match,bad_selector,attr_missing,time_budget,document_too_deep,eval_failed). If none match: 422no_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) ·payTo0x5b32c973596078a967562ca652761404f19be0e9 - Bazaar discovery advertises GET; POST is paid identically
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/extractdeliberately LIVE.
Requires Node.js 20+ (tested on 20.19).
cd horizon-pulse
cp .env.example .env.local
# fill CDP_API_KEY_ID + CDP_API_KEY_SECRET for verify/settle
npm install
npm run devnpm run build && npm startCopy 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 legacyX-PAYMENT) receive 503 untilCDP_API_KEY_ID+CDP_API_KEY_SECRETare set on Vercel (settlement path). - Do not expect live GET to 500 when CDP is missing; that was a prior bug fixed by gating
withX402behind payment + CDP credentials.
- Next.js 16 (App Router) + TypeScript
@x402/next+@x402/core+@x402/evm+@x402/extensions@coinbase/x402for CDP facilitator auth headersviemfor treasury USDCbalanceOfon Base, portfolio balances, and gas feeHistory on Base + Ethereum
- 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
\nescapes if the UI is single-line. - Deploy from
main. Ensure functions use Node.js runtime (routes already setexport 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 newpayTo. - Confirm
/statusshows the treasury balance for0x5b32…e0e9.
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 mainIf 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 # or merge, as appropriate
git push -u origin mainUnpaid 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
# optional paid:
SMOKE_PRIVATE_KEY=0x... SMOKE_ROUTE=/api/pulse node examples/reference-agent.mjsSee examples/README.md. Uses @x402/core + @x402/evm from this package. Never commit secrets.
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 # if needed; uses existing @x402/* + viem
SMOKE_PRIVATE_KEY=0x... node scripts/smoke-pulse.mjs
# or: npm run smoke:pulseOptional:
SMOKE_BASE_URL=https://horizonpulse.dev
# backup: https://horizon-pulse-seven.vercel.app
BASE_RPC_URL=https://mainnet.base.orgNever 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).
Private / as designated by the horizon-pulse org.
