@peerlytics/sdk
v4.0.0
Published
Lightweight TypeScript SDK for the Peerlytics v1 API - ZKP2P P2P protocol analytics
Maintainers
Readme
@peerlytics/sdk
TypeScript SDK for the Peerlytics v1 API -- analytics, explorer, and trading data for the ZKP2P P2P protocol on Base. Works in Node.js and browsers.
The API speaks a Stripe-style wire format: snake_case fields, Unix-seconds timestamps, cursor pagination, and dedicated key endpoints. The SDK adapts it back to a camelCase TypeScript surface, so you work in camelCase and never touch the raw wire shape.
Agent bundle
If you are integrating through an agent (Claude Code, Cursor, etc.), start here:
- Developer portal: https://peerlytics.xyz/developers
- Drop-in skill: https://peerlytics.xyz/skills/peerlytics.md
- Short machine reference: https://peerlytics.xyz/llms.txt
- Full machine reference: https://peerlytics.xyz/llms-full.txt
- OpenAPI 3.1 JSON: https://peerlytics.xyz/api/openapi (also discoverable at
/.well-known/openapi.json) - Starters (Next.js / Vite / Telegram bot, plus runnable example scripts and an x402 agent): https://github.com/ADWilkinson/usdctofiat-peerlytics-starters
- Companion SDK for USDC-to-fiat deposits:
@usdctofiat/offramp
The SDK also exports a typed resource map and prompt builder for developer portals, internal tools, and coding agents:
import {
PEERLYTICS_DEVELOPER_RESOURCES,
getPeerlyticsAgentPrompt,
getPeerlyticsDeveloperResources,
} from "@peerlytics/sdk";
PEERLYTICS_DEVELOPER_RESOURCES.authModes; // ["api-key", "x402"]
PEERLYTICS_DEVELOPER_RESOURCES.links.wellKnownOpenApi; // https://peerlytics.xyz/.well-known/openapi.json
PEERLYTICS_DEVELOPER_RESOURCES.upstreamSourceTruths.map((source) => source.label);
// ["Production ZKP2P indexer live GraphQL schema", "@zkp2p/indexer-schema", ...]
const dashboardPlaybook = getPeerlyticsDeveloperResources("dashboard");
const agentPrompt = getPeerlyticsAgentPrompt("market-maker");Use upstreamSourceTruths before generating raw protocol fields, GraphQL
queries, proof payload mappings, or client behavior. It anchors agent work to
the releases/prod GraphQL schema, typed indexer package, Curator API,
zkp2p-clients, and zkp2p/attestation-service.
Install
npm install @peerlytics/sdk
# or
bun add @peerlytics/sdk
# or
pnpm add @peerlytics/sdkQuick Start
import { Peerlytics } from "@peerlytics/sdk";
const client = new Peerlytics({ apiKey: "pk_live_..." });
// protocol summary
const summary = await client.getProtocolSummary();
// protocol health view
const overview = await client.getProtocolOverview("all");
// live orderbook
const orderbook = await client.getOrderbook({ currency: "GBP" });
// executable route plan
const plan = await client.planRoutes({ currency: "GBP", amountUsd: 500 });
const best = plan.routes[0];
if (best?.executionMode === "single" && best.actions.take) {
// The link preserves the planned amount and exact deposit/payment pair.
console.log(best.actions.take);
} else if (best?.executionMode === "multi_intent") {
// Each leg is a separate intent; the current buy sheet is not atomic.
console.log(best.allocation.legs);
}
// deposit detail
const deposit = await client.getDeposit("8453_0x777...Ef_42");
// search (supports addresses, tx hashes, deposit IDs, ENS, .peer names)
const results = await client.search("vitalik.eth");x402 pay-per-request
Agents can skip API-key provisioning and pay each request directly with USDC on
Base. Pass a viem signer and the SDK handles the 402 Payment Required
challenge, creates the payment payload, retries with PAYMENT-SIGNATURE, and
keeps the normal camelCase response shape.
import { Peerlytics } from "@peerlytics/sdk";
import { privateKeyToAccount } from "viem/accounts";
const client = new Peerlytics({
auth: {
mode: "x402",
signer: privateKeyToAccount(process.env.AGENT_PRIVATE_KEY as `0x${string}`),
onPaymentSettled: (settlement) => {
console.log("paid", settlement.transaction);
},
},
});
const orderbook = await client.getOrderbook({ platform: "venmo" });
const summary = await client.getProtocolSummary({ range: "mtd" });For custom agent runtimes, provide paymentHandler instead of signer and
return the paid retry headers yourself:
const client = new Peerlytics({
auth: {
mode: "x402",
paymentHandler: async ({ paymentRequired }) => ({
"PAYMENT-SIGNATURE": await signWithYourAgentWallet(paymentRequired),
}),
},
});Response shapes
A few gotchas worth knowing up front — especially if you've been hitting the HTTP API directly:
{ data, ... }envelope. Every v1 endpoint wraps the payload in{ data: <payload>, ... }(siblings includemeta,linked, etc.). The SDK unwraps this for you — every method returns the innerdatadirectly. There is nosuccessflag — branch on HTTP status.getDepositsaccepts empty filter sets. Unfiltered calls return a bounded page (defaultlimit50, hard-capped at 200). Optional filters:depositor,delegate,platform,currency, or a date window (from,to, orrange).getIntentsaccepts empty filter sets. Same page cap. Optional filters:taker,recipient,verifier,depositId,status, or a date window (from,to, orrange). Theowneralias is also accepted.getActivityreturns an envelope, not a raw array. The response is{ events, count, hasMore, limit, offset, nextCursor, filters }— iterate overresponse.events, notresponseitself. WhenhasMore=true, pass the returnednextCursorback ascursorto walk forward without offset drift.Currency codes vs hashes. On-chain, currencies are stored as
bytes32(either a keccak256 of the ISO code or an ASCII-padded encoding). EveryDepositMarketexposes both:currency(resolved, e.g."GBP") andcurrencyCode(raw hash). Entries insidedeposit.currencies[]also carry a resolvedcurrencyfield alongside the rawcurrencyCode. If you need to build your own mapping, callgetCurrencies()— each entry includes thecode,label,flag, and all hash forms.
Date filtering
Every analytics, listing, and history endpoint accepts a uniform set of
date-window parameters. Either pass from/to or a range shortcut:
// April 2026 leaderboard
await client.getLeaderboard({ from: "2026-04-01", to: "2026-05-01" });
// last 30 days
await client.getLeaderboard({ range: "last_30d" });
// month-to-date with prior-period comparison block
await client.getProtocolSummary({ range: "mtd", compare: "prior_period" });
// LP retro for a single maker, paginated across the window
await client.getMakerHistory("0xMaker...", {
range: "last_90d",
limit: 50,
offset: 0,
});
// Cash-App-only daily volume series
await client.getTimeseries({
entity: "volume",
groupBy: "platform",
platform: "cashapp",
granularity: "day",
from: "2026-04-01",
to: "2026-05-01",
});
// Vault rollup for the launch week
await client.getVaultsOverview({ from: "2026-04-22", to: "2026-04-29" });from and to accept ISO-8601 strings (2026-04-01T00:00:00Z) or unix-seconds
(numeric). Supported range shortcuts: last_7d, last_30d, last_90d,
last_365d, today, yesterday, mtd, qtd, ytd, all. Hard cap: 400 days.
Windowed responses include a window block — { from, to, fromIso, toIso, days,
range, computedFor } — so you can render the resolved bounds verbatim. The
cumulative path is the default; passing any window param opts you into a
live indexer compute that costs more credits but never returns stale data.
For activity backfills, prefer the cursor over offset:
let cursor: string | null = null;
do {
const page = await client.getActivity({
range: "last_30d",
limit: 200,
cursor: cursor ?? undefined,
});
for (const event of page.events) handle(event);
cursor = page.nextCursor;
} while (cursor);Configuration
const client = new Peerlytics({
baseUrl: "https://peerlytics.xyz", // default
apiKey: "pk_live_...", // API key for authenticated access
headers: { "X-Trace": "abc" }, // custom headers
fetch: customFetch, // custom fetch implementation
});apiKey is kept for backwards compatibility. New code can use
auth: { mode: "api-key", apiKey: "pk_live_..." } or
auth: { mode: "x402", signer }.
API Reference
Analytics
| Method | Description |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| getProtocolSummary(params?) | Cumulative summary by default; pass from/to/range/compare for a windowed payload with optional period delta block. |
| getProtocolOverview(rangeOrOpts) | Legacy TimeRange enum reads cached buckets; OverviewParams (from/to or non-enum range shortcut) computes live. |
| getLeaderboard(params?) | Maker and taker leaderboards. Add from/to/range to recompute every aggregate from intents inside the window. |
| getTimeseries(params) | Hour or day buckets. Pass groupBy=platform\|currency\|maker\|verifier for multi-series + dimension filters. |
| getVaultsOverview(params?) | Cumulative vault overview by default; pass from/to/range for per-vault rollup with feesEarnedUsd, aumChangeUsd. |
Deposits
| Method | Description |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| getDeposits(filters?) | Query deposits. Empty calls return a bounded page (default 50, max 200). Optional filters: depositor, delegate, platform, currency, date window (from/to/range), status. sort=asc\|desc. |
| getDeposit(id, params?) | Deposit detail with intents, payment methods, and linked data |
Intents
| Method | Description |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| getIntents(filters?) | Query intents. Empty calls return a bounded page (default 50, max 200). Optional filters: taker, recipient, verifier, depositId, status, date window (from/to/range). owner is also accepted as an alias. sort=asc\|desc. |
| getIntent(hash) | Intent detail with deposit and related intents |
Explorer
| Method | Description |
| ---------------------------------------- | ------------------------------------------------------------------- |
| getAddress(address, params?) | Address profile: intents, deposits, activity, stats |
| getMaker(address) | Maker portfolio: deposits, allocations, profit, APR |
| getTaker(address) | Taker portfolio: fills, cancelled volume, and currency mix |
| getIntegrator(code, opts?) | ERC-8021 integrator rollup: volume, makers, top markets |
| getIntegratorIntents(code, opts?) | Recent intents attributed to an ERC-8021 integrator (up to 20) |
| getIntegratorReferralFees(code, opts?) | Recent treasury referral fees for an ERC-8021 integrator (up to 20) |
| getPlatform(platform, opts?) | Platform rollup: currencies, makers, takers, recent intents |
| getDelegate(address) | Delegate rollup: rate managers, delegated deposits, PnL |
| getVerifier(address, params?) | Verifier stats: intents, breakdown by currency/taker/maker |
| search(query, opts?) | Multi-type search (address, tx hash, deposit ID, ENS, .peer name) |
Recent integrator intents are available two ways:
const intents = await client.getIntegratorIntents("galleonlabs");
const integrator = await client.getIntegrator("galleonlabs");
const recentIntents = integrator.recentIntents ?? [];Use IntegratorData.recentIntents when you already need the full integrator rollup and want a single network call.
Recent referral fees (0.50% treasury take) follow the same pattern:
const fees = await client.getIntegratorReferralFees("galleonlabs");
const integrator = await client.getIntegrator("galleonlabs");
const recentFees = integrator.recentReferralFees ?? [];Each explorer entity also has a canonical URL at peerlytics.xyz/explorer/<entity>/<slug> — the SDK method and the page are siblings.
Orderbook & Market
| Method | Description |
| ------------------------- | ---------------------------------------------------------- |
| planRoutes(opts?) | Deterministic single, multi-intent, or partial allocations |
| getOrderbook(opts?) | Live orderbook with exact per-deposit bounds and pairs |
| getMarketSummary(opts?) | Rate statistics per (platform, currency) pair |
The orderbook API prefers the indexer's denormalized OrderbookEntry
projection when available, which keeps platform filters canonicalized (for
example, zelle-* variants collapse into one Zelle surface). Each level
retains every deposit's own available amount, single-intent range, and exact
payment pairs. Every pair carries isPublic, disputeProtectionOptedOut, and
disputeProtectionRequiresStake. The opt-out field records an explicit exit
from default-on protection; a non-opted-out pair remains public when it requires
a taker to prepare stake before signalling. planRoutes uses those facts to
produce allocation legs; only a currently valid public single leg exposes
actions.take. Multi-intent routes must be submitted as separate intents and
partial routes report their unallocated remainder. Without amountUsd,
executionMode is null and no route is counted as a successful fill. The
default book is public liquidity only; pass taker to inspect restricted
deposit/payment-method tuples available to a specific buyer wallet. The
response does not expose why access was granted.
getDeposit() and getIntent() expose the same canonical protection pair on
linked.quoteCandidates. Use those fields as route metadata only: the
transaction client must refresh authoritative access, protection, and stake
state before signing or funding. getVerifier() also returns proof and
linkedDeposits, whose restricted-access fields are provenance-free.
Vaults & Delegation
| Method | Description |
| ----------------------- | ------------------------------------------------------------- |
| getVaultsOverview() | All vaults: AUM, fees, adoption rate, daily snapshots |
| getVault(id, params?) | Vault detail: rate manager, delegations, oracle/floor configs |
Activity
| Method | Description |
| --------------------------------- | ------------------------------------------------------------------ |
| getActivity(filters?) | Live blockchain events (signals, fulfills, deposits, rate updates) |
| streamActivity(filters?, opts?) | SSE stream of the same events (requires api key, no x402) |
Time-series
| Method | Description |
| --------------------- | ---------------------------------------------------------------- |
| getTimeseries(opts) | Bucketed volume / deposits / intents by hour or day (Pro). |
History
| Method | Description |
| -------------------------- | --------------------------------------------------------------------- |
| getMakerHistory(address) | Maker historical stats, platform/currency breakdowns, recent activity |
| getTakerHistory(address) | Taker historical stats, cancelled volume, and recent activity |
Metadata
| Method | Description |
| ----------------- | ----------------------------------------------------------- |
| getCurrencies() | Supported fiat currencies with codes, labels, flags, hashes |
| getPlatforms() | Supported payment platforms with IDs, labels, method hashes |
Account Management
| Method | Description |
| --------------------- | ------------------------------------------------------------- |
| listKeys() | List API keys (each carries a stable opaque id) |
| createKey(label?) | Create a new API key (POST /account/keys) |
| rotateKey(id) | Rotate by opaque id (POST /account/keys/{id}/rotate) |
| deleteKey(id) | Delete by opaque id (DELETE /account/keys/{id}) |
| getCredits() | Credit balance and purchase history |
| createCheckout(pkg) | Create a credit checkout order (starter, growth, scale) |
Keys & timestamps
rotateKey(...)anddeleteKey(...)take the opaqueid(sha256 of the key, exposed on everyApiKeyInfo), not the raw key. Full secrets are returned only fromcreateKey()androtateKey(), never fromlistKeys().- Timestamp fields (
createdAt,lastUsedAt,freeCreditsResetAt) are typednumber | string— the server emits Unix seconds (integer); the compatibility path emits ISO strings. Always coerce explicitly if you touch the values. freeCreditsResetAtis expressed in seconds.
Version and release history live in package.json and CHANGELOG.md.
Error Handling
import {
PeerlyticsError,
RateLimitError,
NotFoundError,
ValidationError,
InsufficientCreditsError,
} from "@peerlytics/sdk";
try {
await client.getDeposit("missing");
} catch (err) {
if (err instanceof RateLimitError) {
console.log(`Retry after ${err.retryAfter}s`);
} else if (err instanceof NotFoundError) {
console.log("Not found");
} else if (err instanceof ValidationError) {
console.log(`Bad request: ${err.code} - ${err.message}`);
} else if (err instanceof InsufficientCreditsError) {
// Out of credits — send the developer to a one-click top-up.
console.log(`Buy more: ${err.checkoutUrl}`);
for (const pkg of err.packages) {
console.log(` ${pkg.id}: ${pkg.credits} credits for $${pkg.priceUsd}`);
}
} else if (err instanceof PeerlyticsError) {
console.log(`HTTP ${err.status}: ${err.message}`);
}
}All errors extend PeerlyticsError with status, code, message, and details properties. InsufficientCreditsError (a 402) additionally carries checkoutUrl and packages parsed from the error body.
Credit & rate-limit telemetry
Every request stamps the rate-limit and credit budget onto response headers. The
SDK captures the latest snapshot so you can read it without touching the raw
Response:
await client.getOrderbook({ currency: "GBP" });
const credits = client.getLastCredits();
// { remaining, source: "free" | "paid" | "none", cost, warning, buyUrl, paymentMethod }
if (credits?.warning === "free_tier_near_limit") {
console.log(`Running low — top up at ${credits.buyUrl}`);
}
const rate = client.getLastRateLimit();
// { limit, remaining, resetInSeconds }
if (rate && rate.remaining !== null && rate.remaining < 5) {
console.log(`Throttle: ${rate.remaining} left, resets in ${rate.resetInSeconds}s`);
}Both accessors return null until a request surfaces the relevant headers, and
individual fields are null when a specific header is absent. Credit fields are
populated on the api-key path; x402 requests report paymentMethod: "x402" with
no balance. Reacting to getLastCredits().warning lets you nudge a top-up
before the hard 402 that throws InsufficientCreditsError.
Filter Arrays
Array filter values are comma-joined automatically:
// deposits on revolut or wise, in GBP or EUR
await client.getDeposits({
platform: ["revolut", "wise"],
currency: ["GBP", "EUR"],
});
// activity for multiple deposit IDs
await client.getActivity({
depositId: ["42", "43", "44"],
type: ["intent_signaled", "intent_fulfilled"],
});Related
- Dashboard: https://peerlytics.xyz
- Developer docs: https://peerlytics.xyz/developers
- Agent docs: https://peerlytics.xyz/developers?tab=agents
- LLM docs: https://peerlytics.xyz/llms.txt
- Trading: https://usdctofiat.xyz
License
MIT
