Skip to content

feat: @alchemy/data-apis — dependency-free Data SDK (approved direction)#2541

Draft
blakecduncan wants to merge 21 commits into
mainfrom
blake/data-sdk-standalone
Draft

feat: @alchemy/data-apis — dependency-free Data SDK (approved direction)#2541
blakecduncan wants to merge 21 commits into
mainfrom
blake/data-sdk-standalone

Conversation

@blakecduncan

Copy link
Copy Markdown
Collaborator

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 *Pages companions. createDataClient({ apiKey, network? }) returns a plain container — network is 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

  • One HTTP engine (@alchemy/common): the REST client's retry/timeout/abort/request-id loop extracted and shared with a new AlchemyJsonRpcClient — the 7 JSON-RPC methods now get request ids, Retry-After handling, and AlchemyApiError normalization (impossible under viem transports, which own their fetch). JSON-RPC-level errors are never retried.
  • Viem-free error family: new AlchemyError root (plain Error, BaseError-compatible message format + walk()); AlchemyApiError/ServerError/FetchError re-parent onto it. Wallet-facing BaseError (viem-based) untouched. In-repo instanceof audit: one site (smart-accounts getRevertErrorData), net-identical behavior. External-repo caveat: anything outside this monorepo catching REST-channel errors via viem's BaseError would 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.
  • viem adapter parked, not deleted: src/viem/dataActions.ts (client.extend(dataActions) for viem/wallet clients) is unexported but unit- and live-tested — it becomes the /viem subpath when adapter demand warrants.

Also in this PR

  • api-codegen relocated from packages/ to the repo root (private-package convention).
  • Codegen pipeline: pinned spec snapshots + lockfile, offline deterministic generation, manifest drift alarms (already caught a real docs-spec bug: alchemy_getTokenBalances omits pageKey from its result), CI drift gate, spec-bump workflow.
  • Alpha packaging: 5.0.3-alpha.0, publishConfig.tag: "alpha", intentionally outside the lerna fixed-version set until graduation (checklist in the README).

Test plan

  • 266 unit tests (common 202, data-apis 45, api-codegen 19) + 8 type tests, all passing
  • 30/30 live smoke tests against production APIs — every namespace, pagination walks, the parked viem adapter, and the new JSON-RPC channel
  • viem-free install proof: pnpm pack both packages → npm install in a clean dir → no viem in node_modules, imports + constructs
  • pnpm generate idempotent; lint/typecheck/docs-drift clean

Out of scope (tracked)

/viem subpath 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

blakecduncan and others added 18 commits June 9, 2026 16:24
…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>
blakecduncan and others added 3 commits June 12, 2026 14:51
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant