Thanks for helping improve the TypeScript client for the Glassnode API. This guide walks you through opening a pull request (PR).
A merge to main releases the version in package.json to npm once a maintainer approves
the release (.github/workflows/publish.yml). main only accepts changes through reviewed PRs.
- Node.js 24 for development (see
.nvmrc; the test runner, Vitest, needs Node >= 22.12). Consumers of the published package only need Node.js >= 22. - pnpm (the version is pinned in
package.jsonpackageManager;corepack enablepicks it up).
Fork the repository (or, with write access, create a branch) and install dependencies:
git clone https://github.com/<you>/glassnode-api-ts-client.git
cd glassnode-api-ts-client
git checkout -b my-change
pnpm install
pnpm exec husky # install the pre-commit hookThe repo's .npmrc disables install scripts, so pnpm install does not set up the Husky
pre-commit hook on its own; pnpm exec husky does. The hook runs the tests, ESLint + Prettier on
staged files and the build.
Project layout:
src/index.ts- main entry (glassnode-api): theGlassnodeAPIclient, errors and typessrc/x402.ts- theglassnode-api/x402subpath entry (paid calls via x402)src/types/- Zod schemas and the types inferred from themtest/- Vitest tests, including the contract tests intest/contract.spec.tsexamples/- runnable usage examples (their ownpackage.json)scripts/- the Node-floor smoke test, the contract fixture recorder and the release toolingtypecheck/x402-node16/- a consumer type-check fixtureNOTICE- attribution; listed inpackage.jsonfilesso it ships in the npm tarball, as Apache-2.0 section 4(d) requires
Add or update tests for what you change. The public API is what src/index.ts and src/x402.ts
export; if you change it, update README.md to match and give every new export a doc comment.
The shipped code must keep running on Node.js 22 and in browsers: no node: built-ins and no APIs
newer than Node 22 in src/.
Every change to the published package bumps version in package.json and adds an entry at the
top of CHANGELOG.md describing it. Docs-, CI- and tooling-only changes (README, CONTRIBUTING,
workflows, tests, lint and editor config) don't: they publish nothing, and the top CHANGELOG.md
heading must keep naming the current version. Follow semver:
- Major: breaking changes (removed or renamed exports, changed method signatures, changed defaults, tightened or widened response schemas, a higher minimum Node.js version).
- Minor: new features, methods or config options that are backward compatible.
- Patch: bug fixes and internal refactors with no API change.
The README's Stability and versioning section spells out what counts as breaking.
The version in your PR is exactly the one that gets published: the release workflow does not bump it. A PR merged without a bump publishes nothing.
Release branches. A major release is prepared on a long-lived release/** branch (e.g.
release/1.0). PRs into a release branch carry no version bump (except the last one, below), but
each still adds its CHANGELOG.md entry, with migration notes for breaking changes, under a single
## 1.0.0 (unreleased)-style heading. Before the release branch goes to main, a last PR (into
the release branch, or the final PR itself) bumps the version (e.g. to 1.0.0) and renames that
heading to exactly ## 1.0.0, the form the release workflow extracts the GitHub Release notes
from. CI enforces it: into a release branch the top heading may be ## <x.y.z> (unreleased) or
## <package.json version>, into main it must be exactly ## <package.json version>
(node scripts/check-changelog-heading.mjs <target branch> runs the check locally). The release
branch takes merges from main during its lifetime, and right after any CI change lands there: a
PR's CI runs the ci.yml of its merge commit, so the release branch needs the current one. Only
PRs into it are CI-checked, not direct pushes; the final PR to main checks the combined result.
CI runs these on every PR into main or a release/** branch; run them locally first:
pnpm run lint
pnpm test
pnpm exec tsc -p tsconfig.test.json --noEmit
pnpm exec tsc -p tsconfig.examples.json
pnpm run build
pnpm run build:browser
pnpm exec tsc -p typecheck/x402-node16/tsconfig.json
node scripts/check-changelog-heading.mjs main
npx prettier --check .
pnpm run docs
pnpm dlx publint
pnpm dlx @arethetypeswrong/cli --pack .pnpm run test:coverage enforces the coverage thresholds in vitest.config.ts, and
pnpm run format fixes formatting. pnpm run docs fails on any TypeDoc warning, such as a broken
{@link}.
(Or against the release/** branch, for a change that belongs to an upcoming major release.)
A good PR description says:
- What changed.
- Why: the problem it solves or the issue it closes.
- How it was verified: the checks you ran and any manual testing.
- User-facing impact: behaviour changes for consumers, and any breaking change called out explicitly.
CI must pass, and a maintainer reviews the PR. Once it is approved, a maintainer merges it.
The merge starts the release workflow. It re-runs the full CI check list, then its publish job
waits for a maintainer to approve it (the npm environment). After the approval it publishes the
new version to npm with provenance, tags the commit v<version> and creates a GitHub Release from
your CHANGELOG.md entry. If that version is already on npm, the workflow skips the release.
Merge release PRs one at a time. Publishes run one after another (the npm-publish
concurrency group), and one in progress is never cancelled. But GitHub keeps only one pending
run per group: when a newer merge's publish job queues up, it replaces an older one that is still
waiting, including one waiting for its approval. That older version is then never published. This
happened to 0.29.5 and 0.30.0 on 2026-09-29. Wait for each release to finish (published, tagged,
GitHub Release created) before merging the next PR that bumps the version.
Detecting a skipped version. Every release run's summary lists, as a warning, each
CHANGELOG.md version between the last one published to npm and the one being released that is
missing on npm.
Recovering. Open the skipped version's workflow run (Actions → Publish Package, the merge
commit that carries that version) and choose Re-run all jobs, then approve the publish. If a
higher version is already npm's latest, the older one is published under the
backport-<major>.<minor> dist-tag (e.g. backport-0.29) instead, so latest never moves
backwards, and its GitHub Release is not marked "Latest". A prerelease version is published under
next. A run from a commit older than this guard (1.0.0) publishes with npm's default tag, so after
re-running one, check npm dist-tag ls glassnode-api; if latest moved backwards, a maintainer
restores it with npm dist-tag add glassnode-api@<newest version> latest.
test/fixtures/contract/ holds real API responses recorded by scripts/record-fixtures.mjs.
Never hand-edit them; re-record them. That needs an API key, so it is only done to refresh the
fixtures. A contract test that fails against a fixture is real API drift: fix the schema, not the
fixture or the test (after a re-recording, the README lists the few literal values in
test/contract.spec.ts that may legitimately need updating). See
Recording contract fixtures in the README.
- Report vulnerabilities privately as described in SECURITY.md. Never open a public issue for them.
- Never commit API keys or other secrets, including in fixtures, examples,
.envfiles or test output.
Decisions that are easy to undo by accident. Read the relevant one before changing the toolchain, the build or the workflows.
Two baselines, kept separate:
- Consumers need Node.js >= 22 (
package.jsonengines; raised from 18 in 1.0).src/uses only universal APIs plus globalfetchand also runs in browsers, so the floor is no licence to use Node-only APIs. - Developers run Node.js 24 (
.nvmrc, the main CI and publish jobs). Vitest 5 needs Node.js >= 22.12; that is dev-only and never reaches consumers. The CIcompat-node-floorjob proves the consumer floor: on Node 22 it builds,requires the CJS entry and runsscripts/smoke-timeout.mjs.
@types/nodeis pinned to the floor (^22), not the dev runtime, so the compiler rejects APIs newer than Node 22. It is approximate (CI runs the latest 22.x and@types/nodeis^22.20, so an API added after 22.0 is not caught). Bump its major only when the minimum supported Node.js is intentionally raised, which is a breaking change (major release andenginesbump). Dependabot ignores its major updates for this reason.- TypeScript stays on 6.x. As of 1.0.0,
typescript-eslintdeclares itstypescriptpeer as<6.1.0, so TypeScript 7 breakspnpm run lint(in CI and in the pre-commit hook). Move to 7.x only once atypescript-eslintrelease accepts it. typedocalso gates TypeScript: itstypescriptpeer lists explicit minors (up to 6.0.x on 0.28.20), so any TypeScript bump, even to 6.1, needs atypedocrelease that lists it, orpnpm run docs(and CI) break.
- Entry points are
src/index.ts(glassnode-api) andsrc/x402.ts(glassnode-api/x402), named by their@modulecomments; README.md is the landing page. Config intypedoc.json, output inapi-docs/(not committed, not published to npm). typedoc-plugin-zodexpandsz.infer/z.inputaliases, so Zod-derived types such asMetricMetadataandGlassnodeConfigrender as objects with their field comments.- Every warning is an error (
treatWarningsAsErrors;notExported,invalidLink,invalidPathandrewrittenLinkon).notDocumentedis off: it only reports nested members of Zod objects andz.enumliterals, which cannot carry comments. Every top-level export and class member is documented; keep it so. .github/workflows/docs.ymldeploys the reference to GitHub Pages (https://glassnode.github.io/glassnode-api-ts-client/) on every push tomain. It needs the repo setting Settings → Pages → Source: GitHub Actions.
- CJS:
tsc→dist/(therequireentry). ESM:tsc -p tsconfig.esm.json→dist/esm/, unbundled withzodexternal (theimportentry), plus a generateddist/esm/package.json({"type":"module"}).exportshas per-conditiontypes, checked bypublintand@arethetypeswrong/cliin CI. The package itself is"type": "commonjs". - Browser: Rollup (
tsconfig.browser.json) builds minified UMD (dist/glassnode-api.umd.min.js, globalGlassnodeAPI) and ESM (dist/glassnode-api.esm.min.js) bundles fromsrc/index.ts, withzodbundled in andsrc/x402.tsleft out. Source maps arehiddenand not published.modulepoints at the unbundleddist/esm/index.js, not a bundle.tsconfig.browser.jsonusesmoduleResolution: bundler(not the deprecatednode/node10); Rollup's node-resolve plugin does the actual resolution. - No
browserfield.unpkgandjsdelivrpoint at the UMD bundle so the bare CDN URL serves it. There is deliberately no top-levelbrowserfield and nobrowsercondition inexports: the condition would send webpack 5, Vite and esbuild to the pre-minified bundle withzodinlined instead of the tree-shakeable ESM build (the top-level field only affectedexports-unaware tools such as Browserify). Keep both out, even thoughpublintsuggests the condition (#37). src/imports end in.js(./foo.jsforfoo.ts).dist/esm/is unbundledtscoutput, Node's ESM loader needs full extensions, andtscnever rewrites a.jsspecifier. Extensionless specifiers only work behind a bundler or CommonJS..tsspecifiers withrewriteRelativeImportExtensionswere rejected:tscleaves them as.tsin the emitted.d.ts. Tools that runsrc/directly must map.jsto.ts; ts-node needsexperimentalResolver, set inexamples/tsconfig.json, and CI guards it by importing../srcthrough ts-node fromexamples/.GlassnodeConfigSchemastays a plainz.object(no.transform()), so.shape,.pick,.extendand.partialkeep working. Defaults that depend on other options (maxRetriesisDEFAULT_MAX_RETRIES, orDEFAULT_X402_MAX_RETRIESwithx402) are applied in the constructor.- TypeScript configs:
tsconfig.json(CJS),tsconfig.esm.json(ESM),tsconfig.browser.json(browser),tsconfig.test.json(tests and IDE),tsconfig.examples.json(type-checksexamples/againstsrc/with the root dependencies),examples/tsconfig.json(ts-node, for running the examples) andtypecheck/x402-node16/tsconfig.json(a node16 consumer withskipLibCheck: false).
ci.ymlruns on PRs intomainandrelease/**, andpublish.ymlcalls it as itsverifyjob, so a release runs the same checks. Its concurrency group cancels superseded runs only for PRs; a run called bypublish.ymlis never cancelled.- Its jobs are
test(Node 24: the step-4 check list,test:coverageand the examples' ts-node import guard) andcompat-node-floor(Node 22). The rulesets require them by name, so renaming a job means updating the rulesets;compat-node-flooris version-neutral so a floor bump only changes itsnode-version. - Every action is pinned by full commit SHA with a
# vX.Y.Zcomment, never by a movable tag. To bump one, resolve the tag withgh api repos/<owner>/<repo>/git/ref/tags/<tag>(for an annotated tag, dereference it withgh api repos/<owner>/<repo>/git/tags/<sha>to get the commit), update SHA and comment together, and read the release notes for changed inputs or defaults. - Every workflow sets minimal
permissions::contents: readby default, and write access only on thepublishjob, which needs it.
publish.ymlpublishes with npm Trusted Publishing (OIDC) and provenance: there is noNPM_TOKENsecret. The Trusted Publisher forglassnode-apion npmjs.com names repoglassnode/glassnode-api-ts-client, workflowpublish.ymland environmentnpm.- The
npmenvironment (Settings → Environments) has the maintainer as required reviewer and allows deployments frommainonly. Only thepublishjob has write permissions:id-token: write(OIDC) andcontents: write(tag and Release, viaGITHUB_TOKEN). - The
releasejob runsscripts/release-state.sh: only an E404 fromnpm viewmeans "not published"; any other failure fails the job and never publishes.publishre-checks npm before publishing. An existing tag on the same commit or an existing Release is fine; a tag on another commit is left alone with a warning. If the version is on npm from this very commit (npm records itsgitHead), a re-run only (re)creates the missing tag and Release. - npm CLI >= 11.5.1 does the OIDC exchange, so the workflow installs the latest npm and runs
npm publish, notpnpm publish(which uses the setup-node placeholder token and gets a 404). The setup-node.npmrcis overwritten first so npm authenticates via OIDC. - The repo
.npmrcsetsignore-scripts=true, sonpm publishskipsprepublishOnly: the workflow runsbuildandbuild:browseritself. Thepnpm/action-setupsteps setnpm_config_ignore_scripts: 'false'because the standalone pnpm needs its own setup script. - Dist-tag guard (
scripts/release-plan.mjs, SemVer 2.0.0 precedence, no dependencies):npm publishalways gets an explicit--tag:latest,nextfor a prerelease, orbackport-<major>.<minor>for a version below the currentlatest. Notv<major>.<minor>: npm rejects a tag that is a valid semver range.release-state.shfails on any other tag.package.jsonpublishConfigdeliberately sets notag:tag: "latest"there would forcelateston a manual publish or an npm that doesn't let--tagoverride it. Keep it out.
This project is licensed under the Apache License 2.0. Under its Section 5, any contribution you intentionally submit for inclusion is licensed under the same terms, unless you explicitly state otherwise.