The public honeycluster.io endpoint is keyless, so a proxy isn't
required for basic access — browsers can talk to it directly. But you'll
still want a server-side proxy when:
method values, inject default params, or add caching that's
shared across all your users.This tutorial builds that proxy with Express and xrpl.js.
Bashpnpm add express xrpl pnpm add -D @types/express typescript tsx
TypeScript// server.ts import express from 'express' import { Client } from 'xrpl' const app = express() const PORT = Number(process.env.PORT ?? 4000) // Public cluster — no key. For a private endpoint, add: // { headers: { 'X-API-Key': process.env.HONEYCLUSTER_API_KEY! } } const client = new Client('wss://honeycluster.io') await client.connect() app.get('/api/ledger/:index', async (req, res) => { const ledger_index = Number(req.params.index) if (!Number.isFinite(ledger_index)) { return res.status(400).json({ error: 'ledger_index must be numeric' }) } try { const { result } = await client.request({ command: 'ledger', ledger_index, transactions: true, expand: true, }) res.json(result.ledger) } catch (err) { res.status(502).json({ error: (err as Error).message }) } }) app.listen(PORT, () => console.log(`proxy listening on :${PORT}`))
Two things to notice:
process.env. It never travels to the
caller, never appears in response headers, and never lands in
client-side code.If you're exposing this proxy to an untrusted client (like a browser), add your own authorization middleware before the proxy handlers — session cookies, a JWT from your auth service, or a short-lived per-user token:
TypeScriptapp.use('/api', (req, res, next) => { const token = req.header('authorization')?.replace(/^Bearer /, '') if (!token || !verifyUserToken(token)) { return res.status(401).json({ error: 'unauthorized' }) } next() })
Without this, anyone on the internet can hit your proxy and consume capacity under your IP (or, for private plans, spend your credits).
Historical ledger data is immutable — the same ledger index always returns the same payload. Cache aggressively to cut upstream traffic (and, on private plans, credit spend):
TypeScriptimport { LRUCache } from 'lru-cache' const ledgerCache = new LRUCache<number, unknown>({ max: 500 }) app.get('/api/ledger/:index', async (req, res) => { const ledger_index = Number(req.params.index) const cached = ledgerCache.get(ledger_index) if (cached) return res.json(cached) const { result } = await client.request({ command: 'ledger', ledger_index, transactions: true, expand: true, }) ledgerCache.set(ledger_index, result.ledger) res.json(result.ledger) })
For larger deployments, swap the in-memory LRU for Redis and share the cache across pods.
Honeycluster's errors are already structured. Don't mask them — proxy them through so clients see the real cause:
TypeScripttry { // ...call client.request... } catch (err: any) { const status = err?.data?.error === 'notFound' ? 404 : 502 res.status(status).json({ code: err?.data?.error ?? 'UPSTREAM_ERROR', message: err?.message ?? 'Upstream request failed', }) }
See the Error Codes reference for the full list of codes your proxy might relay.