feat: @alchemy/data-apis — dependency-free Data SDK (approved direction)#2541
Draft
blakecduncan wants to merge 21 commits into
Draft
feat: @alchemy/data-apis — dependency-free Data SDK (approved direction)#2541blakecduncan wants to merge 21 commits into
blakecduncan wants to merge 21 commits into
Conversation
…nnels Vertical-slice prototype of the Data SDK architecture: - 3 actions, one per seam: portfolio.getTokensByAddress (REST, multi-network body via AlchemyRestClient), nft.getNftsForOwner (REST, network-scoped URL with per-request override), transfers.getAssetTransfers (JSON-RPC over AlchemyTransport, override via derived transport instance) - dataActions decorator + createAlchemyDataClient convenience wrapper - common: resolveNetwork accepting viem Chain | slug | CAIP-2, derived from the existing daikon-generated ALCHEMY_RPC_MAPPING; AlchemyRestClient exported See packages/data/README.md for scope and deliberate omissions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Matches the wallet-apis package naming standard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
14-test script covering all three methods (portfolio, nft, transfers), all three network input formats (viem Chain, slug, CAIP-2), per-request network overrides, and the raw decorator path — all verified green. Run with: ALCHEMY_API_KEY=<key> pnpm --filter @alchemy/data-apis smoke-test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Package is already namespaced to Alchemy; the prefix is redundant. Mirrors the wallet-apis pattern of dropping the brand prefix from the factory function name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds @alchemy/api-codegen (private workspace package) implementing the two-stage pipeline from the data SDK codegen plan: - snapshot (network): bundles specs from a local docs checkout using the docs repo's own tooling (redocly / generate:rpc), commits them under specs/ with a lockfile pinning the docs commit SHA + sha256 checksums - generate (offline, deterministic): emits committed TypeScript into packages/data-apis/src/generated — openapi-typescript output + RestRequestSchema entries for REST, json-schema-to-typescript params/ result types + viem RpcSchema entries for OpenRPC The hand-maintained codegen.manifest.ts maps spec operations to the generated surface; a renamed/removed spec operation hard-fails generate (drift alarm), uncovered operations are reported for visibility. data-apis schema/rest.ts, schema/rpc.ts, and types.ts are now thin hand-reviewed aliases over generated internals. Runtime action code is untouched; public types adopt spec-accurate optionality (transfers/ ownedNfts/totalCount/category optional) and richer fields. The spec's "Not Found (null)" string result branch is deliberately collapsed. Repo wiring: turbo generate task (was referenced by root scripts but undefined), nx generate outputs, prettier/eslint handling (generated files carry a file-level eslint-disable and are self-formatted with the repo prettier config; spec snapshots are ignored). Verified: 15 generator unit tests, 4 data-apis unit tests, build + typecheck clean, pnpm generate idempotent, 14/14 live smoke tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n specs Snapshots now pinned to alchemyplatform/docs main (becfd714) instead of a feature branch. prices (REST) and token (OpenRPC) enter the manifest so generation covers all five v1 spec sources; actions land in follow-ups. RPC emitter now strips non-root schema titles before compilation — title-named shared subschemas (e.g. token's Hex Encoded Address) otherwise hoist duplicate identifiers when one spec has multiple methods. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…est-id) - RestRequestSchema gains an optional Query channel; RestRequestParams maps it via a keyof guard so legacy entries without Query keep compiling and reject query payloads. Route-literal Response narrowing unchanged. - Bounded retries with exponential backoff on 429/5xx/network failures only, honoring Retry-After; per-attempt AbortSignal.timeout merged with the caller's signal (caller aborts are never retried); per-request UUID sent as X-Alchemy-Client-Request-Id on every attempt. - New AlchemyApiError base (status/code/requestId/retryAfter) under BaseError; ServerError/FetchError re-parented onto it with an additive trailing details param — existing instanceof checks and constructor calls are unaffected. - composeSignals/sleep utils; 11 new rest client tests (query serialization, retry/backoff/Retry-After, abort, request-id propagation, error fields). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…recated ops - REST tuples now carry the Query channel (required when any query param is required); standalone Query alias emitted whenever query params exist. - Manifest gains optional pagination metadata per operation/method (pageParam / responseCursorField / itemsField, dotted paths for nested cursors); validated against snapshots at generate time — drift in cursor fields fails generation. Consumed by hand-written *Pages actions; emits no runtime code. - Referencing a spec-deprecated operation hard-fails generation. - NFT action drops the URLSearchParams route cast: typed query channel + abort signal via the hardened AlchemyRestClient. Actions accept an optional RequestOptions third arg. - Snapshot lockfile labels detached-HEAD checkouts '(detached)'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- portfolio: + getTokenBalancesByAddress, getNftsByAddress, getNftContractsByAddress (global Data API, multi-network bodies; per-entry fields like address-level filters preserved, network-slug enum deliberately widened for the escape hatch) - prices: getTokenPricesBySymbol (chain-agnostic GET), getTokenPricesByAddress (per-entry network resolution), getHistoricalTokenPrices (symbol or network+address forms) - nft: full v3 read surface — 21 actions (ownership/contract/collection listings, metadata + batch, owners, sales, floor price, search, spam/ airdrop/rarity checks). Mutations and deprecated/v2 ops excluded per scope plan. Bracketed wire keys (contractAddresses[], *Filters[]) exposed unbracketed in params and restored by the actions. - token: getTokenBalances (positional optionals trimmed), getTokenMetadata, getTokenAllowance over a shared getRpcRequest helper (also adopted by transfers.getAssetTransfers) - DataRpcSchema now aggregates transfers + token entries; REST actions all accept an options arg with an abort signal - 29 new wire-level tests (URL/method/body/query assertions per namespace) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- paginate(): shared async-generator driver yielding whole pages; stops on missing/empty cursors, throws on a repeated cursor (infinite-loop guard), supports AbortSignal + maxPages - 9 *Pages companion actions for every paginated method (nft owner/contract/ collection listings + sales, portfolio nfts/contracts by address, transfers), wired into the decorator namespaces; cursor wiring (pageKey vs startToken→pageKey/nextToken, body vs query vs rpc-param) follows the manifest's validated pagination metadata - wrapRpcError(): JSON-RPC failures normalize into AlchemyApiError (code/ status), with /v2/<key> and apiKey query credentials redacted from any URL-bearing text — keys never leak through error chains. REST is already normalized inside AlchemyRestClient. - 10 new tests (cursor walking, repeat-cursor guard, maxPages, consumer break, abort, redaction, error mapping) + a decorator-level paging test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- version 5.0.3-alpha.0 with publishConfig.tag 'alpha'; deliberately NOT in lerna.json's fixed-version publish set so the regular release can't ship it as latest — alpha publishing + graduation checklist documented in the README - typedoc: data-apis entry point + tsconfig.typedoc include + nav registration in generate-typedoc-yaml.ts; generated reference docs committed under docs/pages/reference/data-apis (CI's docs:sdk drift check now covers the package) - README rewritten: install, both entry points, per-namespace quickstart, three network formats, pagination, error semantics, release process Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- on-pull-request: 'pnpm generate && git diff --exit-code' over generated output and spec snapshots, mirroring the existing docs:sdk drift check — fails if generation is stale or snapshots were hand-edited (offline, no new secrets) - bump-api-specs (workflow_dispatch): checks out the public docs repo at main, re-snapshots, regenerates, and opens a PR via create-pull-request; manifest validation hard-fails the run when an SDK-referenced operation was renamed/removed upstream Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- index.test-d.ts: 7 vitest typecheck tests (client/decorator surface equivalence, AlchemyTransport constraint enforcement, spec-accurate result shapes, three-format network params, Pages generator types); data-apis vitest config now enables typecheck under TYPECHECK=true like the shared config - exports.test.ts: locks the package's runtime export surface; generated internals barrel stays internal - smoke test grown 14 → 30 live tests: every namespace exercised against the real API (portfolio x3 new, prices x3, nft x5 new, token x3) plus two-page pagination walks for nft and transfers companions — all passing Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…EST engine - AlchemyError: dependency-free error root (plain Error) replicating BaseError's message format + walk() cause traversal. AlchemyApiError (and ServerError/FetchError via inheritance) re-parent onto it — the REST/RPC error family no longer touches viem at runtime. Wallet-facing BaseError (viem-based) is untouched; in-repo instanceof audit found one net-identical site (smart-accounts getRevertErrorData). - networkRegistry throws AlchemyError (was the hidden runtime-viem path for data-apis). - httpEngine: the retry/timeout/abort/request-id attempt loop extracted from AlchemyRestClient (behavior-identical; existing tests pass unmodified) and shared with the new AlchemyJsonRpcClient. - AlchemyJsonRpcClient: typed JSON-RPC over the same engine — 429/5xx/ network retried honoring Retry-After; JSON-RPC-level errors never retried, mapped to AlchemyApiError with the rpc code; request ids and retryAfter now exist on the RPC channel (not possible under viem transports); credential redaction on server-echoed text (redactUrlCredentials moves to common). - viem marked optional in peerDependenciesMeta so data-only consumers aren't forced to install it; rest/types' Prettify inlined (last type-level viem import in the REST surface). - 12 new tests (error-format parity, hierarchy, walk; rpc envelope/retry/ no-retry/redaction/abort). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ndation
Implements the leadership-approved direction (Data SDK Foundation doc):
- createDataClient returns a plain container ({config, network}) — no viem
client underneath. Chainless construction now works (the eager-transport
ChainNotFoundError dies): portfolio/prices need no network at all;
single-network methods error clearly at call time.
- Network inputs narrow to strings (Alchemy slugs + CAIP-2); viem Chain
objects move to the adapter layer (bridge: eip155:${chain.id}).
- The 7 JSON-RPC methods run on AlchemyJsonRpcClient over the shared HTTP
engine — gaining request ids, Retry-After handling, and AlchemyApiError
normalization at the source (wrapRpcError and the last viem value
imports are deleted).
- The viem decorator is parked at src/viem/dataActions.ts (unexported,
internally tested — unit + live smoke): it becomes the /viem subpath
when adapter demand warrants, per the doc. viem drops to a
devDependency; the published package has no viem requirement.
- Tests updated throughout; new coverage for chainless construction, the
rpc envelope/request-id, and the parked adapter. 247 unit tests, 8 type
tests, 30/30 live smoke tests pass on the inverted core.
BREAKING CHANGE: AlchemyDataClient is no longer a viem client;
dataActions is no longer exported (returns post-v1 as the /viem adapter);
network params accept slug/CAIP-2 strings only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
packages/ is for published packages; private tooling lives at the root (like halp). Updates workspace globs, vitest projects, REPO_ROOT resolution, the data-apis generate script, eslintignore, CI drift-gate and spec-bump workflow paths, and generated-file banners (regenerated; pipeline verified idempotent post-move). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README leads with the dependency-free quickstart (no chain library), documents string-only network inputs with the eip155 bridge for chain holders, proxy/jwt usage, and the post-v1 ecosystem-adapter plan. Reference docs regenerated for the inverted public surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sively The root barrel eagerly loads the viem-based transport, so importing '@alchemy/common' requires viem at runtime. The new '@alchemy/common/core' subpath exports exactly the viem-free surface (AlchemyError family, REST + JSON-RPC runtime, network registry, chain registry data, utils). The data- apis core imports only /core; the codegen emitter targets it for generated schema imports. Root barrel unchanged — wallet-apis unaffected. Verified: packed both tarballs into a clean project with NO viem installed — node_modules contains no viem, the package imports, and clients construct (with and without a network). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jun 12, 2026
setTimeout(10) can measure as 9ms when both timing endpoints are millisecond-truncated; failed Build and Test on CI. Allow 1ms tolerance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
What
@alchemy/data-apis— Alchemy's Data APIs (Portfolio, Prices, NFT, Token, Transfers) as a dependency-free TypeScript SDK. Implements the direction approved at eng tech review (Data SDK Foundation: viem vs. Standalone): chain-library-free core, ecosystem adapters added on demand post-v1.Supersedes #2539 (architecture RFC) and #2540 (full surface on the viem substrate) — this branch contains both plus the inversion, so it's the single PR to review.
Proof of the headline claim (in CI-able form): both tarballs install into a clean project with no viem in node_modules; the package imports and constructs clients. The 30-test live smoke suite passes against production on the inverted core.
Surface
31 methods across 5 namespaces + 9 auto-paginating
*Pagescompanions.createDataClient({ apiKey, network? })returns a plain container —networkis optional (multi-network methods never need one) and accepts slug/CAIP-2 strings (eip155:${chain.id}is the bridge for viem-chain holders).How the inversion works
@alchemy/common): the REST client's retry/timeout/abort/request-id loop extracted and shared with a newAlchemyJsonRpcClient— the 7 JSON-RPC methods now get request ids, Retry-After handling, andAlchemyApiErrornormalization (impossible under viem transports, which own their fetch). JSON-RPC-level errors are never retried.AlchemyErrorroot (plainError, BaseError-compatible message format +walk());AlchemyApiError/ServerError/FetchErrorre-parent onto it. Wallet-facingBaseError(viem-based) untouched. In-repoinstanceofaudit: one site (smart-accountsgetRevertErrorData), net-identical behavior. External-repo caveat: anything outside this monorepo catching REST-channel errors via viem'sBaseErrorwould change behavior — worth a grep in account-kit/signer repos before stable.@alchemy/common/core: additive viem-free subpath (errors, REST/RPC runtime, network registry, utils). The root barrel still exposes the transport for wallet packages; viem becomes an optional peer of common so data-only installs don't pull it.src/viem/dataActions.ts(client.extend(dataActions)for viem/wallet clients) is unexported but unit- and live-tested — it becomes the/viemsubpath when adapter demand warrants.Also in this PR
api-codegenrelocated frompackages/to the repo root (private-package convention).alchemy_getTokenBalancesomitspageKeyfrom its result), CI drift gate, spec-bump workflow.5.0.3-alpha.0,publishConfig.tag: "alpha", intentionally outside the lerna fixed-version set until graduation (checklist in the README).Test plan
pnpm packboth packages →npm installin a clean dir → no viem in node_modules, imports + constructspnpm generateidempotent; lint/typecheck/docs-drift cleanOut of scope (tracked)
/viemsubpath export (post-v1, demand-gated) · Solana/HyperCore adapters · v1 scope trim to high-usage methods (pending data-team thread) · streaming channel🤖 Generated with Claude Code