This document contains general guidelines for AI agents working on the Viem codebase.
Update after learnings or mistakes; when a correction, new convention, or hard-won lesson emerges during development, append it to the relevant section of this file immediately. AGENTS.md and referenced addenda are the source of truth for project conventions.
- Log papercuts and friction (tooling, docs, APIs, tests, conventions) as you hit them with
pnpx frog log. - Do not add global, system, or internal friction.
- Run
pnpx frog listfirst to see what is already known.
- Exact optional properties;
exactOptionalPropertyTypesis enabled.- Optional properties must include
| undefinedwhen assignable toundefined. - Use
foo?: string | undefined, notfoo?: string.
- Optional properties must include
- No unchecked indexed reads; write new code as if
noUncheckedIndexedAccesswere enabled.- Narrow indexed reads before use.
- Otherwise, make the invariant obvious with the smallest practical assertion.
readonlyarrays; usereadonly T[]for array types in type definitions.- Existing
readonlyproperties are fine; viem has DOM-shaped WebAuthn types and inference-heavy literals that intentionally preservereadonlyproperties. Do not churn them just to satisfy a style preference. typeoverinterfaceby default; usetypefor project-owned shapes. Ambient declarations and DOM-shaped compatibility types may useinterface..jsextensions; all relative source imports include.jsfor ESM compatibility.- Follow local import style; viem uses both namespace imports and named internal imports. Match the surrounding file instead of mass-converting import lists.
- Classes for errors only; all other APIs use functions and plain data.
- Errors live next to the code that throws them; keep module-specific failure classes local.
- Put error classes near the bottom of the owning module.
- Keep public functions and types first.
- Set
nameto the namespaced form.
- No enums; use union types or
as constobjects for fixed sets. - camelCase constants; prefer
camelCasefor local constants unless the surrounding file already uses protocol-style uppercase names for numeric constants. constgeneric modifier; use to preserve literal types for full inference.- Options default
= {}; useoptions: Options = {}notoptions?: Options. - Namespace params and return types; put function types in a matching
declare namespace.- Examples:
from.Options,serialize.ErrorType. - Do not lift params to sibling exports by default.
- Examples:
optionsoverargs; useoptionsfor typed option bags. Use domain nouns only when the parameter is not an options bag.- Minimal variable names; prefer short, obvious names. Use
optionsnotserializeOptions,fnnotcallbackFunction, etc. Context makes meaning clear. - No redundant type annotations; if the return type of a function already covers it, do not annotate intermediate variables. Let the return type do the work.
- No inline object types on locals; when a local variable needs an explicit object-type annotation, declare a named
typeon the line directly above and reference it. - Return directly; do not declare a variable just to return it. Use
return { ... }unless the variable is needed for reuse or readability. - IIFE expressions for fallible local derivations; prefer IIFEs for local
try/catchparsing.- Avoid
let value: Tfollowed by assignment insidetry.
- Avoid
- Skip braces for single-statement blocks; omit
{}for single-statementif,for, etc., when the surrounding file follows that style. - No section separator comments; do not use
// ---or// ===divider comments. Let JSDoc and whitespace provide structure. - No internal-tracking references in code or comments; code must read standalone.
- Avoid planning phases, task IDs, and internal labels.
- A bare
TODO:describing actual work is fine.
- No prior-version references in code or comments; do not mention old names or migration framing.
- Comments describe current behavior only.
- Put parity and migration notes in the changeset.
- Static imports by default; use static
importdeclarations. Dynamic imports are reserved for real runtime boundaries (e.g.viem/nodetrusted setups, optional heavyweight paths). - Namespace imports for modules; prefer
import * as <Module>for module imports.- Alias a module's own
./internal/*helper asimport * as internal. - Named imports are fine for
internal/types.ts. - Named imports are fine for single-function helpers like
stringify,uid, andwait. - Named imports are fine for non-namespace third-party packages like
vitest.
- Alias a module's own
- Minimize
as any; avoid newas anywhere a safer assertion is practical, but do not mass-rewrite existing crypto, tuple, and inference glue that already relies on it. - Destructure when accessing multiple properties; prefer
const { a, b } = optionsover repeatedoptions.a,options.b. - Read from
options.xwhen normalizing a single field; avoid inventing a second name.- Applies when transforming one option into a local of the same name.
- Ox helpers over ad hoc conversion; use ox helpers like
Hex.fromNumber,Hex.toBytes,Bytes.fromHex,Value.fromGwei, etc. instead of open-coded conversions. - Keep property order readable; preserve the local ordering style. Do not alphabetize arrays, RLP tuples, ABI parameters, transaction fields, or other order-sensitive wire shapes.
- Preserve literals; use
constgenerics and narrow helper signatures when an API should preserve literal inputs. - Builder outputs compose; preserve account and chain override generics so builder results pass directly to their downstream actions.
- Type tests in
.test-d.ts; use Vitest'sexpectTypeOfin colocated.test-d.tsfiles to assert generic inference works. Type tests are first-class; write them alongside implementation. Run viapnpm test:typecheck. - Snapshot inferred public types; use type snapshots for public surfaces.
- No
anyleakage; user-facing callback, return, and option types should not leakanyunless the surrounding API already intentionally does. - Type inference after every feature; check whether new types can be narrowed.
- Add or update type tests when public inference changes.
- Profile before guessing; find check-time hot spots with
tsc -p <project> --extendedDiagnostics --generateTrace <dir>andnpx @typescript/analyze-trace <dir>. Cost is usually a few call expressions, not spread evenly. - Instantiation baselines gate regressions;
pnpm bench:typesruns colocated*.bench-d.tsattest benches against pinnedattest.instantiationscounts (20% threshold) and CI runs it in the types job. Measure new baselines by leavingattest.instantiations()empty and running the bench once.
- Stateless module APIs; public APIs are module namespaces full of functions and types. Do not introduce stateful classes for normal library behavior.
- Public entrypoint docs; when adding a public module or export, update the owning
index.ts(andsrc/index.tsfor root exports) with the export and a TSDoc block. - Alphabetical exports; barrel/entrypoint export statements sort by module specifier; named-export lists and the exported declaration blocks of action/module files sort by exported name.
- Keep public APIs lean; avoid exposing options for values the library can derive from existing inputs.
- Wire formats stay explicit; serialization, RPC, RLP, ABI, and transaction-envelope code should keep wire-order and field-shape decisions visible at the call site.
- Nullable RPC omissions; skip both
nullandundefinedwhen copying optional fields from filled transactions. - Bound CCIP batch fan-out; cap total queries, nesting, and concurrent requests. Share one budget across recursive local batches.
- Relay plugins are objects; use
(context, next)middleware and explicit post-fill hooks; keep request state off handler functions. - Relay forwarding transports do not retry; keep retry policy at downstream I/O, including explicit per-request overrides.
- Internal helpers stay internal; keep helper modules under
internal/directories unless they are part of the public API.
- Capitalize Viem in prose; write
Viemwhen referring to the product or project.- Keep
viemlowercase in package names, import paths, code identifiers, filenames, and URLs.
- Keep
- Docs immediately after a module; write site docs before moving to the next module.
- Include the sub-page and sidebar wiring.
- Show example responses with
// @log:; annotate action doc Usage examples that return values.- Place the
// @log:comment directly under the bound result. - Show the example response shape.
- Multi-line
// @log:blocks are fine for objects. - Actions returning
voidare exempt.
- Place the
- Alphabetize doc parameters; option-bag properties are listed alphabetically.
- Required and optional properties share the same ordering.
- This matches source
Optionstypes. - Covers qualified fields such as
#### options.fooand##### options.rank.foo. - Positional function arguments stay in signature order.
- Applies to hand-written docs only.
- Doc-driven API changes; write or update the TSDoc before or alongside the implementation, not as an afterthought.
- TSDoc on public exports; every public function, type, and constant gets TSDoc.
- Public type properties get TSDoc too.
- Decorator methods get JSDoc; every method on a decorator's
Decoratortype gets JSDoc.- Use the same docs as the underlying action.
- Rewrite examples for client-extension calls.
- JSDoc annotations; include
@example,@param, and@returnswhen appropriate. - Examples should be small; public examples should show the minimum useful shape and avoid unrelated setup.
- Use Viem clients and transports in examples; do not use Ox RPC transports to demonstrate Viem APIs.
- Callouts follow code examples; place callouts immediately below the code snippet or code group they supplement.
- Source docs first; public API documentation usually belongs in TSDoc near the exported source.
- Site pages; human-written docs live under
site/pages/.- Generated site page files are not edited by hand.
- No eager type definitions; do not extract a named type until it is used in more than one place or makes a difficult local shape easier to read.
- Shared domain types live near their module; keep reusable public types in the module that owns the domain concept.
- Error unions live in namespaces; exported function error unions should live in that function's namespace as
ErrorType.
- Prefer duplication over the wrong abstraction; duplicated code with a clear bug-fix burden is better than a bad abstraction that is scary to change.
- Do not abstract until the commonalities scream; wait for 3+ concrete use cases where the right abstraction becomes obvious. Do not abstract for 1-2 instances.
- Optimize for change; code that is easy to change beats code that is cleverly DRY. We do not know future requirements.
- No flags or mode parameters; if an abstraction needs
ifbranches or boolean params to handle different call sites, it is usually the wrong abstraction. Inline it. - Start concrete, extract later; begin inline. Extract only when a clear pattern emerges across multiple real usages.
-
Separate test phases with blank lines; keep setup, execution, and assertion groups visually distinct.
-
Colocate relay plugin integration tests; add HTTP coverage to each plugin's existing test file under
src/tempo/internal/relay/, not a shared relay suite. -
Use
pnpm testfor tests; run tests through package scripts, notvitestdirectly. -
Pass
--update allbefore paths, not-u;-utakes an optional value, so-u <path>swallows the path and updates snapshots across the whole suite. -
No mocks, ever; tests must not use mocks, stubs, or
vi.- Forbidden examples:
vi.fn,vi.mock,vi.spyOn, fakefetch, fake clients. - Exercise real behavior against the configured test chain or real ephemeral servers.
- If a test seems to need a mock, rework the code or test.
- Forbidden examples:
-
Target the relevant project; prefer narrow test commands while iterating.
- Use
pnpm test --run <paths>for focused runs. - Use
pnpm test --project core --bail=1for core failures. - Use
--project tempofor tempo work.
- Use
-
Synchronize polling tests with observations; await the initial callback before mining and await each expected emission. Fork startup can exceed the polling interval.
-
Gate RPC reads during bulk mining in gap tests;
anvil_minecan span multiple polls and expose intermediate heads. Disable retries on mining requests because a timed-out request may already have mined its blocks. -
Mine pending transactions before balance baselines; shared Anvil tests may leave pending transfers. Mine before measuring a balance delta so earlier tests cannot affect it.
-
Verify type-test discovery; if
pnpm test:typecheckreports no files, temporarily enabletypecheckon the selected Vitest project with an explicit.test-d.tsinclude and a targeted tsconfig extendingtest/tsconfig.json(composite: false). Restore the test configuration afterward; a no-files run does not verify inference. -
Check for orphaned harness listeners before full-suite runs; a killed test run can leave its proxy holding ports 8545/8645/8745/8845/9545/4337/4338, making later runs fail at global setup (
EADDRINUSE) or time out en masse against the wedged instance. Check them withlsof -nPand kill the stalenodeprocess. -
Tempo tests boot one container per test file; the file's
afterAllstops it. A cancelled run leakstempo.<uuid>containers that starve later runs.docker psanddocker rm -fthe leaked ones before re-running--project tempo. -
Full parallel runs can rate-limit the fork upstream; 100+ fresh fork instances cold-fetching pinned state can saturate the fork RPC, failing random fork-state tests (
getBalance,getProof, …). Re-run with--maxWorkers=4, and verify suspicious failures in isolation before suspecting code. -
Omit
blockTimestampfrom fork transaction snapshots; its presence oneth_getTransactionByHash/includeTransactionsobjects depends on the upstream node implementation (reth extension). Destructure it out before snapshotting. -
Isolate concurrent Zone sequencers; use distinct L1 signer accounts because sequencers share fixed nonce keys.
-
Colocate tests; tests are sibling
*.test.tsfiles next to their module; prefer inline snapshots over snapshot files. -
No tests for pure re-exports; upstream packages own coverage for pure re-export modules.
- Once a facade gains project logic, add sibling tests.
-
Import public APIs from package entrypoints in tests; use aliases for public exports.
- Use entrypoints such as
'viem','viem/actions','viem/tempo', and'viem/node'. - Avoid relative imports for public surface tests.
- Internal helpers may stay relative.
- Non-exported members may stay relative.
- Chain definitions may stay relative.
- Use entrypoints such as
-
Import transports by their bare name; use transport sugar exports in tests.
- Use
http(...),custom(...),fallback(...), andwebSocket(...). - Import those functions from
'viem'. - Do not call
Transport.http(...). - Keep
Transportnamespace imports for non-transport members only.
- Use
-
No redundant top-level
describe; do not wrap a whole file in an echoingdescribe.- Put primary export tests at top level.
- Use
describefor nested scenarios. - Use
describeto separate multiple distinct exports in one file.
-
Inline snapshots over direct assertions; prefer
toMatchInlineSnapshot()over.toBe(),.toEqual(), etc. for stable return values. UsetoThrowErrorMatchingInlineSnapshot()for error assertions. -
Test behavior, not call-tracking; assert observable outputs.
- Test preparation hook outputs as well as public actions; the generic preparation action retains the caller's nonce separately and can hide a hook overwriting it.
- Do not assert that a hook or function was invoked.
- Do not use
let xCalled = falseflags or counters. - Make hooks produce distinguishable, verifiable results.
- Example: a chain
serializehook can return a known value. - Example: recover the signer from a
getSignPayloadpayload.
-
Aggregate varying parameterized results for inline snapshots; Vitest cannot write different inline snapshots at the same assertion location.
-
Snapshot whole objects, omit nondeterministic properties; destructure out nondeterministic fields and snapshot the rest, rather than cherry-picking individual fields to assert.
-
Prefer whole-response snapshots over dynamic fixtures; snapshot whole decoded responses.
- Use deterministic data, like a pinned fork block anvil caches.
- Omit nondeterministic values before snapshotting.
- Do not fetch values in
beforeAllfor mutable fixtures. - Do not construct aggregate objects solely to snapshot unrelated values together.
- For nondeterministic lookups, use deterministic not-found assertions.
- Locally-produced transaction equality is fine for whole-object assertions.
-
Serialization vectors live in ox; the upstream ox package owns the vector test suite.
-
Browser tests use browser suffixes; browser-specific behavior uses
*.browser.test.ts. -
Unit and type tests as you go; write unit tests and
.test-d.tstype tests alongside implementation for each public behavior change. -
100% module coverage; modules with coverage requirements must reach 100%.
- Cover error, retry, and timeout paths.
- Delete or wire unreachable branches.
- Use targeted commands; prefer the smallest command that covers the touched behavior.
- Types; run
pnpm check:typesafter TypeScript changes. - Repo checks; run
pnpm check:repowhen package metadata or workspace shape changes. - Docs dev server; use
pnpm docs:devfor documentation UI work;pnpm docs:buildbuilds the site. pnpm checkmutates; it runs Biome with write and unsafe fixes. Use it only when intentionally applying lint/format fixes.pnpm contracts:buildmutates generated contract artifacts; it runs Forge andscripts/generateTypedArtifacts.ts.- Install hooks can mutate;
pnpm installinitializes Git submodules and rebuilds contracts.- Fresh binary packages may need
node node_modules/<pkg>/install.js.
- Fresh binary packages may need
- Contract dependencies use Git submodules;
contracts/foundry.tomlremaps to packages incontracts/lib.pnpm contracts:buildneeds Foundry and runs on demand. - Preserve canonical token definitions during generation; match hand-authored tokens by
chain and address, not symbol, and include them in
tokens.tempowhen they have a Tempo address.
- Changesets only for public behavior; add or update a changeset when a change affects public API or existing behavior.
- Update existing changesets first; if the branch already has a changeset for the same area, update it instead of adding a duplicate.
- One sentence, past tense; changeset entries are a single sentence written in past tense.
- Breaking changes include migration shape; major changes include a
difffence showing the before/after migration shape.
- Maintainer-gated commits; never commit, push, or stage without explicit maintainer approval.
- Implement and verify, then leave the work tree dirty for review.
- The maintainer decides when work gets committed.
- A landed task may stay reviewed but uncommitted.
- Conventional commits; use
feat:,fix:,refactor:,docs:,test:,chore:prefixes. Scope is optional (e.g.feat(abi): add tuple formatter). - Preserve dirty work; do not revert, clean, or overwrite local changes unless asked.
- Never stage the user's untracked in-progress files.
Guidelines for authoring docs and guides under site/pages/.
- Keep paragraphs concise; aim for 30–40 words, with no more than 3 paragraphs per section. Split longer sections under additional headings.
- Do not use em dashes (
—) in docs. Rewrite with a colon, comma, parentheses, or separate sentences instead. - Use
textfences for box-drawing diagrams in site MDX. Shiki does not recognize thediagramlanguage and fails page rendering. - Prebundle Mermaid for docs development; keep
mermaidinsite/vite.config.ts'soptimizeDeps.include. Usepnpm docs:dev, sincevocs devignores that config.
- Use Title Case for all headings.
- Capitalize the first and last word.
- Capitalize major words: nouns, verbs, adjectives, adverbs, and pronouns.
- Keep minor words lowercase unless first or last.
- Minor words include articles, short coordinating conjunctions, and short prepositions.
- Examples:
Send a Transaction,Pay Fees with Stablecoins,Set a Default Fee Token,See More. - Code identifiers inside a heading keep their original casing (e.g.
### Prefer Sync Actions, but## sendTransactionSyncwhen the heading is the identifier).
- Link out to every action (and other API) referenced in prose, on first mention. Tempo Actions
link to
/tempo/actions/<namespace>.<action>; core Viem actions link to their/docs/...page.
- Module docs are concept-first. Each module has a concept landing page plus task/reference
sub-pages.
- Landing page (
index.mdx) is concept-first.- Start with
## Overview. - Explain what the module is, how it works, and when to use it.
- Include a minimal
ts twoslashusage example after the concept prose. - Follow with a
<Cards>grid linking to each succeeding sub-page. - Each
<Card>has a title, short description, icon, andto. - Do not add
## Recipesor reference sections here.
- Start with
- Sub-pages lead with prose, then drill into the API, containing in order:
## Overview; the concept for this task/function.## Recipes; independent, self-contained tasks.- See the Guides section for the recipe shape.
- Cover documented parameters and options through recipes.
- Demonstrate every meaningful parameter or notable option-bag field.
- Show why and how to reach for each option.
- Do not only restate the reference table.
- An API reference; add one
##section per exported function or type.- Name each section after the identifier.
- Example:
## `Account.from`. - Include
### Usage,### Parameters,### Return Value, and### Errorsas applicable.
- Landing page (
-
Keep the established Action page layout.
- Lead with purpose prose directly under the page title. Do not add
## Overviewor a separate## Actions.*reference section. - Add
## Usage, with### Standalone Actioninside it. - Add
## Recipesimmediately after Usage only when the page has additional tasks to show. - Follow with
## Return Value,## Parameters, and## Errorsas applicable.
- Lead with purpose prose directly under the page title. Do not add
-
List single-options-object fields directly.
- Use
### foofor each field. Do not add anoptionsheading or prefix field names withoptions.. - Use
#### foofor fields nested under another field.
- Use
-
These Action page rules override the Module Pages structure above. Apply the remaining reference content requirements at the heading levels defined here.
-
Reference sections open with a one-line description; place it under each function heading.
- Describe what the function does.
- Then add
### Usage. - Keep it terse and sourced from TSDoc.
-
### Parametersnever uses tables; list each parameter as its own heading.- List option-bag fields directly as qualified peer headings, such as
#### options.foo. Do not add a redundant#### optionsheading. - Preserve the full property path for nested fields, such as
##### options.rank.ping. - Add
- **Type:**and- **Default:**bullets when applicable. - Follow type bullets with prose.
- Reserve tables for
### Errors.
- List option-bag fields directly as qualified peer headings, such as
-
Each parameter heading includes a focused example; add a
ts twoslashsnippet.- Place it after the type bullets and prose.
- Cover every documented parameter and option field.
- Show the parameter in realistic use.
- Mark relevant lines with
// [!code focus]. - Keep snippets minimal.
- Hide imports and setup with
// ---cut---. - Parameter reference snippets stay terse.
-
### Return Valueis an inline code fence, not a bullet; put the type alone.- Place bare inline
`Type`directly under the heading. - Do not write
- **Type:** .... - Follow with a sentence describing it.
- Place bare inline
-
### Errorsis a table, not bullets; useErrorandDescriptioncolumns.- List concrete error classes.
- Do not list
.ErrorTypealiases. - Expand aliases to their underlying classes.
-
Don't restate the discriminant in prose; do not append parentheticals like
(type: 'json-rpc')after a concept name; the type is already shown in the usage example. -
Sidebar labels are short concept/task phrases; use fewer than 5 words.
- Use Title Case.
- Describe the concept or task.
- Do not merely echo the identifier.
- Always label the module landing page
Overview.
-
Paths are
/docs/<module>/<slug>; use lowercase, hyphenated paths.- The module landing page lives at
/docs/<module>. - Sub-pages use a short task or concept slug.
- Example:
/docs/chains/create. - Nest deeper only when a module has sub-groups.
- The module landing page lives at
- A guide's main body is a
## Recipessection: independent, self-contained tasks, each a###subheading with no enforced order. Do not use step-by-step "Walkthrough" sections. - Recipes use code focus.
- Show full imports in recipe code blocks.
- Do not use
// ---cut---in recipes. - Mark relevant option or parameter lines with
// [!code focus]. - Focus only the lines the recipe is about.
- Do not repeat client setup as its own recipe.
- Open Recipes with a prerequisite line linking to Getting Started.
- Example: "These recipes assume you have set up a Tempo client."
- Code examples still include a
viem.config.tstab. - Use
[!include ~/snippets/tempo/viem.config.ts:setup].
- Always show imports in code examples.
- Do not use
// ---cut---to hide imports. - Each
example.tsblock starts with imports. - Include
import { client } from './viem.config'when relevant. - Add a blank line before the example body.
- Do not use
- Guide section order:
## Overview→## Recipes→## Best Practices→## See More.