Skip to main content

Reference Implementations

Server-side JavaScript and Python modules for exact Block lookup, list paging, relationship summaries, bounded neighborhoods, and an application route. Every call is a plain HTTP GET against the XNDR Network REST API, so no client package is required; the JavaScript uses the global fetch in Node.js 18+, and the Python uses only the standard library.

Shared Client Setup​

Define one request helper and reuse the helper across modules. The base URL comes from XNDR_API_BASE_URL and an optional operator-issued key from XNDR_API_KEY.

The helper:

  • Sends the key as X-XNDR-API-Key.
  • Applies scope=default unless a request sets another scope.
  • Throws an Error carrying status, code, and retryAfter from the response.
// Trailing slashes are stripped so a path join never produces a double slash.
const BASE_URL = (process.env.XNDR_API_BASE_URL ?? 'https://xndr.network/api/v1').replace(/\/+$/, '');
const API_KEY = process.env.XNDR_API_KEY;

export async function xndrGet(path, params = {}) {
const url = new URL(`${BASE_URL}${path}`);
url.searchParams.set('scope', 'default');

for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
}

const headers = { Accept: 'application/json' };
if (API_KEY) headers['X-XNDR-API-Key'] = API_KEY;

const response = await fetch(url, { headers });
const body = await response.json().catch(() => ({}));

if (!response.ok) {
// Carrying code and Retry-After on the Error lets callers branch without reading the body again.
const error = new Error(body.error?.message ?? `XNDR Network request failed with HTTP ${response.status}`);
error.status = response.status;
error.code = body.error?.code ?? 'unknown';
error.retryAfter = Number(response.headers.get('Retry-After')) || null;
throw error;
}

return body;
}

export function hasStatus(error, status) {
return Boolean(error && typeof error === 'object' && 'status' in error && error.status === status);
}

Exact Block Lookup​

Validate the identifier before the lookup, and keep the returned blockHash as the stable link target.

import { xndrGet, hasStatus } from './client.js';

// The API accepts only a full 0x-prefixed, 64-character hexadecimal hash.
const HASH_PATTERN = /^0x[0-9a-f]{64}$/;

export async function lookupBlock(blockHash) {
const normalized = String(blockHash).trim().toLowerCase();

if (!HASH_PATTERN.test(normalized)) {
return { status: 'invalid', message: 'Expected a full 0x-prefixed 64-hex Block hash.' };
}

try {
const { data, scope } = await xndrGet(`/blocks/${normalized}`);

return {
status: 'found',
block: data.block,
verification: data.verification,
scopeId: scope.id,
indexRevision: scope.indexRevision,
permalink: `/blocks/${data.block.blockHash}`,
};
} catch (error) {
if (hasStatus(error, 404)) {
return { status: 'not-observed', message: 'No match was found in the selected scope.' };
}

throw error;
}
}

Paging​

One pager serves every list route. The pager pins atRevision so every page comes from one snapshot, waits out 429, and restarts from the first page at the current revision when a 409, or a 400 on a cursor, reports that the revision moved. The rules the pager applies are under Pagination and Snapshots.

import { xndrGet, hasStatus } from './client.js';

const wait = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

// maxPages caps the walk so one call cannot page an unbounded list.
export async function pageAll(path, params = {}, { atRevision, maxPages = 20, maxRestarts = 3 } = {}) {
let revision = atRevision ?? (await xndrGet('/status')).scope.indexRevision;
let items = [];
let cursor;
let pages = 0;
let page;
let restarts = 0;

while (true) {
try {
const envelope = await xndrGet(path, { ...params, atRevision: revision, cursor });
items.push(...envelope.data);
page = envelope.page;
cursor = page.nextCursor;
pages += 1;
if (!cursor || pages >= maxPages) break;
} catch (error) {
if (hasStatus(error, 429)) {
await wait(error.retryAfter ?? 60);
} else if ((hasStatus(error, 409) || (hasStatus(error, 400) && cursor)) && restarts < maxRestarts) {
// The revision moved; the cursor is void, so the walk starts over at the current revision.
restarts += 1;
revision = (await xndrGet('/status')).scope.indexRevision;
items = [];
cursor = undefined;
pages = 0;
} else {
throw error;
}
}
}

return { items, atRevision: revision, truncated: page.truncated, nextCursor: cursor ?? null };
}

Relationship Summary​

Retrieve the Block, then page the Block's declared parents and indexed children on the snapshot the Block view came from.

import { xndrGet } from './client.js';
import { pageAll } from './paging.js';

export async function getRelationshipSummary(blockHash) {
const focus = await xndrGet(`/blocks/${blockHash}`);
const options = { atRevision: focus.scope.indexRevision, maxPages: 5 };

const [parents, children] = await Promise.all([
pageAll(`/blocks/${blockHash}/parents`, { limit: 5 }, options),
pageAll(`/blocks/${blockHash}/children`, { limit: 5 }, options),
]);

return {
block: focus.data.block,
declaredParents: focus.data.block.parentHashes,
parents: parents.items,
unavailableParents: parents.items.filter((parent) => !parent.available).map((parent) => parent.blockHash),
children: children.items,
moreChildren: Boolean(children.nextCursor),
indexedChildCount: focus.data.indexedChildCount,
scopeId: focus.scope.id,
indexRevision: focus.scope.indexRevision,
};
}

Present Block Context​

Keep canonical identifiers and source-scoped observation context visible together. Pass the data and scope members of a /blocks/ envelope.

React Example​

export function BlockSummary({ data, scope }) {
const { block, firstObservedAt, verification } = data;

return (
<article>
<h2>Block <code>{block.blockHash}</code></h2>

<dl>
<dt>Content hash</dt>
<dd><code>{block.contentHash}</code></dd>

<dt>Declared parents</dt>
<dd>{block.parentHashes.length > 0 ? block.parentHashes.join(', ') : 'None declared'}</dd>

<dt>Index scope</dt>
<dd>{scope.id} at revision {scope.indexRevision}</dd>

<dt>First observed in this scope</dt>
<dd>{firstObservedAt ?? 'Not reported'}</dd>

<dt>Lineage check</dt>
<dd>{verification.lineage}</dd>
</dl>
</article>
);
}

Bounded Neighborhood​

Request a bounded parent or child neighborhood instead of walking relationships one hop at a time. Neighborhood parameters and caps are in the neighborhood table.

import { xndrGet } from './client.js';

// Server ceilings. Clamping here turns a would-be 400 into a valid request.
const MAX_DEPTH = 8;
const MAX_NODES = 10000;

export async function getNeighborhood(blockHash, options = {}) {
const { direction = 'both', depth = 1, maxNodes = 50, status } = options;
const boundedDepth = Math.min(Math.max(Math.trunc(depth), 0), MAX_DEPTH);
const boundedNodes = Math.min(Math.max(Math.trunc(maxNodes), 1), MAX_NODES);

const { data, scope } = await xndrGet(`/blocks/${blockHash}/neighborhood`, {
direction,
depth: boundedDepth,
maxNodes: boundedNodes,
status,
});

return {
rootHash: data.rootHash,
nodes: data.nodes,
edgeCount: data.edges.length,
budgetTruncated: data.budgetTruncated,
unavailable: data.nodes.filter((node) => !node.available).map((node) => node.blockHash),
scopeId: scope.id,
};
}

Treat budgetTruncated: true as a signal to narrow direction, lower depth, or apply a filter.

Server-Side HTTP Route​

Keep REST API credentials on the server and translate expected upstream responses into application-level responses.

import { xndrGet, hasStatus } from './client.js';

const HASH_PATTERN = /^0x[0-9a-f]{64}$/;

export async function GET(request) {
const hash = new URL(request.url).searchParams.get('hash')?.toLowerCase();

if (!hash || !HASH_PATTERN.test(hash)) {
return Response.json({ error: 'A full 0x-prefixed Block hash is required' }, { status: 400 });
}

try {
const { data, scope } = await xndrGet(`/blocks/${hash}`);

return Response.json({
block: data.block,
verification: data.verification,
scope: { id: scope.id, indexRevision: scope.indexRevision },
});
} catch (error) {
if (hasStatus(error, 404)) {
return Response.json({ error: 'No match in the selected scope' }, { status: 404 });
}

if (hasStatus(error, 429)) {
return Response.json(
{ error: 'Rate limit reached' },
{ status: 429, headers: { 'Retry-After': String(error.retryAfter ?? 60) } },
);
}

return Response.json({ error: 'XNDR Network request failed' }, { status: 502 });
}
}

Implementation Guidelines​

Validate behavior against the OpenAPI description served at /api/v1/openapi.yaml. Scope, availability, and completeness are defined in the API Overview.