Seed Protocol

@seedprotocol/eas

Reference for @seedprotocol/eas, the low-level read client for the EAS GraphQL indexer: client registration, chain and endpoint configuration, schema lookups, Seed, Version and property queries, and the tool attestation helpers.

@seedprotocol/eas reads attestations from an EAS indexer's GraphQL API, by default https://optimism-sepolia.easscan.org/graphql. It returns raw attestations: Seeds, their Versions and their property attestations, with values still encoded in decodedDataJson. The SDK and @seedprotocol/query are built on it. Most apps should use @seedprotocol/query, which turns the same data into JSON items; reach for this package when you want the attestations themselves, for example to list one account's Seeds, check revocation or read history.

import { getSeedsBySchemaName, getItemVersionsFromEas } from '@seedprotocol/eas'
import { registerNodeEasPlatform } from '@seedprotocol/eas/node'

It only reads. Writing attestations is done by @seedprotocol/publish.

Install

npm install @seedprotocol/eas

It has no Seed dependencies and no peer dependencies, and on Node it needs 20.18.1 or later. Its undici dependency is loaded only by @seedprotocol/eas/node. @seedprotocol/query and the SDK install it for you, pinned to their own exact version, so in an app that has them, install it at their version (npm install @seedprotocol/eas@<that version>); a second copy makes @seedprotocol/query throw DuplicateEasModuleError (one version for every package).

Entry points

ImportWhat it has
@seedprotocol/easEvery query function, the model schema lookup, chain configuration, GraphQL documents, UID and value helpers, VERSION_SCHEMA_UID and VERSION_SCHEMA_DEF, tool attestation helpers and types, plus the fetch-based clients DefaultEasClient and DefaultQueryClient, ensureEasPlatform, createEasGraphQLClient and EAS_REQUEST_TIMEOUT_MS
@seedprotocol/eas/noderegisterNodeEasPlatform, the Node client classes NodeEasClient and NodeQueryClient, and createEasNodeDispatcher, EAS_NODE_ECDH_CURVE and EAS_NODE_KEEP_ALIVE_TIMEOUT_MS

There is no /react entry. When client.init runs, the SDK registers DefaultEasClient and DefaultQueryClient from this package (on Node, NodeEasClient instead of DefaultEasClient), and the functions on this page then work against them. Without the SDK, call ensureEasPlatform() (below), which works in browsers and Node.

registerNodeEasPlatform

Registers the Node GraphQL client and request cache that every query function uses.

function registerNodeEasPlatform(options?: { easClient?: NodeEasClientOptions }): void

Importing @seedprotocol/eas/node already registers them as a side effect, so calling the function is optional but makes the dependency explicit. Pass easClient options (NodeEasClientOptions, below) to change the client's connection settings. Each call registers a new client, replacing the one before. Without any registration, query functions throw EasClient not configured. Import from @seedprotocol/eas/node ….

The Node request cache shares one request between concurrent identical calls and keeps nothing once a request settles, so every call reads fresh data. A request the indexer doesn't answer within 30 seconds (EAS_REQUEST_TIMEOUT_MS) rejects with TimeoutError: EAS indexer request timed out after 30000 ms. Requests aren't retried.

NodeEasClient sends its requests through its own undici Agent, made on the first request, which offers only the X25519:P-256 TLS key exchange groups (EAS_NODE_ECDH_CURVE) and keeps idle connections open for 30 seconds (EAS_NODE_KEEP_ALIVE_TIMEOUT_MS). Node's own fetch closes idle connections after 4 seconds, and from Node 24.21 it also offers a post-quantum group that easscan doesn't support, which slows every new connection's handshake. The process's TLS defaults and fetch's global dispatcher aren't changed. No Agent is made under Bun or Deno, or when the host has replaced fetch's global dispatcher with something other than an Agent (a proxy agent, NODE_USE_ENV_PROXY, a MockAgent); requests then go through fetch's default. @seedprotocol/eas/node needs Node 20.18.1 or later on Node 20, and 22.3 or later on Node 22.

import { registerNodeEasPlatform } from '@seedprotocol/eas/node'
import { getSeedsBySchemaName } from '@seedprotocol/eas'

registerNodeEasPlatform()

const seeds = await getSeedsBySchemaName('post', 5)
for (const seed of seeds) console.log(seed.id, seed.attester, new Date(seed.timeCreated * 1000).toISOString())

NodeEasClientOptions:

NameTypeDefaultDescription
dispatcherDispatcher | falsean Agent from createEasNodeDispatcherYour own undici Dispatcher for EAS requests, or false to use fetch's global dispatcher
dispatcherOptions.ecdhCurvestring'X25519:P-256'TLS key exchange groups for the default Agent. Ignored when dispatcher is set
dispatcherOptions.keepAliveTimeoutMsnumber30000How long the default Agent keeps an idle connection, in milliseconds. Ignored when dispatcher is set

createEasNodeDispatcher(options?: { ecdhCurve?, keepAliveTimeoutMs? }) returns a promise that resolves to that Agent, or to undefined where none is made, so await it. It was synchronous before 0.7.13. It loads undici's Agent with a dynamic import from @seedprotocol/eas itself, so a bundler resolves undici from this package rather than from your app. NodeEasClient makes its dispatcher once, on the first request, and every request waits for it. To hold the Agent yourself, for example to close it on shutdown, make it and pass it in:

import { createEasNodeDispatcher, registerNodeEasPlatform } from '@seedprotocol/eas/node'

const agent = await createEasNodeDispatcher({ keepAliveTimeoutMs: 60_000 })
registerNodeEasPlatform({ easClient: { dispatcher: agent ?? false } })

export async function shutdown() {
  await agent?.close()
}
import { registerNodeEasPlatform } from '@seedprotocol/eas/node'

// Send EAS requests through fetch's global dispatcher, e.g. a proxy agent you installed
registerNodeEasPlatform({ easClient: { dispatcher: false } })

ensureEasPlatform

Registers the default fetch-based clients, DefaultEasClient and DefaultQueryClient, for whichever of the two slots has nothing registered yet. Clients already registered, such as the SDK's, are left in place. DefaultEasClient's requests have the same 30-second limit as the Node client's and use fetch's default connection handling. @seedprotocol/query calls it before its first read; on Node, query first registers a NodeEasClient when no EAS client is registered.

function ensureEasPlatform(): void

Use it instead of registerNodeEasPlatform when code may run next to the SDK or in a browser: registerNodeEasPlatform always replaces whatever is registered.

import { ensureEasPlatform, getSeedsBySchemaName } from '@seedprotocol/eas'

ensureEasPlatform()
const seeds = await getSeedsBySchemaName('post', 5)
console.log(seeds.length)

Chain and endpoint

Reads go to one chain's indexer, which defaults to OP Sepolia (chain ID 11155420), the testnet Seed Protocol runs on. Other chains have no Seed data, because Seed's contracts aren't deployed anywhere else.

const DEFAULT_EAS_CHAIN_ID = 11155420

function getEasReadChainId(): number
function getEasEndpoint(): string

getEasReadChainId returns the first of these that's set:

  1. The SDK's config.eas.chainId
  2. @seedprotocol/publish's chain
  3. The chain the SDK's local database already holds data from
  4. The EAS_CHAIN_ID or NEXT_PUBLIC_EAS_CHAIN_ID environment variable
  5. 11155420

The SDK and publish must agree: configuring different chains throws Seed Protocol chain mismatch.

getEasEndpoint returns, in order: the SDK's config.eas.indexerUrl; the EAS_ENDPOINT or NEXT_PUBLIC_EAS_ENDPOINT environment variable (also read from window.env in a browser); the chosen chain's known easscan indexer. It throws when the environment URL is a known indexer for a different chain, and when the chain has no known indexer and none is configured. The endpoint is read on every request, so configuration applied after import takes effect.

In a script without the SDK, set the environment variables:

# Read Base Sepolia's indexer instead
EAS_CHAIN_ID=84532 node read.mjs

# Use your own indexer for the default chain
EAS_ENDPOINT=https://eas-indexer.example.com/graphql node read.mjs

EAS_ENDPOINT is also exported as a constant. It's deprecated: it's read once at import and ignores SDK and publish configuration. Call getEasEndpoint() instead.

Known deployments

EAS_CHAIN_DEPLOYMENTS lists the chains with a known EAS deployment, keyed by chain ID. Each entry is an EasChainDeployment:

interface EasChainDeployment {
  chainId: number
  name: string
  easContractAddress: `0x${string}`
  schemaRegistryAddress: `0x${string}`
  indexerUrl?: string
  explorerUrl?: string
  testnet?: boolean
}
ChainIDIndexer
OP Sepolia (default)11155420optimism-sepolia.easscan.org
Ethereum, Sepolia1, 11155111easscan.org, sepolia.easscan.org
Optimism10optimism.easscan.org
Base, Base Sepolia8453, 84532base.easscan.org, base-sepolia.easscan.org
Arbitrum One42161arbitrum.easscan.org
Arbitrum Sepolia421614none: set an indexer URL
Polygon, Scroll, Linea137, 534352, 59144polygon, scroll, linea.easscan.org

OP Stack chains use the EAS predeploys, exported as OP_STACK_EAS_CONTRACT_ADDRESS (0x4200…0021) and OP_STACK_SCHEMA_REGISTRY_ADDRESS (0x4200…0020).

FunctionSignatureDescription
getEasChainDeployment(chainId: number) => EasChainDeployment | undefinedThe known entry, if any
resolveEasChainDeployment(chainId: number, overrides?: Partial<Omit<EasChainDeployment, 'chainId'>>) => EasChainDeploymentThe known entry merged with your overrides. Throws for an unknown chain unless you pass both contract addresses
getEasAttestationExplorerUrl(deployment: Pick<EasChainDeployment, 'explorerUrl'>, uid: string) => string | undefined{explorerUrl}/attestation/view/{uid}, or undefined without an explorer

configureEasReadChain, expectEasReadChain, isEasReadChainSettled, isEasReadChainConfigured, whenEasReadChainSettled and resetEasReadChain are how the SDK and publish package set the chain. Don't call them from app code; use SeedConfig.eas or the environment variables.

Schema lookups

Every Seed model has its own EAS schema, bytes32 <name>, where <name> is the model's EAS name (post, blog_post, html_page), and each property has a schema such as string title. Most queries need the schema's UID.

getEasSchemaUidForExactDefinition

The UID of the schema whose definition is exactly the string you pass, such as a property schema (string title) or a model's Seed schema (bytes32 post).

function getEasSchemaUidForExactDefinition(schemaDefinition: string): Promise<string | undefined>

It returns undefined when there's no such schema. Found UIDs are cached for the process, per EAS endpoint, and misses for five minutes (SCHEMA_LOOKUP_MISS_TTL_MS). A UID recorded with setSchemaUidForSchemaDefinition, as publish does for each schema it registers, answers without a request, before the indexer has the schema. It throws when the request fails.

import '@seedprotocol/eas/node'
import { getEasSchemaUidForExactDefinition } from '@seedprotocol/eas'

const postSchemaUid = await getEasSchemaUidForExactDefinition('bytes32 post')

Model schemas

A model's Seed schema can be published under more than one EAS name: SDKs before 0.7.4 wrote HTMLPage as htmlpage, and a model's nameMatch level decides which names it accepts. These functions find every Seed schema a model can match in one request.

type ModelSchemaLookup = { name: string; level?: NameMatchLevel }
type EasModelSchemaMatch = { schema: EASSchema; modelName: string; tier: NameMatchTier }

function getEasModelSchemasForNames(models: readonly ModelSchemaLookup[]): Promise<EasModelSchemaMatch[]>
function fetchEasModelSchemas(models: readonly ModelSchemaLookup[]): Promise<EASSchema[]>
function matchEasModelSchemas(schemas: readonly EASSchema[], models: readonly ModelSchemaLookup[]): EasModelSchemaMatch[]
function modelSchemaDefinition(modelName: string): string

fetchEasModelSchemas asks for every bytes32 <name> schema whose name is in easNamesToFetch for each model at its level ('fuzzy' by default; at 'exact' only bytes32 <easName>). matchEasModelSchemas says which model each schema is for and at which tier: the tightest tier wins, a tie between models matches nothing, and schemas no model matches are left out. A match at tier 'exact' is the schema new Seeds are published under; looser matches are schemas older SDKs published under. getEasModelSchemasForNames does both. They return [] when the clients aren't configured and throw when the request fails. modelSchemaDefinition(name) is bytes32 <easName(name)>.

import '@seedprotocol/eas/node'
import { getEasModelSchemasForNames } from '@seedprotocol/eas'

const matches = await getEasModelSchemasForNames([{ name: 'BlogPost' }, { name: 'HTMLPage', level: 'exact' }])
for (const m of matches) console.log(m.modelName, m.schema.schema, m.schema.id, m.tier)

EAS names

easName is the function the SDK and @seedprotocol/publish use to turn a model or property name into the name written to EAS (ADR 0008). It splits words where a lowercase letter or digit meets a capital and before the last capital of an acronym, turns any other run of non-alphanumeric characters into _, trims and lowercases.

function easName(name: string): string

easName('BlogPost')  // 'blog_post'
easName('HTMLPage')  // 'html_page'
easName('ID3Tag')    // 'id3_tag'
easName('New model') // 'new_model'

SDKs before 0.7.4 published acronyms without the split (HTMLPage as htmlpage). To read names written by any version, match instead of converting back:

ExportDescription
matchEasName(name, candidates)Matches an EAS name against local candidates ({ value, name, relation?, level? }) and returns { kind: 'match', value, tier }, { kind: 'ambiguous', values, tier } when several candidates tie in the best tier, or { kind: 'none' }
nameMatchTier(localName, name, { relation? })The tightest tier at which a local name and an EAS name match: 'exact' (name is exactly easName(localName): the local name Title matches title here, but matches an EAS name Title only at 'normalized'), 'normalized' (same lowercase letters and digits, so htmlpage matches HTMLPage), 'fuzzy' (also ignoring a trailing plural s and, for relations, a trailing id/ids), or undefined
localNamesInBestMatchTier(name, localNames, { relation?, level? })Every local name in the best tier, as a Set. level is one level for all names, or a function from a local name to its level
easNameVariants(name)The names an SDK may have published a local name under (its easName, the pre-0.7.4 form and the normalized key), for fetching schemas by name
easNamesToFetch(name, level?)The EAS names to fetch schemas under for a local model name: at 'exact' only its easName; at 'normalized' also easNameVariants(name) and name itself; at 'fuzzy' (the default) also the plural or singular of each. Filter what comes back with matchEasName
normalizedNameKey, fuzzyNameKey, isNameMatchAllowed, compareNameMatchTiersThe keys and tier comparisons the functions above use
NAME_MATCH_LEVELS, isNameMatchLevel(value)['exact', 'normalized', 'fuzzy'], tightest first, and a type guard for it
DEFAULT_NAME_MATCH_LEVEL'fuzzy'

Types: NameMatchLevel and NameMatchTier ('exact' | 'normalized' | 'fuzzy'), NameMatchCandidate<T>, NameMatchResult<T>. A schema file sets the level per model or property with nameMatch.

getSchemaNamesBySchemaUids

The registered names of schemas, by UID.

function getSchemaNamesBySchemaUids(schemaUids: string[]): Promise<Map<string, string[]>>

Schemas without a name are left out. Found names are cached for the process; resetSchemaNamesCache() clears the cache.

Reading Seeds

A Seed attestation is an item's permanent identity. Its id is the item's Seed UID, its schemaId is the model's schema, and its attester is the publisher.

getSeedsBySchemaName

Seeds of one model, newest first, with revoked Seeds left out.

function getSeedsBySchemaName(
  schemaName: string,
  limit?: number,
  skip?: number,
  options?: { uidPrefix?: string; attesters?: readonly string[] },
): Promise<Attestation[]>
NameTypeDefaultDescription
schemaNamestringThe schema's registered name, e.g. post. Case-sensitive
limitnumber10Maximum Seeds to return
skipnumber0Seeds to skip, for paging
options.uidPrefixstringnoneOnly Seeds whose UID starts with this. Matching is case-sensitive: pass lowercase with 0x
options.attestersreadonly string[]noneOnly Seeds attested by one of these addresses, in any case. Values that aren't 20-byte hex addresses are dropped; when none is left it returns [] without a request. Applies before limit and skip

This matches the schema's name, not its definition, so 'post' finds the Seed schema. The name belongs to the model, not to an app, so it returns every publisher's Seeds. Each result has schema.schemaNames set to the name you passed. getSeedUidsBySchemaName(schemaName, limit?) returns just the UIDs.

getSeedsFromSchemaUids

Seeds of the given schemas published by the given accounts, newest first.

function getSeedsFromSchemaUids(args: {
  schemaUids: string[]
  addresses: string[]
  excludeRevoked?: boolean // default true
}): Promise<Attestation[]>

Addresses are checksummed for you, and values that aren't 20-byte hex addresses are dropped, so a list of only such values matches nothing. The same goes for the address lists of getPublishedByFromEas, getDomainOwnershipFromEas and getPublishAuthorizationFromEas. There's no limit: it returns every match.

import '@seedprotocol/eas/node'
import { getEasSchemaUidForExactDefinition, getSeedsFromSchemaUids } from '@seedprotocol/eas'

const postSchemaUid = await getEasSchemaUidForExactDefinition('bytes32 post')
if (!postSchemaUid) throw new Error('No Post schema on this chain')

const seeds = await getSeedsFromSchemaUids({
  schemaUids: [postSchemaUid],
  addresses: ['0x1234567890abcdef1234567890abcdef12345678'],
})
console.log(seeds.length, 'posts from this publisher')

getSeedsByUidsFromEas

Seeds by UID, newest first, with schema.schemaNames filled in.

function getSeedsByUidsFromEas(args: { uids: string[]; excludeRevoked?: boolean }): Promise<Attestation[]>

excludeRevoked defaults to true. An empty list returns [] without a request.

Reading Versions and properties

A Version attestation references its Seed (refUID is the Seed UID), and each property attestation references its Version. Property values are in decodedDataJson, a JSON string of { name, type, signature, value } entries. Values that live on Arweave (Html bodies, files) are stored there as references, not inline; @seedprotocol/query resolves them for you.

The indexer gives an integer value as a JSON number or numeric string when it's small, and as { type: 'BigNumber', hex } when it's wide (uint256). Seed publishes a Number property as a decimal string and a Date as uint256 Unix seconds. These functions read them:

FunctionSignatureDescription
decodeEasNumber(value: unknown) => string | undefinedThe value as a decimal string. Accepts a finite number, a bigint, a decimal or 0x hex string, and { type: 'BigNumber', hex } or { _hex }. Integers stay exact ('9007199254740993'); other numbers come back as String(Number(value)) ('1.50' gives '1.5'). undefined for anything else, including '' and Infinity
isNumericEasType(easType: string | null | undefined) => booleanTrue for uint, int and their sized forms (uint8 … uint256, int8 … int256)
isEasEmptyValue(easType: string | null | undefined, value: unknown) => booleanTrue for the empty value Seed attests when a property is removed: '' for string, a zero bytes32. Treat the property as absent. An empty bytes32[] is an empty list, not a removal
isUnfilledReference(value: unknown) => booleanTrue for a reference slot a publish never filled: a 0x plus 64 hex digits string that's non-zero and below 2^32. Treat it as no related Seed. False for zero, which is a removal (isEasEmptyValue), for a full hash, and for anything that isn't such a string, including the number 1

getItemVersionsFromEas and getItemPropertiesFromEas

FunctionSignatureReturns
getItemVersionsFromEas(args: { seedUids: string[]; excludeRevoked?: boolean }) => Promise<Attestation[]>Every Version of those Seeds, newest first: attestations that reference one of the Seeds and use the Version schema (VERSION_SCHEMA_UID). Other attestations that reference a Seed, such as tool attestations, are left out
getItemPropertiesFromEas(args: { versionUids: string[]; excludeRevoked?: boolean }) => Promise<Attestation[]>Every property attestation of those Versions, newest first

excludeRevoked defaults to true. Edits published as patches add more property attestations to the same Version, so several can share a Version and schema.

Neither checks who attested. Anyone can make an attestation that references your Seed or Version, and it comes back alongside yours. Filter with keepAttestationsFromRefAttester (below), or use getCanonicalItemPropertiesFromEas, which does it for you.

getCanonicalItemPropertiesFromEas

One property attestation per Version and property schema: the newest non-revoked one from the right account.

function getCanonicalItemPropertiesFromEas(args: {
  versionUids: string[]
  excludeRevoked?: boolean // default true
  checkAttesters?: boolean // default true
}): Promise<Attestation[]>

With checkAttesters, a Version counts only if its Seed's attester made it, and a property only if its Version's attester did. That costs two more requests, for the Versions and their Seeds. A property whose Version or Seed the indexer doesn't return is dropped, since there's nothing to check it against. checkAttesters: false skips the check and the two requests, and keeps the newest attestation whoever made it, so another account's property on a trusted Version can still win.

pickLatestPropertyAttestationsByRefAndSchema(attestations, options?) makes the same newest-per-schema pick on a list you already have, without checking attesters. It skips revoked attestations; pass { ifAllRevoked: 'newestRevoked' } to keep the newest revoked one where every attestation for a schema is revoked.

FunctionSignatureDescription
keepAttestationsFromRefAttester(children, parents) => childrenKeeps a child (a Version, or a property attestation) unless its attester differs from the attester of the parent its refUID points to. Compared case-insensitively. A child whose parent isn't in parents, or where either attester is unknown, is kept
partitionByRefAttester(children, parents) => { kept, foreign }The same split, also returning the children it dropped

An item's current values, from its publisher only:

import '@seedprotocol/eas/node'
import { getItemVersionsFromEas, getCanonicalItemPropertiesFromEas } from '@seedprotocol/eas'

const seedUid = process.argv[2]!
const versions = await getItemVersionsFromEas({ seedUids: [seedUid] })
const properties = await getCanonicalItemPropertiesFromEas({ versionUids: versions.map((v) => v.id) })

for (const p of properties) {
  const [field] = JSON.parse(p.decodedDataJson) as { name: string; value: { value: unknown } }[]
  if (field) console.log(field.name, field.value.value)
}

getAttestationChangesSince

Attestations created or revoked after a time, for checking whether cached data is still current.

function getAttestationChangesSince(args: {
  refUIDs: string[]
  ids: string[]
  since: number // Unix seconds
}): Promise<AttestationChange[]>

type AttestationChange = { id: string; refUID: string; timeCreated: number; revocationTime: number }

It returns attestations that reference one of refUIDs (new or revoked Versions of a Seed, properties of a Version) or are one of ids (a revoked Seed), created or revoked after since. Long lists are split into requests of 400 UIDs. revocationTime is 0 when not revoked.

Attestation type

Query functions return the indexer's Attestation type (generated from the easscan schema). The fields Seed's queries select:

FieldDescription
idThe attestation UID
schemaIdThe schema UID
attesterThe account that made it
refUIDThe attestation it references (a Version's Seed, a property's Version)
decodedDataJsonThe decoded data, as a JSON string
timeCreatedUnix seconds
revoked, revocationTimeRevocation flag and Unix seconds (0 if not revoked)
isOffchainWhether the attestation is off-chain
txid, timeOn Version and property queries
schema.schemaNamesOn Seed queries only

EASSchema (the indexer's Schema type) and SchemaWhereInput are exported too.

GraphQL documents

The queries are exported as typed GraphQL documents, for your own requests through BaseEasClient.getEasClient().request(...): GET_SCHEMAS, GET_SCHEMA_BY_NAME, GET_SEEDS, GET_SEEDS_LEAN (without the slow schema join), GET_SEED_IDS, GET_VERSIONS, GET_PROPERTIES, GET_ALL_PROPERTIES_FOR_ALL_VERSIONS, GET_FILES_METADATA, GET_IMAGE_SEEDS, GET_IMAGE_VERSIONS, GET_STORAGE_TRANSACTION_ID, GET_ATTESTATION_CHANGES, GET_DOMAIN_OWNERSHIPS (DomainOwnership attestations, with expirationTime) and GET_PUBLISH_AUTHORIZATIONS (PublishAuthorization attestations, with expirationTime and revoked). Each takes a where: AttestationWhereInput (or SchemaWhereInput); withExcludeRevokedFilter(where) adds revoked: false to one.

import '@seedprotocol/eas/node'
import { BaseEasClient, GET_SCHEMAS } from '@seedprotocol/eas'

const { schemas } = await BaseEasClient.getEasClient().request(GET_SCHEMAS, {
  where: { schema: { equals: 'string title' } },
})
console.log(schemas.map((s) => s.id))

Client classes

ExportDescription
BaseEasClient (also EasClient)Holds the registered GraphQL client. getEasClient() returns a graphql-request GraphQLClient for the current endpoint; configure(impl) registers an IEasClient; isConfigured() says whether anything has
BaseQueryClientHolds the registered request cache. getQueryClient() returns an IQueryClient with fetchQuery, getQueryData and removeQueries; configure(impl) registers an IQueryClientFactory; isConfigured() says whether anything has
DefaultEasClient, DefaultQueryClientThe fetch-based implementations, for browsers and Node. ensureEasPlatform() registers them. DefaultEasClient builds its client with createEasGraphQLClient, so requests abort after 30 seconds
NodeEasClient, NodeQueryClient (from /node)The Node implementations, which extend the default ones. new NodeEasClient(options?: NodeEasClientOptions) sends requests through its own connection settings
createEasGraphQLClient(url, timeoutMs?, { dispatcher? }?)A graphql-request GraphQLClient for url whose requests abort after timeoutMs (default EAS_REQUEST_TIMEOUT_MS, 30000) with TimeoutError: EAS indexer request timed out after <timeoutMs> ms. A signal you pass to a request still aborts it, rejecting with the signal's reason (an AbortError for a plain abort()). dispatcher is passed to fetch on every request; a promise is awaited first, and a request aborted while it resolves isn't sent

To run these functions somewhere the SDK doesn't, implement IEasClient (getEasClient(): GraphQLClient) and IQueryClientFactory and pass them to configure. Build the GraphQLClient with createEasGraphQLClient(url) to keep the 30-second limit; a plain new GraphQLClient(url) has none. BaseEasClient.getSeedsBySchemaName is deprecated and always throws; use the getSeedsBySchemaName function.

Timing log

Set DEBUG=seedEas:timing (in a browser, localStorage.debug = 'seedEas:timing') to log every indexer request. It's off by default. Each request through createEasGraphQLClient logs request <operation> <filters> rows=<n> bytes=<n> <ms>ms, where <filters> gives the length of each id.in or refUID.in list, or (failed: <error name>) in place of the counts. Each lookup through the query functions' request cache logs fetchQuery <key> network|deduped|cache <ms>ms: network when it sent the request, deduped when it waited on the same request already in flight, cache when the cache answered.

seedEas:timing request GetSchemas rows=1 bytes=122 12ms
seedEas:timing request GetSchemas 504ms (failed: TimeoutError)

One copy of the package

The registered clients live in module state, and assertSingleEasModule() throws DuplicateEasModuleError when they were registered through another copy of this package; @seedprotocol/query calls it before every read. When two copies happen and how to fix it is in query's troubleshooting.

UID and address helpers

FunctionSignatureDescription
isValidEasAttestationUid(value: string | null | undefined) => booleanTrue for a 0x plus 64 hex digits UID that isn't zero
isPlaceholderUid(value: string | null | undefined) => booleanTrue for empty, 'NULL' or all-zero values
normalizeBytes32Hex(value: string | null | undefined) => stringLowercase and 0x-prefixed, for comparing UIDs. Takes exactly 64 hex digits, with or without 0x; anything else, including a shorter or non-hex value, gives ''
bytes32FromNumberHex(hex: string) => stringA BigNumber's hex (leading zeros dropped, such as 0x0abc) padded to bytes32, lowercase with 0x; '' when it isn't hex or is longer than 64 digits
checksumAddress(address: string) => stringEIP-55 checksum; returns the input unchanged if it isn't an address
checksumAddresses(addresses: readonly string[]) => string[]Trims each value, keeps the 20-byte hex addresses, checksums them and drops duplicates, as the query functions do for their in filters (the indexer compares addresses case-sensitively)
withExcludeRevokedFilter(where) => whereAdds revoked: { equals: false } to a where input

Constants: ZERO_ADDRESS, ZERO_BYTES ('0x') and ZERO_BYTES32, and the Version schema, VERSION_SCHEMA_DEF ('bytes32 version') and its UID VERSION_SCHEMA_UID (0x13c0fd59d69dbce40501a41f8b37768d26dd2e2bb0cad64615334d84f7b9bdf6, the same on every chain).

The schema UID cache the SDK uses (getEasSchemaUidForSchemaDefinition, getSchemaUidForSchemaDefinitionFromCache, setSchemaUidForSchemaDefinition, setSchemaUidForModel, getSchemaUidForModelFromCache, cachedSchemaLookup, clearSchemaLookupMisses, resetSchemaUidCaches) is exported but is SDK plumbing; app code doesn't need it. getEasSchemaUidForSchemaDefinition is getEasSchemaUidForExactDefinition returning undefined when the request fails, and getSchemaUidForSchemaDefinitionFromCache reads the same cache without a request. All three take the exact definition (string title).

Tool attestations

Tools that publish for users can add tool attestations next to the Seed: who published it (seedprotocol.publishedBy), a domain ownership claim (seedprotocol.domainOwnership), and a publish authorization grant (seedprotocol.publishAuthorization). This package has the read and verify side; @seedprotocol/publish writes them.

ExportDescription
getPublishedByFromEas({ toolAddresses, refUIDs?, schemaUid?, excludeRevoked? })PublishedBy attestations by the given tool wallets, optionally for given Seed UIDs
decodePublishedByData(data), hashPublishedByBatch(uids), verifyPublishedByBatch({ attestationUids, batchHash })Decode one, and check its batch hash
getDomainOwnershipFromEas({ toolAddresses, domains?, claimers?, schemaUid?, excludeRevoked? })DomainOwnership attestations, filtered by domain and claimer
decodeDomainOwnershipData, assessDomainOwnership, domainOwnershipTxtName, buildDomainOwnershipTxtValue, parseDomainOwnershipTxtValue, hashDomainOwnershipChallenge, hashDomainRegistryFingerprintDecode, assess (valid, expired, likely_transferred, stale, unknown) and the DNS TXT challenge helpers
getPublishAuthorizationFromEas({ identities?, sessionKeys?, apps?, schemaUid?, excludeRevoked? })PublishAuthorization grants
decodePublishAuthorizationData, assessPublishAuthorization, hashPublishAuthorizationMaterialDecode and assess (valid, revoked, expired, scope_mismatch, unknown). assessPublishAuthorization reports revoked only when you pass the attestation's revoked, and EAS expiry only when you pass its expirationTime; getPublishAuthorizationFromEas returns both
PUBLISHED_BY_SCHEMA_*, DOMAIN_OWNERSHIP_*, PUBLISH_AUTHORIZATION_*Schema definitions, names and defaults, including the domain ownership scopes DOMAIN_OWNERSHIP_SCOPE_REGISTRABLE ('registrable') and DOMAIN_OWNERSHIP_SCOPE_HOSTNAME ('hostname')

Domain ownership covers the flow.

On this page