@direct.dev/sdk
v1.0.6
Published
Direct.dev SDK for RPC fetch acceleration.
Downloads
943
Readme
@direct.dev/sdk
A drop-in fetch wrapper for Direct.dev RPC endpoints. Your existing code keeps working — Direct.dev quietly takes over JSON-RPC requests to its own URLs and serves them faster, with caching, sync, and failover built in.
Highlights
Sub-millisecond responses Popular requests are answered from a local cache; the rest are routed through the Direct.dev edge.
Less bandwidth Direct Sync streams only what changed since your last request, instead of resending the full payload every time.
Compact wire format Direct Wire is a binary protocol tuned for Web3 traffic — smaller frames and faster decode than JSON-RPC.
Built-in failover Requests reroute through a healthy path automatically when an upstream is unavailable. No retry logic to write.
Drop-in
Plugs into native fetch, so viem, ethers, and your own code work unchanged. Only Direct.dev URLs are intercepted.
Install
npm install @direct.dev/sdkQuick start
Run install() once at app startup and you're done:
import { install } from "@direct.dev/sdk";
install();
const res = await fetch("https://rpc.direct.dev/v1/<project>.<token>/ethereum", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [] }),
});
const { result } = await res.json();
// "0x134a1c0"That's it. Every request to a Direct.dev RPC URL now goes through the SDK.
With options
install({
logging: { client: "warn", requests: "summary" },
});Without touching globals
If you'd rather not wrap globalThis.fetch (e.g. inside a library, or to scope to a single instance), use createFetch():
import { createFetch } from "@direct.dev/sdk";
const directFetch = createFetch({
logging: { client: "info" },
});
const res = await directFetch("https://rpc.direct.dev/v1/<project>.<token>/ethereum", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [] }),
});
const { result } = await res.json();Pick one —
install()orcreateFetch(). Don't mix them.
Using with viem
With install(), viem (and anything else that reaches for fetch) just works:
import { install } from "@direct.dev/sdk";
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
install();
const client = createPublicClient({
chain: mainnet,
transport: http("https://rpc.direct.dev/v1/<project>.<token>/ethereum"),
});
await client.getBlockNumber();If you'd rather keep the global fetch untouched, pass createFetch() directly:
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
import { createFetch } from "@direct.dev/sdk";
const directFetch = createFetch({ logging: { client: "info" } });
const client = createPublicClient({
chain: mainnet,
transport: http("https://rpc.direct.dev/v1/<project>.<token>/ethereum", {
fetch: directFetch,
}),
});Preload
Warm endpoints at startup so the first request resolves locally.
In the browser, prefer an HTML preload link. It lets the browser start fetching the warmup payload before your JS bundle even loads:
<link
rel="preload"
as="fetch"
crossorigin="anonymous"
href="https://rpc.direct.dev/v1/<project>.<token>/ethereum"
/>On the server, or in environments without preload links, pass preload to install():
install({
preload: [
"https://rpc.direct.dev/v1/<project>.<token>/ethereum",
"https://rpc.direct.dev/v1/<project>.<token>/sonic",
],
});Configuration
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| logging.client | "silent" \| "error" \| "warn" \| "info" \| "debug" | "warn" | Operational / lifecycle log verbosity. |
| logging.requests | "off" \| "summary" \| "debug" | "off" | Per-request tracing. |
| logging.onEvent | (event: LogEvent) => void | — | Structured event sink. Replaces console output when set. |
| fetch | typeof fetch | globalThis.fetch | Underlying fetch to wrap. |
| createWebSocket | (url) => WebSocket | globalThis.WebSocket | WebSocket factory for Direct Wire sync. |
| preload | string[] | auto from <link rel="preload"> | Endpoint URLs to warm at startup. In the browser, the SDK already picks up <link rel="preload"> tags automatically — use this option for SSR / non-browser runtimes, or to add endpoints not declared in the HTML. |
fetch
Wrap a specific implementation instead of globalThis.fetch. Useful in tests, custom runtimes, or when you need a createFetch() instance that bypasses an existing install():
const directFetch = createFetch({
fetch: window.fetch.bind(window),
});createWebSocket
Provide your own constructor for the Direct Wire sync connection. Handy for instrumentation, tests, or runtimes where globalThis.WebSocket isn't what you want:
install({
createWebSocket: (url) => new WebSocket(url),
});Logging
The logging option covers three independent concerns:
logging.client— operational logs (preload, sync, client phases).logging.requests— per-request tracing.logging.onEvent— a structured sink. When set, events go here instead of the console.
logging.client and logging.requests don't gate each other — requests: "summary" still logs request summaries when client is "warn".
logging.client
| Level | What's emitted |
| --- | --- |
| silent | Nothing. |
| error | Errors only. [direct:ERROR] Batch contains duplicate request IDs { ids: [1, 1] } |
| warn | Errors + warnings. [direct:WARN] Request failed { input: ..., err: ... } |
| info | Sparse operational summaries, including live sync connection state. |
| debug | Full lifecycle: preload, sync, client phases. |
Example debug output:
[direct:DEBUG] preload.phase {
phase: "applied",
client_instance_id: "client_ethereum_f47ac10b_1",
network_id: "ethereum"
}
[direct:DEBUG] client.phase {
phase: "created",
client_instance_id: "client_ethereum_f47ac10b_1",
network_id: "ethereum"
}
[direct:INFO] sync.connection {
client_instance_id: "client_ethereum_f47ac10b_1",
network_id: "ethereum",
connected: true,
transport: "wire_socket",
reason: "started"
}
[direct:INFO] sync.ready {
client_instance_id: "client_ethereum_f47ac10b_1",
network_id: "ethereum",
ready: true,
transport: "wire_socket",
reason: "block_state_available",
current_block_height: "0x134a1c0"
}
[direct:INFO] block.advanced {
client_instance_id: "client_ethereum_f47ac10b_1",
network_id: "ethereum",
source: "wire_socket",
current_block_height: "0x134a1c1"
}logging.requests
| Level | What's emitted |
| --- | --- |
| off | No request tracing. |
| summary | One concise resolution log per handled request. |
| debug | request_resolution + request_pipeline_resolution + request_handled + block.state_probe. |
Summary example:
[direct:REQUEST] request_resolution {
method: "eth_blockNumber",
latency_ms: 8,
stage_type: "cache",
stage_name: "local_memory_cache"
}Debug example (a single request):
[direct:REQUEST_DEBUG] request_pipeline_resolution {
method: "eth_call",
latency_ms: 12,
stage_type: "cache",
stage_name: "continuum_cache"
}
[direct:REQUEST_DEBUG] request_handled {
req: { jsonrpc: "2.0", id: 1, method: "eth_call", params: [...] },
res: { jsonrpc: "2.0", id: 1, result: "0x1" },
latency_ms: 12
}logging.onEvent
Get events as structured objects — useful for dashboards, telemetry, or piping into your own observability stack:
import { install, type LogEvent } from "@direct.dev/sdk";
const onDirectEvent = (event: LogEvent) => {
dashboard.push(event);
};
install({
logging: {
client: "debug",
requests: "debug",
onEvent: onDirectEvent,
},
});Event shape:
type LogEvent = {
level: "debug" | "info" | "warn" | "error";
name: string;
value: Record<string, unknown>;
timestamp: number;
};Stable public events — always safe to consume:
request_resolutionpreload.phasepreload.clock_syncsync.rebuildsync.connectionsync.readyblock.advancedclient.phase
Debug-only diagnostics — only emitted when the relevant log level is active:
request_pipeline_resolutionrequest_handledblock.state_probe
The SDK exports LogEvent, LoggingOptions, and CreateFetchOptions for typed integrations.
Inspecting the live client
The SDK intercepts a special JSON-RPC method, direct_client_status, and answers it locally without touching the network. Useful for debugging your integration:
const res = await fetch("https://rpc.direct.dev/v1/<project>.<token>/ethereum", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "direct_client_status",
params: [],
}),
});
const { result } = await res.json();
// {
// initialized: true,
// projectId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
// projectToken: "abc123",
// networkId: "ethereum",
// baseUrl: "https://rpc.direct.dev",
// logging: { client: "warn", requests: "off" }
// }How it works
The SDK only takes over POST requests whose body is JSON-RPC and whose URL matches a Direct.dev RPC endpoint:
https://rpc.direct.dev/v1/<project>.<token>/<network>https://staging.rpc.direct.dev/v1/<project>.<token>/<network>https://prod.rpc.direct.dev/v1/<project>.<token>/<network>
License
Provided under the Direct.dev Terms and Conditions. Use of this package requires agreement to those terms.
For questions, reach out at [email protected].
