Add x402 payment support (Node-first, opt-in) - #14
Merged
Conversation
Approach A: subpath export (glassnode-api/x402) + core x402 preset, with x402-fetch/viem as optional peer deps. Coinbase x402-fetch v1, Base/USDC, maxPaymentPerCall cap, apiKey optional under x402. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
- Mandate excluding src/x402.ts from browser tsconfig/rollup (release-pipeline risk)
- Add source DEFAULT_API_URL constant; apiUrl .url().optional(); apiKey field -> string|undefined
- Note apiKey conditional-required is runtime-only (z.input)
- Helper: import type for viem, verify signer type before building walletClient,
cast wrapped fetch as typeof fetch, cut walletClient override (YAGNI)
- Tests: vi.mock dynamic imports, assert parseUnits('0.06',6)===60000n, 402-not-retried
- Add per-call spend-guard caveat + wallet-safety guidance; subpath CJS-only note
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
Verified against live x402.glassnode.com / .tech 402 challenges: - Endpoint speaks x402 v2 (x402Version:2, header-based challenge, eip155 CAIP-2 networks) -> switch client from Coinbase x402-fetch v1 to x402-foundation @x402/fetch v2 (v1 cannot parse v2) - Bulk endpoints 404 over x402 -> callBulkMetric unsupported in x402 mode - Pricing confirmed: metadata $0.01 (10000), metrics $0.05 (50000) - Testnet x402.glassnode.tech = Base Sepolia (eip155:84532); add X402_TESTNET_API_URL + opt-in testnet integration test step - Require fetch when x402:true (construction-time Zod refine) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
…ded) Coinbase's CDP buyer quickstart itself now installs @x402/fetch @x402/evm (scoped v2); the unscoped x402-fetch v1 is the deprecated predecessor and cannot parse the v2 challenge the Glassnode endpoint returns. So the "Coinbase client" and the foundation v2 client are the same package. - Helper builds the v2 client (registerExactEvmScheme + x402Client) and wrapFetchWithPayment(fetch, client); deps @x402/fetch + @x402/evm + viem - Spend cap: use v2 max-amount option if present, else a payment-requirements selector that throws over the cap - Removes the "pending confirmation" caveat on the client decision Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
7 TDD tasks: config preset, base-URL resolution, 402 error, packaging (optional peer deps + subpath export + browser exclude), createX402Fetch helper, opt-in testnet integration, docs. Helper source pre-verified with tsc 6.0.3 against real @x402/fetch + @x402/evm + viem types. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
Opus plan review found one blocker: Task 1 dropped the apiUrl Zod default but deferred the constructor's base-URL resolution to Task 2, so the existing default-URL test would fail under the Husky vitest-related pre-commit hook and block the Task 1 commit. Merge config schema + constructor/request changes into a single Task 1; renumber the rest (now 6 tasks). All other review checks passed (Zod, tests, packaging, lint). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
Adds src/x402.ts: usdcDecimalToAtomic, createMaxAmountPolicy, and createX402Fetch, which turns a viem LocalAccount into an x402-payment capable fetch (registers ExactEvmScheme for Base mainnet + Sepolia, enforces a per-call USDC spend ceiling). All value imports of the optional peer deps (@x402/fetch, @x402/evm) are dynamic import()s inside the async function, so the subpath stays importable without the deps installed until createX402Fetch is called; a missing dep rejects with a clear install-instructions error. Also adds "types": ["node"] to tsconfig.json: @x402/core's shipped .d.mts references the global Buffer type without an explicit reference/import, and this project's base tsconfig was not auto-including @types/node ambient globals (same gap tsconfig.test.json already worked around), which broke `tsc` once src/x402.ts pulled in @x402/core's types transitively. Verified this doesn't leak into the browser build (tsconfig.browser.json already excludes src/x402.ts; confirmed no x402 symbols appear in the rollup bundles).
…a env) Adds examples/x402.active-addresses.ts: builds a payment-capable fetch from a funded Base wallet, checks the metadata endpoint for SUI + 1h support, then fetches SUI active addresses (last 1 month, 1h) over x402. Network (testnet/ mainnet), wallet key, and per-call cap are env-driven. Updates .env.example, examples package.json (@x402/fetch, @x402/evm, viem), and the examples README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
The old 402 message ("pass an x402-capable fetch") misleads when the fetch
IS x402-capable but the payment didn't settle (e.g. insufficient USDC or
over maxPaymentPerCall) — the exact case an unfunded testnet wallet hits.
Reword to cover both: payment-not-completing (check USDC balance / cap) and
the no-wrapper case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
…upported SUI active_count isn't offered on the testnet x402 endpoint (metadata: SUI absent from the supported-asset list), so default the example to ETH (works on testnet) and make the asset overridable via X402_ASSET. When the chosen asset isn't supported for the metric, the example now prints a few supported assets and returns early instead of wasting the $0.05 metric call on a 403. Renamed to ex.x402.active-addresses.ts to match the local examples naming. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
Glassnode returns a JSON body like {"message":"Resolution 1h is not
allowed. Allowed resolutions: [24h, 1w, 1month]"} on failures, but the
client discarded it — so a 403/400 gave only a generic message. request()
now reads the error body (best-effort, never throws), extracts message/error
(or raw text), and appends it; GlassnodeApiError gains an optional `detail`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
active_count rejects i=1h on the x402 endpoint ("Resolution 1h is not
allowed"), so default to 24h. Make metric/asset/resolution env-configurable
and add X402_SKIP_METADATA to isolate a single paid call.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
The optional logger received the full request URL, which in non-x402 mode includes ?api_key=<key> — leaking the API key into any log sink. Mask the api_key value before logging (other params preserved). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
Remove the X402_TESTNET_API_URL constant (which baked the testnet domain into the shipped library) and every reference to that domain across tests, example, README, and design docs. Target a non-default x402 endpoint by passing its URL as `apiUrl`: - example reads an optional X402_API_URL override (not in .env.example) - integration test reads X402_TESTNET_URL and skips when unset Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
A 402 body of literal "null" (the x402 challenge lives in a header, body is null) was being appended to the error as "— null". readErrorDetail now only uses a string message/error field from valid JSON, and falls back to raw text only for non-JSON bodies. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
…example/docs X402_ASSET is an example-only override, so keep it in the script (default BTC) but remove it from .env.example and the README. .env.example documents X402_API_URL (mainnet) with a valid https URL and drops the contradictory "not shown" comment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
…port # Conflicts: # package.json # pnpm-lock.yaml
Per PR review: >=2.18.0 would allow a breaking @x402/* v3; ^2.18.0 pins to the v2 API the helper targets (matches viem's ^2.48.11). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Cf2DgwfRTWWuKtEhUeuZq
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds opt-in, Node-first x402 payment support so callers can make paid Glassnode calls (USDC on Base) through the existing
GlassnodeAPIclient — without the core package taking on any crypto dependency.x402: trueconfig preset routes to the paid endpoint;apiKeybecomes optional in that mode,fetchrequired.glassnode-api/x402withcreateX402Fetch({ account, maxPaymentPerCall }).@x402/fetch,@x402/evm,viem) is an optional peer dependency; the core entry stayszod-only and browser-safe (src/x402.tsis the only file that touches crypto, via dynamicimport()).@x402/fetch+@x402/evm); spend cap enforced via a payment policy; a bare viemLocalAccountsigns EIP-3009 authorizations.Bumps to 0.8.0.
Also included (from hands-on testing)
GlassnodeApiErrornow surfaces the server's error-body message (e.g. "Resolution 1h is not allowed") and exposes it on.detail.api_keyis redacted in URLs passed to the optionallogger.402message for failed x402 payments.examples/ex.x402.active-addresses.ts(default BTC @ 24h), env-configurable; the testnet endpoint URL is supplied via env, never hardcoded.Verification
X402_TESTNET_PRIVATE_KEY+X402_TESTNET_URLare set); lint,build, andbuild:browserclean.LocalAccountpaid metadata ($0.01) + metric ($0.05) and returned validated data.🤖 Generated with Claude Code