Developer Tools
Four real ways to pull Bridge Radar's data into your own app: the TypeScript SDK, the REST/WebSocket API directly, a drop-in embeddable badge, or reading the on-chain oracle yourself. Every URL below points at the real, live API (https://bridge-radar-api-3dbs.onrender.com), not a placeholder domain — deployed on Render's free tier, so expect an occasional cold-start delay after a period of inactivity.
SDK — @bridge-radar/sdk
Minimal TypeScript client — real REST fetch or read the on-chain oracle directly. Its only real dependency is @solana/web3.js.
@bridge-radar/sdk@0.2.0, public, zero unresolvable dependencies. Verify yourself: npmjs.com/package/@bridge-radar/sdk.Install
npm install @bridge-radar/sdkgetBridgeHealth() takes the API URL as an explicit argument rather than baking in a default — every example below points at https://bridge-radar-api-3dbs.onrender.com, our real live instance. Point it at your own instance instead if you're self-hosting.
getBridgeHealth() — real GET /v1/bridges/:id under the hood
import { getBridgeHealth } from "@bridge-radar/sdk";
const { bridge, health, defillama } = await getBridgeHealth("wormhole", "https://bridge-radar-api-3dbs.onrender.com");
console.log(health?.score); // 0-100, or undefined if never scored
console.log(health?.components); // { parity_severity, outflow_severity, signer_recency, frontend_recency, oracle_staleness }
console.log(defillama?.tvl_usd); // real cross-referenced TVL, if DeFiLlama has itgetBridgeHealthOnChain() — reads the real Anchor PDA directly, no API involved
import { Connection } from "@solana/web3.js";
import { getBridgeHealthOnChain } from "@bridge-radar/sdk";
const connection = new Connection("https://api.devnet.solana.com");
const score = await getBridgeHealthOnChain(connection, "wormhole");
// throws BridgeRadarError if "wormhole" was never registered on-chain --
// never silently returns 0 for "doesn't exist"getFinalityHealth() — real GET /v1/network/finality under the hood
@bridge-radar/sdk@0.2.0 on npm. No source-only caveat anymore: npm install @bridge-radar/sdk gets you this export directly.import { getFinalityHealth } from "@bridge-radar/sdk";
const health = await getFinalityHealth("https://bridge-radar-api-3dbs.onrender.com");
console.log(health.latest?.elapsedMs); // real ms between this slot's confirmed and finalized observation
console.log(health.rollingBaselineMs); // real trailing-hour median, or null if under 5 real samples
console.log(health.isAnomalous); // true only when latest exceeds the real baseline by 3xEvery other real export
Every remaining export — 3 values plus 7 types, nothing hidden beyond what's already shown above:
bandOf(score: number): HealthBand
// "green" if score >= 80, "yellow" if score >= 50, else "red".
// (Note: this is the raw threshold function -- it doesn't know about
// "unmonitored". A disabled/unscored bridge is a band your own code
// derives from health being undefined, same as bandFor() does server-side.)
RADAR_ORACLE_PROGRAM_ID: PublicKey
// = 6148M4aXYbDsscWn14zCazPy9V4fQFGozdDQp4LFmqHM (Devnet). The default
// programId getBridgeHealthOnChain() uses if you don't pass your own.
class BridgeRadarError extends Error {}
// Thrown by both getBridgeHealth() (non-2xx response) and
// getBridgeHealthOnChain() (account missing, or shorter than the real
// 82-byte layout) -- catch this specifically to distinguish "Bridge Radar
// told us something's wrong" from a generic network/RPC failure.
// Types (all plain data, re-exported for your own function signatures):
interface BridgeHealth { bridge: BridgeRow; health?: HealthScore; defillama?: DefiLlamaProtocolTvl }
interface HealthScore { bridge_id: string; computed_at: string; score: number; components: HealthComponents }
interface HealthComponents { parity_severity: number; outflow_severity: number; signer_recency: number; frontend_recency: number; oracle_staleness: number }
type HealthBand = "green" | "yellow" | "red" | "unmonitored";
interface BridgeRow { id: string; display_name: string; homepage?: string; enabled: boolean }
interface DefiLlamaProtocolTvl { source: "defillama"; fetched_at: string; defillama_slug: string; defillama_name: string; category: string | null; tvl_usd: number }
interface FinalityHealth { latest: { slot: number; confirmedAt: string; finalizedAt: string; elapsedMs: number } | null; rollingBaselineMs: number | null; sampleCount: number; windowStart: string; isAnomalous: boolean; anomalousBridgeEventsLastHour: number; note: string }Connection subclass that wraps this SDK's getBridgeHealth, published separately since it's a different concern (RPC reliability, not bridge data).Real source: packages/sdk/src/index.ts
REST + WebSocket API
No API key, no rate limiting currently enforced in the code — every route below is a real, public GET. (That will very likely change as this deployment matures; this page reflects the code as it stands.)
/v1/bridgesEvery monitored bridge with its latest real health score and DeFiLlama TVL cross-reference. No query parameters -- always returns all 16 real bridges (14 implemented + 2 planned), no pagination. Planned bridges (no adapter yet) come back with enabled:false and no health key at all -- that's how to tell them apart from a real 0/unscored implemented bridge.
curl -s https://bridge-radar-api-3dbs.onrender.com/v1/bridges{
"scoring": { "algorithm": "v1-mixed", "weights": { "parity": 40, "outflow": 25, "signer": 15, "frontend": 10, "oracle": 10 }, "description": "outflow_severity = z-score over a rolling 30-day distribution ... (full text in /v1/bridges/:id/health)" },
"bridges": [
{ "id": "wormhole", "display_name": "Wormhole", "homepage": "https://wormhole.com", "enabled": true,
"health": { "bridge_id": "wormhole", "computed_at": "2026-08-08T19:30:05.176Z", "score": 90, "components": { "parity_severity": 0, "outflow_severity": 0, "signer_recency": 0, "frontend_recency": 0.989213, "oracle_staleness": 0 } } },
{ "id": "axelar", "display_name": "Axelar", "homepage": "https://axelar.network", "enabled": true,
"health": { "bridge_id": "axelar", "computed_at": "2026-08-08T19:30:05.176Z", "score": 60, "components": { "...": "..." } } },
{ "id": "hyperlane", "display_name": "Hyperlane", "homepage": "https://hyperlane.xyz", "enabled": false }
/* real, live, all 16: across, allbridge, atomiq, axelar, base-solana-bridge, cctp*, debridge,
garden, hyperlane*, layerzero, mayan, orderly, portal, relay, rhinofi, wormhole
(* = planned, no health key). Current real scores: 100 (across, allbridge, atomiq,
base-solana-bridge, garden, orderly, relay, rhinofi), 90 (portal, wormhole),
60 (axelar, debridge, layerzero, mayan) -- verify live, these change. */
]
}/v1/bridges/:idOne bridge's real health score plus its real DeFiLlama TVL. No query parameters. 404 with {"error":"bridge not found"} for an id not in the registry at all -- distinct from a real, registered bridge that just has no health/defillama key yet (200, those fields simply absent).
curl -s https://bridge-radar-api-3dbs.onrender.com/v1/bridges/wormhole{
"bridge": { "id": "wormhole", "display_name": "Wormhole", "homepage": "https://wormhole.com", "enabled": true },
"health": {
"bridge_id": "wormhole",
"computed_at": "2026-08-08T19:30:05.176Z",
"score": 90,
"components": { "parity_severity": 0, "outflow_severity": 0, "signer_recency": 0, "frontend_recency": 0.989213, "oracle_staleness": 0 }
},
"defillama": { "source": "defillama", "fetched_at": "2026-08-08T19:31:21.883Z", "defillama_slug": "portal", "defillama_name": "Portal", "category": "Bridge", "tvl_usd": 1448828504.72 }
}/v1/bridges/:id/healthJust the scoring metadata + one bridge's HealthScore, flattened (not nested under a "health" key like /v1/bridges/:id -- a real, easy-to-miss shape difference). 404 with {"error":"no score yet"} for both an unscored bridge AND an id that doesn't exist at all -- unlike /v1/bridges/:id, this route can't tell those two cases apart, since it only ever looks in the scores table, never the bridge registry.
curl -s https://bridge-radar-api-3dbs.onrender.com/v1/bridges/wormhole/health{
"scoring": { "algorithm": "v1-mixed", "description": "outflow_severity = z-score over a rolling 30-day distribution of 5-min bucket counts (z=4 -> severity 1.0); falls back to clamp(events_per_5min / 10, 0, 1) for the first ~4 hours of observations. parity_severity = 1 - min(origin, solana) / max(origin, solana) over a 5-min window (count proxy; USD-weighted parity per Appendix B follows once per-bridge ABI decoders populate amount_usd). signer / frontend / oracle stream live once their detectors are deployed.", "weights": { "parity": 40, "outflow": 25, "signer": 15, "frontend": 10, "oracle": 10 } },
"bridge_id": "wormhole",
"computed_at": "2026-08-08T19:30:05.176Z",
"score": 90,
"components": { "parity_severity": 0, "outflow_severity": 0, "signer_recency": 0, "frontend_recency": 0.989213, "oracle_staleness": 0 }
}/v1/bridges/:id/historyReal score history for one bridge, from ?since= (ISO timestamp; optional, defaults to 24h ago) onward. That's the only query parameter -- there is no ?limit= on this route and no pagination. Real example: since=2026-08-08T00:00:00Z on wormhole returns 364 real entries (this bridge gets scored roughly every ~1min), not 2 -- expect and handle a large array.
curl -s "https://bridge-radar-api-3dbs.onrender.com/v1/bridges/wormhole/history?since=2026-08-08T00:00:00Z"{
"bridge_id": "wormhole",
"since": "2026-08-08T00:00:00Z",
"history": [
{ "bridge_id": "wormhole", "computed_at": "2026-08-08T14:22:14.762Z", "score": 100, "components": { "parity_severity": 0, "outflow_severity": 0, "signer_recency": 0, "frontend_recency": 0, "oracle_staleness": 0 } },
{ "bridge_id": "wormhole", "computed_at": "2026-08-08T14:23:11.876Z", "score": 100, "components": { "parity_severity": 0, "outflow_severity": 0, "signer_recency": 0, "frontend_recency": 0, "oracle_staleness": 0 } }
/* ... all 364 real entries for this real window, unpaginated. Full array, no truncation --
this is genuinely everything scoreHistory() returns for the given since. */
]
}/v1/eventsReal detected anomaly + transfer events (signer_change, frontend_change, oracle_stale, lock/mint/burn/unlock), newest first. Complete real query parameter list, confirmed against the route handler: ?bridge= (exact bridge_id), ?type= (exact BridgeEventKind), ?chain= (exact chain id), ?since= (ISO timestamp), ?limit= (integer). All optional; no others exist. Every event also carries a real finality_anomaly_at_time (see /v1/network/finality below) -- true only if a real Finality Watch observation within 5s of this event's own event_time was itself flagged anomalous; purely descriptive, not a claim about this specific transaction.
curl -s "https://bridge-radar-api-3dbs.onrender.com/v1/events?limit=1"{
"events": [
{ "id": "90a9ee82-dd2e-4688-a810-aeb52e0ac253", "bridge_id": "across", "event_time": "2026-08-10T20:01:20.956Z", "type": "lock", "chain": "solana", "asset": "unknown", "amount_usd": 0, "tx": "5PGc8soBEVnu4BXvT2PFg3yrrVcdFVWvEcCxuoG5ZeceMv8oXFDKZ3JmBJAy1iqY54QXLJ9uJkfZ2NVEXnWhLd64", "finality_anomaly_at_time": false }
]
}/v1/registryEvery bridge we track (implemented and planned), independent of live health data -- chains supported, homepage, adapter status. No query parameters, no pagination -- this is genuinely the complete, real, current list, exactly 16 entries.
curl -s https://bridge-radar-api-3dbs.onrender.com/v1/registry{
"summary": { "total": 16, "implemented": 14, "planned": 2 },
"implemented": [
{ "id": "wormhole", "name": "Wormhole", "homepage": "https://wormhole.com", "supportedChains": ["solana","ethereum","polygon","avalanche","arbitrum","optimism","bsc","base","sui","aptos"], "hasSolana": true, "status": "active" }
/* ... all 14 real implemented ids, in this real order: wormhole, allbridge, debridge,
layerzero, mayan, portal, axelar, relay, across, garden, base-solana-bridge, atomiq,
rhinofi, orderly -- same shape as above for every one. */
],
"planned": [
{ "id": "hyperlane", "name": "Hyperlane", "homepage": "https://hyperlane.xyz", "supportedChains": ["solana","ethereum","polygon","arbitrum","optimism","base"], "hasSolana": true, "status": "active" },
{ "id": "cctp", "name": "Circle CCTP", "homepage": "https://www.circle.com/en/usdc/bridge", "supportedChains": ["solana","ethereum","polygon","arbitrum","optimism","base","avalanche"], "hasSolana": true, "status": "active" }
]
}/v1/weekly-digestTrailing-7-day anomaly-event count and healthy/watch/alert tally across every monitored bridge — the same computation the Telegram weekly digest sends.
curl -s https://bridge-radar-api-3dbs.onrender.com/v1/weekly-digest{
"windowStart": "2026-08-03T10:03:35.927Z",
"windowEnd": "2026-08-10T10:03:35.927Z",
"anomalyEventCount": 128,
"monitoredBridgeCount": 14,
"bridgeHealthTally": { "healthy": 10, "watch": 4, "alert": 0, "unmonitored": 2 }
}/v1/network/finalityReal observed Solana finality health -- 'Finality Watch' (see the on-page explainer above). latest is the most recent real confirmed->finalized observation; rollingBaselineMs is the real trailing-hour median (null under 5 real samples); isAnomalous is true only when latest exceeds that baseline by 3x. No query parameters.
curl -s https://bridge-radar-api-3dbs.onrender.com/v1/network/finality{
"latest": { "slot": 438465853, "confirmedAt": "2026-08-10T20:01:07.707Z", "finalizedAt": "2026-08-10T20:01:20.948Z", "elapsedMs": 13241 },
"rollingBaselineMs": 12221.5,
"sampleCount": 122,
"windowStart": "2026-08-10T19:08:34.915Z",
"isAnomalous": false,
"anomalousBridgeEventsLastHour": 0,
"note": "rollingBaselineMs and isAnomalous are computed from real observed data only"
}WebSocket — GET /v1/ws
Live event stream. Sends a real hello on connect, then pushes every new bridge_event row as it's indexed (the API polls its own store every 1s, no separate notify mechanism).
const ws = new WebSocket("wss://bridge-radar-api-3dbs.onrender.com/v1/ws");
ws.onmessage = (e) => console.log(JSON.parse(e.data));
// real first message: {"kind":"hello","data":{"server_time":"2026-08-09T20:17:14.013Z"}}Real error responses
Every error is a real JSON body shaped { error: string }, sometimes with a detail field — read from the actual route handlers, not guessed:
| Status | Where | Real body |
|---|---|---|
| 404 | /v1/bridges/:id | {"error":"bridge not found"} |
| 404 | /v1/bridges/:id/health | {"error":"no score yet"} -- for an unscored bridge or an unknown id alike |
| 404 | /widget/health/:bridgeId | {"error":"unknown bridge \"<id>\""} |
| 404 | any unmatched route | {"error":"not found"} |
| 400 | wallet-activity / wallet-holdings / wallet-timeline / streak / game-scores | {"error":"invalid Solana address"} / "wallet_address must be a real, valid Solana address" |
| 400 | /v1/defillama/price/:mint | {"error":"invalid mint address"} |
| 400 | wallet-activity / wallet-timeline | {"error":"limit must be a number"} |
| 400 | /v1/game-scores | {"error":"score must be an integer between 0 and 100000"} (and matching messages for blocks_used / distance) |
| 501 | /v1/wallet-timeline/:address | {"error":"not configured","detail":"..."} -- no HELIUS_API_KEY / non-Helius SOLANA_RPC_URL. Never a degraded fake classification. |
| 502 | wallet-activity / wallet-holdings / wallet-timeline | {"error":"failed to fetch wallet ...","detail":"<real underlying error message>"} |
| 503 | wallet-activity / wallet-holdings / wallet-timeline | {"error":"...rate-limited...","detail":"real guidance to set a paid SOLANA_RPC_URL"} |
No API key and no rate limiting enforced by Bridge Radar itself on any route today (noted at the top of this section) — the 503s above come from the upstream Solana RPC being rate-limited, not from us.
Embeddable health badge
One script tag, vanilla JS, no framework or build step required on your site. Polls the real live score every 30s.
<script src="https://bridge-radar-api-3dbs.onrender.com/widget.js" data-bridge="wormhole"></script>Live, on this actual page
The badge below is the real widget script, embedded on this real page right now — not a screenshot or a mockup:
Every real data-* attribute — two, no more
Read straight from apps/api/src/widget.ts: the whole script reads exactly two attributes off its own <script> tag. There is no color/size/theme override, no custom label text, no other data attribute — the badge's inline styles are fixed in the script itself.
| Attribute | Required | Real behavior |
|---|---|---|
| data-bridge | Yes | Bridge id to query. If missing, the script silently returns and renders nothing at all — no badge, no error in the DOM. |
| data-api | No | Defaults to the script tag's own origin (new URL(script.src).origin). Set this when the widget script is served from a different host than the API. |
Real states — exactly what renders, from the actual source
- On load: a grey dot and "Bridge Radar: loading…", immediately, before the first fetch resolves.
- On a successful response: the dot becomes green/yellow/red per the real
bandfield (grey for any unrecognized band, e.g."unmonitored"), and the label becomes exactly{displayName}: {score} · {Healthy|Watch|Alert}— or an em dash instead of the score whenscoreis null. - Invalid bridge id, unreachable API, or any other fetch failure — identical fallback: grey dot, "Bridge Radar: unavailable". The script's single
.catch()doesn't distinguish a real 404 (bad bridge id) from a network error (API down) — both render exactly the same text. If you need to tell those apart, callGET /widget/health/:bridgeIdyourself instead of embedding the script. - Every 30s after that, forever, via
setInterval— no backoff, no max-retry cutoff.
Data comes from a real, openly-CORS'd GET /widget/health/:bridgeId, separate from the restricted-origin /v1/* routes above. Real source: apps/api/src/widget.ts.
On-chain oracle
A real deployed Anchor program on Solana Devnet — not mainnet. Single-attester model (v1); dApps read the PDA directly, no API dependency.
- Program ID
- 6148M4aXYbDsscWn14zCazPy9V4fQFGozdDQp4LFmqHM
- Network
- Solana Devnet
Read a bridge's real on-chain score
import { Connection, PublicKey } from "@solana/web3.js";
import { sha256 } from "@noble/hashes/sha256";
const PROGRAM_ID = new PublicKey("6148M4aXYbDsscWn14zCazPy9V4fQFGozdDQp4LFmqHM");
const connection = new Connection("https://api.devnet.solana.com");
const bridgeIdHash = sha256(new TextEncoder().encode("wormhole"));
const [pda] = PublicKey.findProgramAddressSync(
[Buffer.from("health"), Buffer.from(bridgeIdHash)],
PROGRAM_ID,
);
const info = await connection.getAccountInfo(pda);
// Account layout (BridgeHealth, after the 8-byte Anchor discriminator):
// bridge_id: [u8; 32] (offset 8)
// score: u8 (offset 40)
// last_updated: i64 (offset 41)
// attester: Pubkey (offset 49)
// bump: u8 (offset 81)
const score = info.data[40];
const lastUpdated = info.data.readBigInt64LE(41);Complete account structure — BridgeHealth (82 bytes incl. discriminator)
| Field | Type | Real meaning |
|---|---|---|
| bridge_id | [u8; 32] | sha256(bridge slug) — the PDA seed, not a display name. |
| score | u8 | 0..=100. 0 means "no score yet," not "perfectly unhealthy." |
| last_updated | i64 | Unix timestamp of the last update_health call. The program's own doc comment says treat scores older than ~10 minutes as stale. |
| attester | Pubkey | The only key allowed to call update_health / rotate_attester for this bridge. |
| bump | u8 | PDA bump seed. |
Every real instruction — three, all attester-gated except registration
init_bridge(bridge_id: [u8; 32], attester: Pubkey)
// Permissionless -- anyone can register a bridge by funding its PDA
// (init, payer = whoever calls it). Sets score = 0, last_updated = 0,
// bump = ctx.bumps.health. Emits BridgeRegistered { bridge_id, attester }.
// Accounts: health (PDA, init), payer (mut signer), system_program.
update_health(bridge_id: [u8; 32], score: u8)
// Attester-only: requires attester.key() == health.attester exactly,
// else RadarError::Unauthorized. Requires score <= 100, else
// RadarError::InvalidScore. Sets score + last_updated = Clock::get()?
// .unix_timestamp. Emits HealthUpdated { bridge_id, prev_score, score,
// timestamp }. Accounts: health (mut, PDA re-derived from seeds+bump),
// attester (signer).
rotate_attester(bridge_id: [u8; 32], new_attester: Pubkey)
// Attester-only (same Unauthorized check as update_health) -- the
// *current* attester must sign to hand off to a new one. Overwrites
// health.attester. Real detail: unlike the other two, this emits no
// event at all. Accounts: health (mut), attester (signer).Two real error codes: Unauthorized ("only the registered attester can perform this action") and InvalidScore ("score must be between 0 and 100 inclusive"). Two real events: BridgeRegistered and HealthUpdated (fields shown above) — those are the only two; rotate_attester emits nothing.
Real source: programs/radar-oracle/src/lib.rs