Seed Protocol
Contracts

Base schemas

The EAS schemas Seed Protocol relies on beyond your own models: their definitions and UIDs, which ones must exist before anyone can publish, and which the SDK registers on demand.

Every Seed attestation is made under an EAS schema. Most of those schemas come from your models and properties, and the publish package registers them as it needs them. A few belong to the protocol itself: every publish uses them, whatever the app.

Schema UIDs are the same on every chain

An EAS schema's UID is keccak256(abi.encodePacked(schema, resolver, revocable)). It doesn't depend on the chain or on who registered it. Every schema on this page is registered with no resolver (the zero address) and as revocable, so each one has the same UID on every chain where it's registered. Registering a schema is permissionless, and a schema can't be changed or removed once it's registered.

Revocable matters: the Seed contracts make every attestation revocable, and EAS rejects a revocable attestation under an irrevocable schema. See Security and session keys.

Protocol schemas

These three are listed in schemas/base-schemas.json in the contracts repo. The OP Sepolia status on this page was read on 2026-10-18 from the SchemaRegistry (getSchema) and OP Sepolia's EAS Scan.

KeySchemaUIDOP Sepolia
versionbytes32 version0x13c0fd59d69dbce40501a41f8b37768d26dd2e2bb0cad64615334d84f7b9bdf6Registered, named version
schemaNamebytes32 schemaId,string name0x44d562ac1d7cd77e232978687fea027ace48f719cf1d58c7888e509663bb87fcRegistered (EAS schema #1, "Name a Schema")
storageTransactionIdstring storage_transaction_id0x55fdefb36fcbbaebeb7d6b41dc3a1a9666e4e42154267c889de064faa7ede517Registered, named storage_transaction_id
  • version is the schema of every Version attestation. multiPublish creates one per publish with refUID set to the Seed, and its data is the Version schema's own UID (abi.encode(versionSchemaUid)). The SDK exports the UID as VERSION_SCHEMA_UID.
  • schemaName is EAS's schema for naming schemas. The SDK names every schema it registers with an attestation under it (schemaId, name), and finds model schemas by those names.
  • storageTransactionId links a property to the Arweave transaction that stores its file. Its refUID is the Version.

OP Sepolia also has a legacy bytes32 storage_transaction_id schema (0xa1a5bee9e88ea4bac8e7a37a65ce735d47e3a9ff3e428308d40be499b4944817, EAS schema #132, no resolver, revocable, unnamed on EAS Scan). It isn't a protocol schema and isn't the one to use. Its 8 attestations, all from February 2024, hold a string encoded under a bytes32 schema, so a reader decodes each value as 0x…0020 instead of a transaction ID. SDKs before 0.7.6 picked a property's schema by name and could attest uploads under it; the SDK now picks by the exact definition, doesn't read a bytes32 value as an Arweave transaction ID, and doesn't recover those 8.

Who registers them

On a new chain, the protocol operator registers them with npx hardhat seed:ensure-schemas --network <name> from the contracts repo, which registers any that are missing and leaves the rest. --check-only only reports. See Deploying to a new chain.

The SDK only checks version and schemaName. ensureEasSchemasForItem and ensureEasSchemasForModels look both up before the first publish on a chain (once per chain, per process), and if either is missing it fails before anything is sent, with Seed base schema "<definition>" (<uid>) not registered on chain <id> (schemas … and … when both are). string storage_transaction_id is different: if it's missing when a publish uploads a file, the publish package registers and names it like any other property schema.

Model and property schemas

A model becomes an EAS schema when an item of it is first published, and a property when it's first published with a value. Before every publish, ensureEasSchemasForItem (from @seedprotocol/publish) checks every schema that publish attests under and then registers the missing ones, each with no resolver, as revocable, followed by a schemaName attestation that names it with its EAS name. Schema naming has the definition and name for each model and property.

The built-in storage models are model schemas too, and they're registered on OP Sepolia:

ModelSchemaUID
Imagebytes32 image0x5442a1004fe4152a06ed806367a989942f7051bad21bbed5b98d855509f79aed
Filebytes32 file0x9215fa8182c35e94a9201127f1067822ff2bc55323f0e889dba0bf39b0eda89e
Htmlbytes32 html0x745173b1044be69150b650aeffc2f5c479f8695b0d260e7a2a3b6ece77798996

Registering costs a transaction per schema plus one per name, sent from the publishing wallet. EAS schemas are shared: if another app already registered string title, your app uses the same one. Change a schema after you've shipped explains why names and types are a contract.

A session key can't register schemas: it can only call the Seed executor. Have the owner's wallet register them first with ensureEasSchemasForModels(modelNames, wallet), which covers every property of those models. An ordinary publish registers only what that item attests, so it leaves out the properties the item leaves empty. A session-key publish that needs a missing schema fails while it checks the item, before anything is uploaded, with EasSchemasNotRegisteredError (code: 'EAS_SCHEMAS_NOT_REGISTERED', missing lists every missing definition) and the message schema "<definition>" is not registered; automation keys can't register schemas. Call ensureEasSchemasForModels with the owner wallet first. (schemas "<definition>", "<definition>" are not registered; … when several are missing).

Tool schemas registered by the publish package

@seedprotocol/publish writes three kinds of tool attestations, each under its own schema. Each attest… function registers its schema the first time it's used, unless you pass ensureSchema: false, and the ensure…Schema(wallet) functions do it on their own.

NameSchemaUIDOP Sepolia
seedprotocol.publishAuthorizationaddress identity,address sessionKey,address app,string scopes,uint64 grantedAt,uint64 expiresAt,bytes32 permissionsHash0x04d3bf31bb5c07640f222023ee63b37769a966fc1500e96543ea72fb45b28a19Registered and named
seedprotocol.domainOwnershipstring domain,address claimer,string method,bytes32 challengeHash,uint64 verifiedAt,string registryCreationDate,string registryExpirationDate,bytes32 registryFingerprint,string scope0x3c78a221b5a06b61155981a4cdd50556ddf71452dd26d59512163219c18f1e2aNot registered yet
seedprotocol.publishedBybytes32 seedUid,bytes32 versionUid,bytes32[] attestationUids,bytes32 batchHash,string toolName,string toolVersion0x8856401efbc464e86c8c4077ded3c6dc4a9d4f8d398a7c1047c2bde9d8f40818Not registered yet

getPublishAuthorizationSchemaUid(), getDomainOwnershipSchemaUid() and getPublishedBySchemaUid() compute these UIDs locally. The attestations themselves are covered in Security and session keys, Domain ownership and Tool attestations.

Checking a schema yourself

Read the SchemaRegistry: on OP Stack chains it's the predeploy at 0x4200000000000000000000000000000000000020. An unregistered UID comes back with a zero uid.

import { createPublicClient, http, parseAbi, zeroHash } from 'viem'
import { optimismSepolia } from 'viem/chains'

const registry = '0x4200000000000000000000000000000000000020'
const abi = parseAbi([
  'struct SchemaRecord { bytes32 uid; address resolver; bool revocable; string schema; }',
  'function getSchema(bytes32 uid) view returns (SchemaRecord)',
])

const publicClient = createPublicClient({ chain: optimismSepolia, transport: http() })

export async function isRegistered(uid: `0x${string}`): Promise<boolean> {
  const record = await publicClient.readContract({ address: registry, abi, functionName: 'getSchema', args: [uid] })
  return record.uid !== zeroHash
}

To get a UID from a definition, SchemaRegistry.getSchemaUID(schema, resolver, revocable) from @ethereum-attestation-service/eas-sdk computes it offline.

On this page