Skip to content

TypeScript SDK

Cerebrus Pulse takes payment over x402 v2. An unpaid request gets 402 Payment Required with the terms in the base64 PAYMENT-REQUIRED header; the client pays by retrying with a PAYMENT-SIGNATURE header and gets the data plus a PAYMENT-RESPONSE receipt. The v2 fetch client, @x402/fetch, does all of that for you.

Terminal window
npm install @x402/fetch @x402/evm viem

The examples were checked against @x402/fetch, @x402/evm and @x402/svm 2.27.0. They are ES modules (top-level await), for Node.js 18 or later.

wrapFetchWithPaymentFromConfig wraps the native fetch. It makes the request, reads the 402 terms, signs a USDC payment on a network you registered, and retries. The rest of your code just calls fetchWithPayment.

import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm";
import { privateKeyToAccount } from "viem/accounts";
// A dedicated, low-balance wallet. Read the key from the environment, never from source.
const account = privateKeyToAccount(process.env.BASE_WALLET_PRIVATE_KEY as `0x${string}`);
export const fetchWithPayment = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [{ network: "eip155:8453", client: new ExactEvmScheme(account) }], // Base mainnet
spendControls: { maxAmountPerPayment: "$0.10" }, // refuse any single payment above $0.10
});
export const BASE_URL = "https://api.cerebruspulse.xyz";

Register both schemes to let the client pay on whichever rail the 402 offers first (Base is listed first).

// Pulse — multi-timeframe technical analysis
export async function getPulse(coin: string, timeframes = "1h,4h") {
const resp = await fetchWithPayment(`${BASE_URL}/pulse/${coin}?timeframes=${timeframes}`);
return resp.json();
}
// Sentiment — a bucketed market sentiment label
export async function getSentiment() {
const resp = await fetchWithPayment(`${BASE_URL}/sentiment`);
return resp.json();
}
// Funding — funding rate statistics over 1-168 hours
export async function getFunding(coin: string, lookbackHours = 24) {
const resp = await fetchWithPayment(`${BASE_URL}/funding/${coin}?lookback_hours=${lookbackHours}`);
return resp.json();
}
// Bundle — /pulse, 24h /funding and /sentiment in one request
export async function getBundle(coin: string, timeframes = "1h,4h") {
const resp = await fetchWithPayment(`${BASE_URL}/bundle/${coin}?timeframes=${timeframes}`);
return resp.json();
}
// Free endpoints — no payment needed, use plain fetch
export async function getCoins(): Promise<{ coins: string[] }> {
const resp = await fetch(`${BASE_URL}/coins`);
return resp.json();
}

A paid response carries the settlement receipt in the PAYMENT-RESPONSE header:

import { decodePaymentResponseHeader } from "@x402/fetch";
const resp = await fetchWithPayment(`${BASE_URL}/pulse/BTC`);
const receipt = resp.headers.get("PAYMENT-RESPONSE");
if (receipt) console.log(decodePaymentResponseHeader(receipt)); // transaction, network, payer

The shapes below follow what the engine returns; see GET /pulse for every field.

type TrendLabel = "bullish" | "weak_bullish" | "neutral" | "weak_bearish" | "bearish" | "insufficient_data";
interface PulseResponse {
coin: string;
market_symbol: string; // Hyperliquid's name for the market (e.g. "kPEPE")
price_unit: string; // prices are per this many tokens (e.g. "1000 PEPE")
price: { current?: number };
timeframes: Record<string, {
indicators: {
rsi_14: number | null;
rsi_zone: string;
trend: { direction: -1 | 0 | 1; label: TrendLabel; ema_stack: string };
bollinger: { position_pct?: number; width_pct?: number };
zscore_100: number | null;
};
last_closed_candle_time?: string;
stale?: boolean;
} | { error: string }>;
confluence: {
score: number; // bullish vote share on the primary timeframe: 1 = all bullish, 0.5 = neutral, 0 = all bearish
bias: "bullish" | "bearish" | "neutral" | "unknown";
primary_timeframe: string | null;
};
regime: { current: string; as_of: number; stale?: boolean };
derivatives: {
funding_rate?: number; // hourly rate
funding_annualized_pct?: number;
open_interest?: number; // in contracts (price_unit), not USD
};
meta: { execution_ms: number; warning?: string; warnings?: string[] };
}
async function analyzeMarket(coin: string) {
const data: PulseResponse = await getPulse(coin);
console.log(`${coin} @ $${data.price.current?.toLocaleString()} | regime: ${data.regime.current}`);
console.log(`Confluence: ${data.confluence.bias} (bullish share ${data.confluence.score})`);
for (const [tf, analysis] of Object.entries(data.timeframes)) {
if ("error" in analysis) {
console.log(` [${tf}] ${analysis.error}`);
continue;
}
const { rsi_14, rsi_zone, trend } = analysis.indicators;
console.log(` [${tf}] RSI: ${rsi_14 ?? "n/a"} (${rsi_zone}) | Trend: ${trend.label}`);
}
if (data.meta.warning || data.meta.warnings?.length) {
console.warn("Data warnings:", data.meta.warning, data.meta.warnings);
}
}
async function safePulse(coin: string) {
try {
const resp = await fetchWithPayment(`${BASE_URL}/pulse/${coin}`);
if (!resp.ok) {
switch (resp.status) {
case 400: console.error("Invalid input, e.g. a bad timeframe (not charged)"); break;
case 402: console.error("Payment not accepted, e.g. not enough USDC (nothing settled)"); break;
case 404: console.error(`Unknown coin: ${coin} (GET /coins lists them)`); break;
case 429: console.error("Rate limited: wait and retry"); break;
case 503: console.error(`Unavailable; retry after ${resp.headers.get("Retry-After")} s`); break;
case 504: console.error("Engine timeout (not charged): try again"); break;
default: console.error(`Error ${resp.status}`);
}
return null;
}
return resp.json();
} catch (err) {
// Also thrown when a payment would exceed spendControls
console.error("Request failed:", err);
return null;
}
}
async function dashboard() {
const { coins } = await getCoins(); // free
console.log(`Scanning ${coins.length} coins...\n`);
// Paid: five /pulse calls. One /screener call covers every coin instead.
const results = await Promise.all(
coins.slice(0, 5).map(async (coin) => {
const data = await safePulse(coin);
return data ? { coin, ...data.confluence } : null;
}),
);
// Highest bullish vote share first (1 = all bullish, 0 = all bearish)
const sorted = results
.filter((r): r is NonNullable<typeof r> => r !== null)
.sort((a, b) => b.score - a.score);
console.log("Coin Bias Bullish share");
for (const r of sorted) console.log(`${r.coin.padEnd(9)}${r.bias.padEnd(11)}${r.score}`);
}
  • Free endpoints (/health, /coins, /demo/{coin}) don’t need fetchWithPayment; use regular fetch
  • Promise.all batches requests, but the gateway allows 3 concurrent requests and 12 per minute per IP
  • Check /health before a batch to avoid paying into a degraded gateway
  • meta.warning (stale_data) and meta.warnings (for example stale_candles) flag old inputs
  • /bundle returns /pulse, 24h /funding and /sentiment for one coin in one request (one rate-limit slot instead of three); its page shows how its price compares