Seed Protocol
@seedprotocol/query

Overview

Reference for @seedprotocol/query: queryBySchema, getSeed, getSeeds, getSeedAttestations, pinned reads and the other exports, the SeedRecord shape, paging and UID prefixes, schema names, caching and frozen records, and the environment variables it reads.

@seedprotocol/query reads published Seed items as JSON, without the SDK's local database, a wallet or a schema file. For each Seed it takes the latest Version, or the one a pinned read names, then the newest attestation of each property, expands relations and fetches Html bodies stored on Arweave. @seedprotocol/feed builds on it, and the SDK uses it for getPublishedSeedRecord.

import { queryBySchema, getSeed, getSeeds, getSeedAttestations, type SeedRecord } from '@seedprotocol/query'

For a walkthrough, see Read published data. Data sources (remote, local, auto) are on Query adapters.

Install

npm install @seedprotocol/query

It brings @seedprotocol/eas (attestation reads) and @seedprotocol/arweave (content reads) with it, pinned to its own exact version. On Node it needs 20.18.1 or later.

EntryResolves toAdds
@seedprotocol/query in Node and Bundist/index.node.jsA disk cache (FileCache) under CACHE_DIR
@seedprotocol/query in a browser bundle (browser condition)dist/index.jsMemory cache only, no fs
@seedprotocol/query/nodedist/node.jsOnly initializeQueryPlatform, teardownQueryPlatform, isQueryPlatformInitialized and FileCache

The main entry exports the same API in both builds; Query adapters lists what differs.

queryBySchema

Lists a schema's published items, newest first by Seed attestation time.

function queryBySchema(
  schemaName: string,
  options?: QueryBySchemaOptions,
): Promise<QueryBySchemaResult>

Options

NameTypeDefaultDescription
limitnumber100Seeds to list. Seeds with no Version from their attester, or only revoked ones, are dropped after listing, so a page can hold fewer items than limit, even none, while later pages still have more. listed says when the listing has ended
skipnumber0Seeds to skip, for paging
uidPrefixstringnoneOnly Seeds whose UID starts with this. See UID prefixes
attestersreadonly string[]noneOnly Seeds attested by one of these accounts. See One publisher's items
relations'expand' | 'uids''expand''expand' replaces each relation's UID with the related item's values, or an Html or File relation's with its Arweave URL on the first read gateway (https://arweave.net/raw/<txId> by default). 'uids' leaves every relation a UID and reads no related items, which saves their requests. Either way a relation comes back under the same key. Any other value throws a TypeError before any request
hydrateStoragebooleantrueFetch Html bodies and other text stored on Arweave into the value. false leaves gateway URLs and skips those requests
cachebooleanThe CACHE_ENABLED settingfalse skips the cache for this call. Only remote reads use the cache
source'remote' | 'local' | 'auto''remote'Where to read from. See Query adapters
maxAgenumberCACHE_MAX_AGE, else 0Seconds after this process last checked the collection during which it's returned from the cache without any request. 0 checks on every call. Only where the collection cache applies

relations replaced expandRelations in 0.7.9. An expandRelations option passed from untyped code is ignored, so relations come back expanded.

Returns

type QueryBySchemaResult = {
  items: SeedRecord[]
  limit: number
  skip: number
  listed: number // Seeds the listing returned for this page, before any were left out
  etag?: string  // only for a cached remote read of the first page (skip: 0, no uidPrefix or attesters)
}

items is a new array each call, but the records in it may be the cache's own objects. See Frozen records.

items can be shorter than listed, so page on listed, not on items.length. listed < limit means the listing has ended. listed === limit doesn't promise more: when the last page is exactly full, the next one comes back with listed: 0.

What's left out:

  • Revoked Seeds, Seeds with no Version from their attester, and Seeds whose Versions were all revoked.
  • Seeds of other schemas, however similar the name.
  • Seeds of other accounts, when you pass attesters. Without it every account's Seeds are listed; see One publisher's items.
  • Versions not attested by the Seed's attester, and property attestations not attested by their Version's attester. Anyone can make an attestation that references your Seed or Version, so these are dropped before the latest Version and the newest value of each property are picked. Attesters are compared case-insensitively, and an attestation whose attester (or parent's attester) is unknown is kept. getSeed and its changelog apply the same rule.
  • Unfilled references: a relation value that's a non-zero bytes32 below 2^32, which a publish left in a slot it never filled (isUnfilledReference). A relation holding one is absent, a list drops it and keeps its other members, and neither is read as a related Seed. getSeed and getSeeds do the same; changelogs and getSeedAttestations keep the value.

Example

import { queryBySchema } from '@seedprotocol/query'

const limit = 20

for (let skip = 0; ; skip += limit) {
  const { items, listed } = await queryBySchema('post', { limit, skip })

  for (const post of items) {
    if (typeof post.data.title === 'string') console.log(post.seedUid, post.data.title)
  }

  if (listed < limit) break
}

getSeed

Reads one item by its Seed UID, at its latest Version or a pinned one, optionally with its edit history.

function getSeed(seedUid: string, options?: GetSeedOptions): Promise<GetSeedResult | null>

type GetSeedResult = SeedRecord & { readonly changelog?: readonly ChangelogEntry[] }

It returns null when the UID is empty, there's no such Seed, the Seed has no Version from its attester, the Seed has no schema name, every Version was revoked, or the Seed has no Version that matches the pin. The UID is matched in any case and the record's seedUid is lowercase.

Options

relations, hydrateStorage, cache and source work as for queryBySchema. Also:

NameTypeDefaultDescription
versionUidstringnoneRead the Seed at this Version, in any case. See Pinned reads
asOfnumbernoneRead the Seed as it was at this Unix time (seconds). See Pinned reads
include'data' | 'data+changelog' | 'changelog''data''data+changelog' adds changelog. 'changelog' returns only the history, with data: {}
changelog.granularity'version' | 'property''version'One entry per Version, or one per property attestation
changelog.sincenumbernoneLeave out entries before this Unix time (seconds)
changelog.limitnumbernoneKeep only the newest this many entries

Pinned reads

versionUid and asOf read a Seed at one of its Versions instead of the latest:

  • versionUid alone: that Version, with the newest attestation of each property on it. A patch publish adds property attestations to the current Version, so this includes patches made after the Version was created.
  • asOf alone: the newest Version created at or before asOf, with only the property attestations made by then.
  • Both: the named Version, if it was created by asOf, with the property attestations made by then.

A revoked Version, a Version of another Seed, or one created after asOf returns null. Relations are expanded and storage is hydrated as for the latest Version, but related items are read at their latest Version. The changelog isn't pinned: 'data+changelog' returns pinned data with the full history, and 'changelog' ignores the pin.

import { getSeed } from '@seedprotocol/query'

const seedUid = '0x6e195761cf9fce0a81ad24e23dbafce37e2b2d2099357d634acafebc174778f6'
const asPublished = await getSeed(seedUid, { versionUid: '0xd76fcd99122c77e9f1f31e1dc16b4dfa66b8137533a04488f8e71b8ea928773d' })
const beforeTheEdit = await getSeed(seedUid, { asOf: Date.parse('2026-02-09T13:55:00Z') / 1000 })

Changelog entries

Entries are in chronological order, oldest first, also after limit.

type VersionChangelogEntry = {
  type: 'version'
  at: number                       // Unix seconds
  versionUid: string
  before: Record<string, unknown>  // the previous Version's values
  after: Record<string, unknown>   // this Version's values
  changedKeys: string[]            // sorted
}

type PropertyChangelogEntry = {
  type: 'property'
  at: number
  versionUid: string
  property: string                 // the key it has in data, e.g. 'html' or 'author'
  attestationUid: string
  previousValue: unknown
  nextValue: unknown
}

before and after use the same keys as data. A removed property shows as a change to undefined in property entries and is missing from a Version entry's after. Changelog values are flat: relations stay UIDs and Arweave text isn't fetched. Each value is a string, or an array of UIDs for a list of relations, in the same forms as data, and an unfilled reference stays in as a value. Property granularity also shows edits published as patches to the same Version, which version granularity can't.

Example

import { getSeed } from '@seedprotocol/query'

const post = await getSeed('0xfd8c50ca131b725c288be7f46e4abe0c7b06f1226e98f552642edde8280ad900', {
  include: 'data+changelog',
  changelog: { granularity: 'property', limit: 10 },
})

for (const entry of post?.changelog ?? []) {
  if (entry.type === 'property') console.log(new Date(entry.at * 1000).toISOString(), entry.property)
}

getSeeds

Reads many items by Seed UID, of any schemas, in one set of indexer requests instead of one per item.

function getSeeds(seedUids: readonly string[], options?: GetSeedsOptions): Promise<Map<string, SeedRecord>>

type GetSeedsOptions = AssembleOptions & { pins?: ReadonlyMap<string, VersionPin> }
type VersionPin = { versionUid?: string; asOf?: number }

The map is keyed by each UID as you passed it, in input order. UIDs are matched in any case, so a repeated UID is read once, under its first spelling. Empty strings are skipped.

UIDs that getSeed would return null for are left out of the map: no such Seed, a revoked Seed, no Version from its attester, no schema name, every Version revoked, or no Version that matches the pin.

Options

relations, hydrateStorage, cache and source work as for queryBySchema. pins maps a Seed UID (in any case) to a { versionUid, asOf } pin, read as in Pinned reads; Seeds without a pin are read at their latest Version. There's no include, so for a changelog call getSeed.

getSeeds shares the item cache with getSeed: a record either one stored is returned to the other, and the cached Seeds in one call are checked for changes in a single request. Under source: 'auto', Seeds the local source doesn't have are read from EAS; see Query adapters.

Example

import { getSeeds } from '@seedprotocol/query'

const first = '0x6e195761cf9fce0a81ad24e23dbafce37e2b2d2099357d634acafebc174778f6'
const second = '0x2a34bcc7d384ee1a0999affae1b4712cb538ee0a1e237de3b748d3dba2395a53'

const posts = await getSeeds([first, second], {
  pins: new Map([[first, { versionUid: '0xd76fcd99122c77e9f1f31e1dc16b4dfa66b8137533a04488f8e71b8ea928773d' }]]),
})

for (const [seedUid, post] of posts) console.log(seedUid, post.versionUid, post.data.title)

getSeedAttestations

Reads many items with the property attestations they're assembled from, keeping the EAS name and EAS type that a SeedRecord doesn't. Use it when you need the on-chain types, for example to export items to another format.

function getSeedAttestations(
  seedUids: readonly string[],
  options?: Pick<GetSeedsOptions, 'source' | 'pins'>,
): Promise<Map<string, SeedAttestations>>

type SeedAttestations = {
  readonly record: SeedRecord // assembled with relations: 'uids' and no storage hydration
  readonly properties: readonly SeedPropertyAttestation[]
}

type SeedPropertyAttestation = {
  readonly attestationUid: string
  readonly schemaUid: string // the one-field EAS schema, '<easType> <easName>'
  readonly easName: string   // e.g. 'title', 'author_identity_ids'
  readonly easType: string   // e.g. 'string', 'bytes32', 'bytes32[]', 'bool', 'uint256'
  readonly value: string | boolean | readonly string[]
  readonly isRelation: boolean
  readonly attester?: string
  readonly timeCreated: number
}

The map follows getSeeds: keyed by each UID as you passed it, with the UIDs getSeed would return null for left out. source and pins work as they do there. It never reads or writes the query cache.

properties holds the newest attestation of each property on the Version read. value is a decimal string for integer types, a boolean for bool, a string array for array types, and otherwise the attested string. isRelation is the rule data uses. A removed property is left out, but an unfilled reference stays, with isRelation: true, so check relation values with isUnfilledReference before you treat them as Seed UIDs.

import { getSeedAttestations } from '@seedprotocol/query'

const seedUid = '0xfd140023f3a01376f350e9e5b883c729139e9333827ac48af2f23d6591068d98'
const seeds = await getSeedAttestations([seedUid])

for (const property of seeds.get(seedUid)?.properties ?? []) {
  console.log(property.easType, property.easName, property.isRelation)
}

SeedRecord

type SeedRecord = {
  readonly seedUid: string     // the Seed attestation UID, the item's permanent ID
  readonly schemaName: string  // e.g. 'post'
  readonly attester?: string   // the account that attested the Seed (checksummed)
  readonly timeCreated: number // Seed attestation time, Unix seconds
  readonly versionUid: string  // the Version the values come from
  readonly data: SeedData      // { readonly [key: string]: unknown }
}

What's in data:

  • The newest value of each property on the latest Version, or on the pinned one. A property whose newest attestation is the empty value, '' for a string or all zeros for a bytes32, is left out: that's how a publisher removes a property.
  • Each property under its EAS name (published_at), plus a camelCase copy (publishedAt) when that's different. data also repeats seedUid, attester, timeCreated and versionUid.
  • Relations under their schema key, in both relations modes and in changelogs. A relation is a bytes32 or bytes32[] property; an attestation with no type counts when its EAS name ends in _id or _ids. author_id comes back as author, and a list's author_identity_ids as authors, each with its camelCase copy when different (feature_image_id as feature_image and featureImage). A string external_id isn't a relation, so it stays external_id and externalId. A relation holding an unfilled reference is absent.
  • List keys from the first word of the EAS name, unless you set your models. A list's EAS name runs the singular key, the related model and ids together, so query takes the first word and makes it plural: co_author_identity_ids comes back as cos, featured_portfolio_clip_ids as featureds. With models set, they come back as co_authors and coAuthors, and as featured.
  • Relations expanded into the related item's values (with the default relations: 'expand'); a list becomes an array of objects. Only relations expand, so a string value that equals a Seed's UID stays a string.
  • Html and other text stored on Arweave fetched into the value (with hydrateStorage), from the first read gateway that has it. Arweave links inside the body (src, href, poster, srcset) are moved onto the first read gateway, so they load from the gateway this process reads from; see Arweave and gateways.
  • Marker keys _feedFieldStorageModels and _feedListElementStorageModels, which record which fields held Html, File or Json content. The feed package uses them to pick an item's body.

Apart from relations and lists, property values are strings as attested. An integer (uint8 to uint256, int8 to int256 on EAS) reads as its decimal string, exact at any size ('9007199254740993'). A Date is published as a uint256 of Unix seconds, so it reads as '1700000000', not an ISO date; the SDK converts it when it syncs, query doesn't. Any other non-string value is its JSON: a bool reads as 'true'.

Every app that publishes to a schema decides its own properties, so check a field's type before you use it.

When hydrateStorage can't fetch a body, it leaves the field as the gateway URL (https://arweave.net/raw/<txId> with the default gateway) without an error. It asks each read gateway in turn, skipping any that failed a read in the last 60 seconds unless all have, and moves on when a gateway doesn't answer, returns an error status or takes over 15 seconds. A 4xx other than 429 (a 404, say) only means that gateway doesn't have the body, so it doesn't count as a failed read; 5xx, 429, timeouts and network errors do. The body is requested in the stored link's form, {gateway}/raw/<txId> or {gateway}/<txId>. It stops without a body when a gateway answers with an image, video or audio content type, or with a body over 8 MB, empty, or containing a NUL character. With the cache on, the record keeps the URL until CACHE_TTL runs out. Arweave and gateways says how the read gateway list is built and how to change it.

Schema names

The schema name is the model's EAS name, as easName from @seedprotocol/eas builds it: Post is published under post, BlogPost under blog_post, HTMLPage under html_page. It's matched exactly and case-sensitively, so queryBySchema('Post') returns nothing. SDKs before 0.7.4 published names with acronyms or digits differently (HTMLPage as htmlpage), and those items are only found under the old name.

The name belongs to the model, not to your app: post returns every app's post items. The data model explains why.

UID prefixes

uidPrefix finds Seeds whose UID starts with some hex digits, for short links like /posts/fd8c50ca.

  • At least 4 and at most 64 hex digits, with or without 0x, in any case.
  • Anything else returns no items and listed: 0 without a request.
  • Otherwise it filters like a normal listing (schema, not revoked) and pages with limit and skip. It never uses the collection cache.

normalizeUidPrefix applies the same rules:

function normalizeUidPrefix(prefix: string): string | null

normalizeUidPrefix('FD8C50CA') // '0xfd8c50ca'
normalizeUidPrefix('0xfd8')    // null: fewer than 4 digits

One publisher's items

Without attesters, queryBySchema lists every account's Seeds of a schema; each Seed's values still come only from its own attester (see What's left out). Pass attesters to list only some accounts' Seeds:

import { queryBySchema } from '@seedprotocol/query'

const { items } = await queryBySchema('post', {
  attesters: ['0xE870B14fCfF4A408e6eCb65Bd40f64d08859Efb3'],
  limit: 20,
})

Addresses are matched in any case, and duplicates count once. Entries that aren't addresses are dropped; if none is left, or the list is empty, you get no items and listed: 0 without a request. The filter applies before limit and skip, so other accounts' Seeds don't shorten a page, and it combines with uidPrefix. Like uidPrefix, it never uses the collection cache. normalizeAttesters(attesters) returns the list as query uses it (lowercase, 0x plus 40 hex digits, no duplicates), or null when no entry is an address.

To limit every read to one account, whatever the call passes, register a custom source, as in Query adapters.

queryBySchemaForMonth

Every item of a schema whose Seed was attested in one calendar month, up to 1,000. The feed package uses it for archives.

function queryBySchemaForMonth(
  schemaName: string,
  year: number,
  month: number, // 1–12
  options?: AssembleOptions, // relations, hydrateStorage, cache, source
): Promise<SeedRecord[]>

The month runs from 00:00 UTC on its first day up to 00:00 UTC on the next month's first day, whatever the process's time zone. It writes items to the item cache but has no collection cache.

initializeQueryPlatform

Fills in whatever the process is missing to read remotely. Clients that are already registered, for example by a running SeedClient, are kept as they are. queryBySchema, getSeed, getSeeds and queryBySchemaForMonth call it themselves, so you only need it to choose the Arweave gateway or set models.

  • EAS, in Node: registers NodeEasClient from @seedprotocol/eas/node if no EAS client is registered, then ensureEasPlatform() fills the query-client slot if it's empty.
  • EAS, in the browser build: ensureEasPlatform() registers the default fetch-based clients for whichever of the two client slots is empty.
  • Arweave, in Node: registers the Node Arweave client with the gateway below, if no Arweave client is registered.
  • Arweave, in the browser build: only sets the preferred read gateway, if no Arweave client is registered. It doesn't load the Node clients.
  • arweaveDomain, when you pass it, always becomes the Arweave host, even when a client was already registered. It also locks the host: the read-gateway probe won't switch away from it, and ARWEAVE_HOST no longer overrides it.
  • One copy of each package: every call, including the ones the read functions make, throws DuplicateEasModuleError or DuplicateArweaveModuleError when another copy of @seedprotocol/eas or @seedprotocol/arweave registered the clients, unless source is 'local'. See Troubleshooting.
function initializeQueryPlatform(options?: InitializeQueryPlatformOptions): Promise<void>
function teardownQueryPlatform(): Promise<void>
function isQueryPlatformInitialized(): boolean
OptionDefaultDescription
arweaveDomainNEXT_PUBLIC_ARWEAVE_HOST, ARWEAVE_HOST or VITE_ARWEAVE_HOST, else arweave.netGateway for Arweave bodies and URLs. The default only applies when query registers the Arweave client itself
sourcenoneThe source the caller reads from. 'local' skips the one-copy check. The read functions pass their own source
modelsnoneYour schema file's models, as for setQueryModels. null clears them; leaving it out keeps what's set

The first call does the setup, and later calls skip it, so a later arweaveDomain has no effect. models and the one-copy check are the exceptions: they apply on every call. teardownQueryPlatform clears the flag and the models, so the next call runs the setup again. Because query keeps a registered client, calling it after @seedprotocol/feed's initializeFeedPlatform({ arweaveDomain }) keeps feed's gateway.

setQueryModels

Gives query your models, so List relations come back under their schema keys instead of the first word of their EAS name. initializeQueryPlatform({ models }) does the same.

function setQueryModels(models: QueryModels | null): void

QueryModels takes the models of a schema file imported as JSON:

import { initializeQueryPlatform } from '@seedprotocol/query'
import schema from './seed.schema.json'

await initializeQueryPlatform({ models: schema.models })

A value typed SchemaFileFormat['models'] from the SDK fails to type-check against QueryModels with TS2345, because the SDK's property type shares no declared field with query's. It works at run time, so cast it: models as QueryModels.

Each call replaces the models set before, and they stay set for the process until another call, null or teardownQueryPlatform(). Query reads each property whose type or dataType is List, in any case, and takes the related model from ref, refModelName or items.model. models and properties can be objects keyed by name, or arrays with a name on each entry.

A list is looked up in the model matching the Seed's schema name, then across every model given if they agree on its key. Anything else keeps the first-word key.

Caching

Remote reads are cached in memory and, in Node, on disk under CACHE_DIR (collections/ and items/). Reads from the local source are never cached.

  • Collection cache: remote queryBySchema calls with skip: 0 and no uidPrefix or attesters, per schema and options. Each call fetches the Seed list (it shows new and revoked Seeds) and, at the same time, asks EAS whether anything a cached item depends on changed: new or revoked Versions, property edits, and the items it relates to. Only new or changed items are assembled again.
  • maxAge: with maxAge or CACHE_MAX_AGE above 0, a collection this process checked less than that many seconds ago is returned without any request, so new, changed and revoked Seeds show up to that many seconds late. The time is kept in memory per schema, options and limit, and any checked call starts it, including one made without maxAge.
  • Item cache: remote getSeed and getSeeds, keyed by Seed UID, options (relations included) and pin, so a pinned read and a latest read are cached apart. Checked for changes the same way on each hit. Other remote reads write their items into it.
  • relations: 'uids' records depend only on their own Seed and Version, so an edit to a related item doesn't invalidate them.
  • Models: the models set are part of the key, so records cached without them aren't read with them.
  • TTL: entries are rebuilt from scratch CACHE_TTL seconds after their last full assembly, which catches changes the check can't see, like edits to a relation's relations.
  • ETag: the collection cache's etag comes from the content, so it changes with any edit.
  • Concurrent calls for the same schema, options and limit share one refresh.
  • Arweave bodies are cached in memory by transaction ID and link form whether or not the cache is on, up to about 32 million characters.

Cache keys change when a record's shape does, so collections and items cached by an earlier release, on disk included, aren't read, and the first reads after an upgrade assemble every record from scratch.

Pass cache: false to skip the cache for one call. To drop it from code:

import { getQueryCacheManager } from '@seedprotocol/query'

await getQueryCacheManager().clearCollection('post') // every cached listing of 'post'
await getQueryCacheManager().clearAll()             // everything, including feed's cached documents in a shared CACHE_DIR

Frozen records

Records from a cached read (and from a fresh skip: 0, getSeed or getSeeds read that was just stored) are the cache's own objects, shared with every later caller.

  • Unless NODE_ENV is production, they're deep-frozen, so assigning to post.data.title throws a TypeError.
  • In production they aren't frozen, and a write silently changes what later callers get.
  • In a browser bundle process.env usually isn't there, so records are always frozen.

Copy before you change anything, nested values included: structuredClone(post.data).

Environment variables

Query reads these when its cache manager is first created, once per process:

VariableDefaultWhat it does
CACHE_ENABLEDOn, except when NODE_ENV=developmentfalse, 0, no or off turns caching off. true, 1 or yes turns it on, including in development
CACHE_TTL3600Seconds before an entry is rebuilt from scratch
CACHE_DIR./cacheDisk cache directory, relative to the working directory (Node only). Shared with @seedprotocol/feed
NODE_ENVdevelopment turns caching off by default. Anything but production freezes cached records
CACHE_MAX_AGE0Default for queryBySchema's maxAge: seconds a checked collection is returned without a request

And through @seedprotocol/eas and @seedprotocol/arweave, on each request:

VariableWhat it does
EAS_CHAIN_ID (or NEXT_PUBLIC_EAS_CHAIN_ID)The chain to read, and with it that chain's easscan indexer. Default OP Sepolia. The SDK's or publish's chain config takes precedence when they run in the same process
EAS_ENDPOINT (or NEXT_PUBLIC_EAS_ENDPOINT)An EAS GraphQL indexer URL, e.g. one you run. A URL that is a different chain's known indexer throws
NEXT_PUBLIC_ARWEAVE_HOST, ARWEAVE_HOST, VITE_ARWEAVE_HOSTPrimary Arweave gateway, first match wins. Default arweave.net
ARWEAVE_READ_GATEWAYS (or NEXT_PUBLIC_ARWEAVE_READ_GATEWAYS)Comma-separated fallback gateways, tried after the primary one for bodies and other Arweave reads

Without any of these, reads go to https://optimism-sepolia.easscan.org/graphql. See EAS indexing.

Lower-level exports

Most apps only need the functions above. The package also exports the pieces they're built from:

AreaExports
SourcesregisterLocalQuerySource, clearLocalQuerySource, getRegisteredLocalQuerySource, hasLocalQuerySource, createRemoteQueryDataSource, getRemoteQueryDataSource, resolveQuerySource, normalizeSourceMode, types QueryDataSource, SeedListFilter, ResolvedQuerySource, AttestationLike, AttestationChange. See Query adapters
AssemblyassembleSeeds(schemaName, seeds, options?, dataSource?): Seed attestations to SeedRecord[]. assembleSeedChangelog(seedUid, options?, dataSource?): { latestVersionUid, changelog }
ChangelogdiffVersionSnapshots, diffPropertyAttestations, applyChangelogFilters, decodePropertyAttestation, buildFlatSnapshotFromProperties, changelogValuesEqual. decodePropertyAttestation, buildFlatSnapshotFromProperties and diffPropertyAttestations take an optional last modelName, the Seed's schema name, which picks a list's key from the models set
CachegetQueryCacheManager, resetQueryCacheManager, createQueryCacheManager(config?, persistent?), CacheManager, loadQueryCacheConfig, buildAssembleOptionsKey, FileCache (Node entry), types QueryCacheConfig, QueryCacheStats, PersistentCache, CachedCollectionData, CachedItemData
Arweave contenthydrateArweaveRichTextInItems(items, options?), isArweaveTransactionGatewayUrl (true for /raw/<txId> on any host, and /<txId> on a known or configured gateway), resetArweaveBodyCache, enrichImageSeedClone. getArweaveUrlForTransaction(txId) is @seedprotocol/arweave's, re-exported: {first read gateway}/raw/{txId}
FiltersnormalizeUidPrefix (see UID prefixes), normalizeAttesters (see One publisher's items)
Property namesparseEasPropertyMetadata, parseEasRelationPropertyName, publicListRelationPropertyKey, stripListRelationStorageAliasesForPublicKey, tryCoerceJsonStringArray
Rich-body markersRICH_BODY_STORAGE_SCHEMAS, isRichBodyStorageSchema, richBodyModelPriority, getFieldStorageModels, setFieldStorageModel, getListElementStorageModels, setListElementStorageModels
Other typesSeedData, QuerySourceMode, AssembleOptions, GetSeedOptions, GetSeedsOptions, VersionPin, ChangelogInclude, ChangelogOptions, ChangelogEntry, VersionChangelogEntry, PropertyChangelogEntry, QueryBySchemaOptions, InitializeQueryPlatformOptions, RelationsMode, QueryModels, SeedAttestations, SeedPropertyAttestation

DEFAULT_ARWEAVE_HOST ('arweave.net') is re-exported from @seedprotocol/arweave.

Troubleshooting

DuplicateEasModuleError or DuplicateArweaveModuleError

DuplicateEasModuleError: Two copies of @seedprotocol/eas are loaded, and EAS clients were registered on the other copy (e.g. by SeedClient). …

Something, usually a running SeedClient, registered the EAS or Arweave clients through a different copy of the package than the one query imports. Reading through query's copy would use other clients, and possibly another chain, so every queryBySchema, getSeed, getSeeds, queryBySchemaForMonth and initializeQueryPlatform call throws until there's one copy. Reads with source: 'local' skip the check, because they use neither client; 'auto' reads don't.

The Seed packages pin each other to exact versions, so an install where every @seedprotocol/* package is at the same version has one copy of each. Two copies still happen when:

  • A Seed package is at another version. It brings its own eas and arweave, for example @seedprotocol/query left a release behind the SDK. Install every @seedprotocol/* package at the same version, as the toolkit describes.
  • A bundler loads the same files as two modules. With Vite, see @seedprotocol/vite troubleshooting.

To check, run npm ls @seedprotocol/eas @seedprotocol/arweave: each should be at one version, with every repeat marked deduped.

TimeoutError

A remote read rejects with TimeoutError when an indexer request gets no answer within 30 seconds. It isn't retried. Check that the indexer reads go to is up; EAS indexing says which one that is. hydrateStorage's Arweave fetches have their own 15-second limit and leave the URL instead of failing.

On this page