@direct.dev/sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,6 @@
1
+ Copyright © 2025 0ms Inc. d/b/a Direct. All rights reserved.
2
+
3
+ This software is provided under the Direct Terms and Conditions, available at:
4
+ https://direct.dev/terms
5
+
6
+ By using this software, you agree to the Direct Terms and Conditions.
package/README.md ADDED
@@ -0,0 +1,339 @@
1
+ # @direct.dev/sdk
2
+
3
+ <div align="center">
4
+ <p>
5
+ <a href="https://direct.dev/">
6
+ <img src="https://s6.imgcdn.dev/YVbIIy.png" alt="Direct.dev logo" width="125" />
7
+ </a>
8
+ </p>
9
+
10
+ <p>
11
+ <a href="https://www.npmjs.com/package/@direct.dev/sdk"><img alt="NPM Version" src="https://img.shields.io/npm/v/%40direct.dev%2Fsdk?style=for-the-badge&labelColor=555&color=00BCB1"></a>
12
+ <a href="https://direct.dev/terms"><img alt="License" src="https://img.shields.io/badge/license-Direct.dev%20Terms-blue?style=for-the-badge&labelColor=555&color=00BCB1"></a>
13
+ </p>
14
+ </div>
15
+
16
+ A drop-in `fetch` wrapper for [Direct.dev](https://direct.dev/) RPC endpoints. Your existing code keeps working — Direct.dev quietly takes over JSON-RPC calls to its own URLs and serves them faster, with caching, sync, and failover built in. Everything else passes through to native `fetch` untouched.
17
+
18
+ ## Highlights
19
+
20
+ **Sub-millisecond responses**<br />
21
+ Popular requests are answered from a local cache; the rest are routed through the Direct.dev edge.
22
+
23
+ **Less bandwidth**<br />
24
+ Direct Sync streams only what changed since your last call, instead of resending the full payload every time.
25
+
26
+ **Compact wire format**<br />
27
+ Direct Wire is a binary protocol tuned for Web3 traffic — smaller frames and faster decode than JSON-RPC.
28
+
29
+ **Built-in failover**<br />
30
+ Calls reroute through a healthy path automatically when an upstream is unavailable. No retry logic to write.
31
+
32
+ **Drop-in**<br />
33
+ Plugs into native `fetch`, so viem, ethers, and your own code work unchanged. Only Direct.dev URLs are intercepted.
34
+
35
+ ## Install
36
+
37
+ ```sh
38
+ npm install @direct.dev/sdk
39
+ ```
40
+
41
+ ## Quick start
42
+
43
+ Call `install()` once at app startup and you're done:
44
+
45
+ ```ts
46
+ import { install } from "@direct.dev/sdk";
47
+
48
+ install();
49
+
50
+ const res = await fetch("https://rpc.direct.dev/v1/<projectId>.<token>/ethereum", {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [] }),
54
+ });
55
+
56
+ const { result } = await res.json();
57
+ // "0x134a1c0"
58
+ ```
59
+
60
+ That's it. Every call to a Direct.dev RPC URL now goes through the SDK; everything else still hits native `fetch`.
61
+
62
+ ### With options
63
+
64
+ ```ts
65
+ install({
66
+ logging: { client: "warn", requests: "summary" },
67
+ });
68
+ ```
69
+
70
+ ### Without touching globals
71
+
72
+ If you'd rather not wrap `globalThis.fetch` (e.g. inside a library, or to scope to a single instance), use `createFetch()`:
73
+
74
+ ```ts
75
+ import { createFetch } from "@direct.dev/sdk";
76
+
77
+ const directFetch = createFetch({
78
+ logging: { client: "info" },
79
+ });
80
+
81
+ const res = await directFetch("https://rpc.direct.dev/v1/<projectId>.<token>/ethereum", {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json" },
84
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [] }),
85
+ });
86
+
87
+ const { result } = await res.json();
88
+ ```
89
+
90
+ > Pick one — `install()` or `createFetch()`. Don't mix them.
91
+
92
+ ## Using with viem
93
+
94
+ With `install()`, viem (and anything else that reaches for `fetch`) just works:
95
+
96
+ ```ts
97
+ import { install } from "@direct.dev/sdk";
98
+ import { createPublicClient, http } from "viem";
99
+ import { mainnet } from "viem/chains";
100
+
101
+ install();
102
+
103
+ const client = createPublicClient({
104
+ chain: mainnet,
105
+ transport: http("https://rpc.direct.dev/v1/<projectId>.<token>/ethereum"),
106
+ });
107
+
108
+ await client.getBlockNumber();
109
+ ```
110
+
111
+ If you'd rather keep the global `fetch` untouched, pass `createFetch()` directly:
112
+
113
+ ```ts
114
+ import { createPublicClient, http } from "viem";
115
+ import { mainnet } from "viem/chains";
116
+ import { createFetch } from "@direct.dev/sdk";
117
+
118
+ const directFetch = createFetch({ logging: { client: "info" } });
119
+
120
+ const client = createPublicClient({
121
+ chain: mainnet,
122
+ transport: http("https://rpc.direct.dev/v1/<projectId>.<token>/ethereum", {
123
+ fetch: directFetch,
124
+ }),
125
+ });
126
+ ```
127
+
128
+ ## Preload
129
+
130
+ Warm endpoints at startup so the first request resolves locally.
131
+
132
+ In the browser, prefer an HTML preload link. It lets the browser start fetching the warmup payload before your JS bundle even loads:
133
+
134
+ ```html
135
+ <link
136
+ rel="preload"
137
+ as="fetch"
138
+ crossorigin="anonymous"
139
+ href="https://rpc.direct.dev/v1/<projectId>.<token>/ethereum"
140
+ />
141
+ ```
142
+
143
+ On the server, or in environments without preload links, pass `preload` to `install()`:
144
+
145
+ ```ts
146
+ install({
147
+ preload: [
148
+ "https://rpc.direct.dev/v1/<projectId>.<token>/ethereum",
149
+ "https://rpc.direct.dev/v1/<projectId>.<token>/sonic",
150
+ ],
151
+ });
152
+ ```
153
+
154
+ ## Configuration
155
+
156
+ | Option | Type | Default | Description |
157
+ | --- | --- | --- | --- |
158
+ | `logging.client` | `"silent" \| "error" \| "warn" \| "info" \| "debug"` | `"warn"` | Operational / lifecycle log verbosity. |
159
+ | `logging.requests` | `"off" \| "summary" \| "debug"` | `"off"` | Per-request tracing. |
160
+ | `logging.onEvent` | `(event) => void` | — | Structured event sink. Replaces console output when set. |
161
+ | `fetch` | `typeof fetch` | `globalThis.fetch` | Underlying fetch to wrap. |
162
+ | `createWebSocket` | `(url) => WebSocket` | `globalThis.WebSocket` | WebSocket factory for Direct Wire sync. |
163
+ | `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. |
164
+
165
+ ### `fetch`
166
+
167
+ Wrap a specific implementation instead of `globalThis.fetch`. Useful in tests, polyfilled environments, or when you want a `createFetch()` instance that bypasses an existing `install()`:
168
+
169
+ ```ts
170
+ const directFetch = createFetch({
171
+ fetch: window.fetch.bind(window),
172
+ });
173
+ ```
174
+
175
+ ### `createWebSocket`
176
+
177
+ Provide your own constructor for the Direct Wire sync connection. Handy for instrumentation, tests, or runtimes where `globalThis.WebSocket` isn't what you want:
178
+
179
+ ```ts
180
+ install({
181
+ createWebSocket: (url) => new WebSocket(url),
182
+ });
183
+ ```
184
+
185
+ ## Logging
186
+
187
+ The `logging` option covers three independent concerns:
188
+
189
+ - **`logging.client`** — operational logs (preload, sync, client phases).
190
+ - **`logging.requests`** — per-request tracing.
191
+ - **`logging.onEvent`** — a structured sink. When set, events go here instead of the console.
192
+
193
+ `logging.client` and `logging.requests` don't gate each other — `requests: "summary"` still logs request summaries when `client` is `"warn"`.
194
+
195
+ ### `logging.client`
196
+
197
+ | Level | What's emitted |
198
+ | --- | --- |
199
+ | `silent` | Nothing. |
200
+ | `error` | Errors only. `[direct:ERROR] Batch contains duplicate request IDs { ids: [1, 1] }` |
201
+ | `warn` | Errors + warnings. `[direct:WARN] Request failed { input: ..., err: ... }` |
202
+ | `info` | Reserved for sparse operational summaries. No default emissions yet. |
203
+ | `debug` | Full lifecycle: preload, sync, client phases. |
204
+
205
+ Example debug output:
206
+
207
+ ```text
208
+ [direct:DEBUG] preload.phase {
209
+ phase: "applied",
210
+ client_instance_id: "client_ethereum_f47ac10b_1",
211
+ network_id: "ethereum"
212
+ }
213
+ [direct:DEBUG] client.phase {
214
+ phase: "created",
215
+ client_instance_id: "client_ethereum_f47ac10b_1",
216
+ network_id: "ethereum"
217
+ }
218
+ ```
219
+
220
+ ### `logging.requests`
221
+
222
+ | Level | What's emitted |
223
+ | --- | --- |
224
+ | `off` | No request tracing. |
225
+ | `summary` | One concise resolution log per handled request. |
226
+ | `debug` | `request_resolution` + `request_pipeline_resolution` + `request_handled` + `block.state_probe`. |
227
+
228
+ Summary example:
229
+
230
+ ```text
231
+ [direct:REQUEST] request_resolution {
232
+ method: "eth_blockNumber",
233
+ latency_ms: 8,
234
+ stage_type: "cache",
235
+ stage_name: "local_memory_cache"
236
+ }
237
+ ```
238
+
239
+ Debug example (a single request):
240
+
241
+ ```text
242
+ [direct:REQUEST_DEBUG] request_pipeline_resolution {
243
+ method: "eth_call",
244
+ latency_ms: 12,
245
+ stage_type: "cache",
246
+ stage_name: "continuum_cache"
247
+ }
248
+ [direct:REQUEST_DEBUG] request_handled {
249
+ req: { jsonrpc: "2.0", id: 1, method: "eth_call", params: [...] },
250
+ res: { jsonrpc: "2.0", id: 1, result: "0x1" },
251
+ latency_ms: 12
252
+ }
253
+ ```
254
+
255
+ ### `logging.onEvent`
256
+
257
+ Get events as structured objects — useful for dashboards, telemetry, or piping into your own observability stack:
258
+
259
+ ```ts
260
+ install({
261
+ logging: {
262
+ client: "debug",
263
+ requests: "debug",
264
+ onEvent(event) {
265
+ dashboard.push(event);
266
+ },
267
+ },
268
+ });
269
+ ```
270
+
271
+ Event shape:
272
+
273
+ ```ts
274
+ type DirectLogEvent = {
275
+ level: "debug" | "info" | "warn" | "error";
276
+ name: string;
277
+ value: Record<string, unknown>;
278
+ timestamp: number;
279
+ };
280
+ ```
281
+
282
+ **Stable public events** — always safe to consume:
283
+
284
+ - `request_resolution`
285
+ - `preload.phase`
286
+ - `preload.clock_sync`
287
+ - `sync.rebuild`
288
+ - `client.phase`
289
+
290
+ **Debug-only diagnostics** — only emitted when the relevant log level is active:
291
+
292
+ - `request_pipeline_resolution`
293
+ - `request_handled`
294
+ - `block.state_probe`
295
+
296
+ Typed helper unions live in `@direct.dev/client`: `KnownDirectLogEvent`, `StableDirectLogEvent`, `DebugDirectLogEvent`.
297
+
298
+ ## Inspecting the live client
299
+
300
+ The SDK intercepts a special JSON-RPC method, `direct_client_status`, and answers it locally without touching the network. Useful for debugging your integration:
301
+
302
+ ```ts
303
+ const res = await fetch("https://rpc.direct.dev/v1/<projectId>.<token>/ethereum", {
304
+ method: "POST",
305
+ headers: { "Content-Type": "application/json" },
306
+ body: JSON.stringify({
307
+ jsonrpc: "2.0",
308
+ id: 1,
309
+ method: "direct_client_status",
310
+ params: [],
311
+ }),
312
+ });
313
+
314
+ const { result } = await res.json();
315
+ // {
316
+ // initialized: true,
317
+ // projectId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
318
+ // projectToken: "abc123",
319
+ // networkId: "ethereum",
320
+ // baseUrl: "https://rpc.direct.dev",
321
+ // logging: { client: "warn", requests: "off" }
322
+ // }
323
+ ```
324
+
325
+ ## How it works
326
+
327
+ The SDK only takes over POST requests whose body is JSON-RPC and whose URL matches a Direct.dev RPC endpoint:
328
+
329
+ - `https://rpc.direct.dev/v1/<projectId>.<token>/<networkId>`
330
+ - `https://staging.rpc.direct.dev/v1/<projectId>.<token>/<networkId>`
331
+ - `https://prod.rpc.direct.dev/v1/<projectId>.<token>/<networkId>`
332
+
333
+ Everything else — REST APIs, image loads, third-party APIs — passes straight through to native `fetch`.
334
+
335
+ ## License
336
+
337
+ Provided under the [Direct.dev Terms and Conditions](https://direct.dev/terms). Use of this package requires agreement to those terms.
338
+
339
+ For questions, reach out at [info@direct.dev](mailto:info@direct.dev).