Seed Protocol
Contracts

SeedProtocolExtension

Reference for SeedProtocolExtension, the extension that publishes Seeds, Versions and properties from a thirdweb ManagedAccount: multiPublish, getEas, structs, events, errors and caller rules.

SeedProtocolExtension is the contract code behind multiPublish on a user's thirdweb ManagedAccount. It's registered on the account factory's Router, so when the account receives a multiPublish call it runs this code with delegatecall: storage, balance and address(this) are the account's, and EAS sees the account as the attester. Shared logic lives in the abstract SeedProtocolExtensionBase and the SeedPublishLib library.

You call it at the user's account address, not at the implementation's address (Deployments lists that). The publish package does this for you; this page is for calling the contract yourself, decoding its events, or reading a revert.

The account routes two selectors to it:

FunctionSelector
multiPublish(PublishRequestData[])0xa1263421
getEas()0xd5f5127d

multiPublish

Publishes one or more items in a single transaction. After checking msg.value, it works through the whole batch in three phases:

  1. For every request, it creates the Seed attestation if seedUid is zero, and the Version attestation if versionUid is zero.
  2. For every request, it writes the Seed UID into the slots named by propertiesToUpdate (see Cross-references).
  3. For every request, it points each property attestation without a refUID at the request's Version, forces it revocable, and sends the request's properties to EAS in one multiAttest.
function multiPublish(PublishRequestData[] memory requests)
    external payable returns (bytes32[] memory)

Parameters

NameTypeDescription
requestsPublishRequestData[]One entry per item, attested in array order. A cross-reference can target any request in the array, by position.

Returns

The Seed UID of each request, in request order: the new Seed when the request's seedUid was zero, otherwise the one passed in. Version and property UIDs come back in the events, not the return value.

Who may call it

multiPublish checks msg.sender in the account's context and reverts with Unauthorized(caller) unless it's one of:

CallerHow it happens
An account adminThe admin's address calls the account directly
The account's EntryPointA UserOp whose signature the account has validated. thirdweb accounts only let session keys sign execute/executeBatch UserOps, so a UserOp that calls multiPublish directly was signed by an admin.
The account itselfexecute(account, 0, multiPublish(...)) or executeBatch. An admin can do this, and so can any session key whose approvedTargets include the account.

A session key with the account as a target can call anything that trusts self-calls, not only multiPublish. The publish package never grants that: its session keys target only the executor. Security and session keys has the details.

What it creates

AttestationSchemarefUIDDataWhen
SeedseedSchemaUid (the model's schema, such as bytes32 post)noneabi.encode(seedSchemaUid)seedUid is zero
VersionversionSchemaUid (the bytes32 version base schema)the Seedabi.encode(versionSchemaUid)versionUid is zero
Propertieseach entry's schema in listOfAttestationsthe Version, unless the entry already has a non-zero refUIDas the client encoded italways, if the list isn't empty

Every attestation is created with revocable: true, no recipient and no expiry for the Seed and Version. For properties, the contract only overrides refUID (when zero) and revocable; the recipient, expiry and data you send are kept. seedIsRevocable and each entry's revocable flag are ignored.

So a request with both UIDs zero publishes a new item; a request with a seedUid and a zero versionUid adds a new Version to an existing item; and a request with both UIDs set only adds property attestations to an existing Version. That's how the publish package's new_version and patch modes map onto the contract. Working with EAS shows the resulting attestation tree.

The contract doesn't check that a seedUid or versionUid you pass exists or belongs to the account; EAS rejects a refUID that doesn't exist, but not one made by someone else. Readers should only trust a Version or property whose attester matches the Seed's.

Value

multiPublish is payable, and msg.value must equal the sum of the value fields of the batch's attestations. Otherwise it reverts with PublishValueMismatch(required, sent) before attesting anything. Each request's multiAttest gets that request's share, and the Seed and Version attestations are made with no value. The extension runs as the account, so the check also stops a batch being paid from the account's balance. Seed's schemas have no resolvers, so a Seed publish carries no value. encodeMultiPublish sends whatever the attestations sum to.

The contract forces every attestation revocable, so every schema it attests with must be registered as revocable. EAS reverts with Irrevocable() for a schema registered as irrevocable. The publish package registers all its schemas as revocable with no resolver.

Example

The publish package builds requests with getPublishPayload and sends them for you. It exports the selector as MULTI_PUBLISH_SELECTOR but not its ABI. To call the account yourself, describe the function with viem (this encodes one new Post with a single string title property; the schema UIDs are the real OP Sepolia ones):

import { encodeAbiParameters, encodeFunctionData, parseAbi, zeroAddress, zeroHash } from 'viem'

const multiPublishAbi = parseAbi([
  'struct AttestationRequestData { address recipient; uint64 expirationTime; bool revocable; bytes32 refUID; bytes data; uint256 value; }',
  'struct MultiAttestationRequest { bytes32 schema; AttestationRequestData[] data; }',
  'struct PropertyToUpdateWithSeed { uint256 publishIndex; bytes32 propertySchemaUid; bool listElement; uint256 elementIndex; }',
  'struct PublishRequestData { string localId; bytes32 seedUid; bytes32 seedSchemaUid; bytes32 versionUid; bytes32 versionSchemaUid; bool seedIsRevocable; MultiAttestationRequest[] listOfAttestations; PropertyToUpdateWithSeed[] propertiesToUpdate; }',
  'function multiPublish(PublishRequestData[] requests) payable returns (bytes32[])',
])

const VERSION_SCHEMA_UID = '0x13c0fd59d69dbce40501a41f8b37768d26dd2e2bb0cad64615334d84f7b9bdf6' // bytes32 version
const POST_SCHEMA_UID = '0x8fad85bc255f47ba00cdf83e0bb4b606ca1e703974ba0d8295241cbba04ce7d7' // bytes32 post
const TITLE_SCHEMA_UID = '0x1ea9eab5cdf2c82a2ca64c931932c0a10cf8a0ce50f9de4bcff1be2a5ca89ed7' // string title

const data = encodeFunctionData({
  abi: multiPublishAbi,
  functionName: 'multiPublish',
  args: [[{
    localId: 'post-1', // carried through, never read
    seedUid: zeroHash, // create a Seed
    seedSchemaUid: POST_SCHEMA_UID,
    versionUid: zeroHash, // create a Version
    versionSchemaUid: VERSION_SCHEMA_UID,
    seedIsRevocable: true, // ignored
    listOfAttestations: [{
      schema: TITLE_SCHEMA_UID,
      data: [{
        recipient: zeroAddress,
        expirationTime: 0n,
        revocable: true, // forced to true anyway
        refUID: zeroHash, // filled with the new Version's UID
        data: encodeAbiParameters([{ type: 'string' }], ['Hello']),
        value: 0n,
      }],
    }],
    propertiesToUpdate: [],
  }]],
})

// `data` starts with 0xa1263421. Send it to the user's ManagedAccount address,
// from an admin or as an admin-signed UserOp.

getEas

Returns the EAS contract the extension attests to.

function getEas() external view returns (address)

On OP Sepolia it returns 0x4200000000000000000000000000000000000021. You can call it on the implementation or on any account from the factory; both return the same address.

EAS is fixed at deployment

The EAS address is an immutable set in the constructor, which reverts with InvalidEAS(eas) if the address has no code. Immutables live in the contract's bytecode, so they read correctly under delegatecall, and no account can change them. There's no setEas: the old extension's setEas let anyone redirect an account's EAS, and it's no longer routed. To use a different EAS, the factory admin (the holder of the factory's EXTENSION_ROLE) deploys a new extension and calls replaceExtension, which switches every account from that factory at once.

Before a multiPublish sent by the account's admin, the publish package reads getEas() from the account. If it returns a different contract than easContractAddress in your publish config, the publish fails with MANAGED_ACCOUNT_SET_EAS_FAILED and nothing is sent. A zero address, which only an extension from before 2026-10-03 returned, makes it send setEas from the admin. A session-key publish reads the EAS the executor stored for the account instead of getEas(), and fails with AUTOMATION_UNSUPPORTED_ACCOUNT if it differs.

Structs

PublishRequestData

The executor's multiPublish takes the same struct, field for field, so one encoding works for both.

FieldTypeDescription
localIdstringThe client's ID for the item. Carried through and never read by the contract.
seedUidbytes32Existing Seed UID, or zero to create one
seedSchemaUidbytes32Schema for a new Seed: the model's bytes32 <model> schema
versionUidbytes32Existing Version UID, or zero to create one
versionSchemaUidbytes32Schema for a new Version: the bytes32 version base schema
seedIsRevocableboolIgnored. Everything is revocable.
listOfAttestationsMultiAttestationRequest[]The property attestations, grouped by schema, in EAS's own MultiAttestationRequest shape (schema plus an array of AttestationRequestData)
propertiesToUpdatePropertyToUpdateWithSeed[]Cross-references to fill with this request's Seed UID

PropertyToUpdateWithSeed

FieldTypeDescription
publishIndexuint256Index in requests of the request to write into. Any request in the batch, this one included.
propertySchemaUidbytes32The property schema in that request whose single data entry receives the Seed UID
listElementboolfalse: the entry is one bytes32. true: the entry is exactly abi.encode(bytes32[]) and one element receives the UID.
elementIndexuint256The list element to fill. Must be 0 when listElement is false.

The slot must hold bytes32(sourceIndex + 1) before the write, where sourceIndex is this request's position. Cross-references explains that rule, the others and the errors.

CreatedAttestationResult

FieldType
schemaUidbytes32
attestationUidbytes32

Events

Because the extension runs by delegatecall, these events are emitted by the account's address, not by the extension's. EAS's own Attested events come from the EAS contract as usual.

EventWhen
CreatedAttestation(CreatedAttestationResult result)Once for each Seed and each Version created
SeedPublished(bytes returnedDataFromEAS)Once per request, after its properties. The bytes are abi.encode(bytes32[]) of the property attestation UIDs, in multiAttest order; an empty array when the request had no properties.

Events follow the phases: every Seed's and Version's CreatedAttestation, in request order, then each request's SeedPublished. For a new item, the first CreatedAttestation of the request is its Seed and the second its Version. The publish package reads neither event, only EAS's Attested.

Errors

ErrorFromCause
Unauthorized(address caller)SeedAccountAuthThe caller isn't an admin, the EntryPoint or the account itself
AttestationFailed(bytes32 schemaUid)ExtensionEAS returned a zero UID for a Seed or Version
InvalidEAS(address eas)ExtensionConstructor only: the EAS address has no code
PublishValueMismatch(uint256 required, uint256 sent)SeedPublishLibmsg.value isn't the sum of the attestations' value fields; see Value
PublishIndexOutOfBounds, EmptyAttestationData, PropertyToUpdateNotFound, AmbiguousPropertyToUpdate, InvalidRelationEncoding, ElementIndexOnSingleRelation, InvalidListEncoding, ListElementOutOfBounds, PlaceholderMismatchSeedPublishLibA cross-reference in propertiesToUpdate breaks a rule; see Cross-references

EAS's own errors (InvalidSchema, NotFound for an unknown refUID, Irrevocable, and so on) bubble up unchanged. The publish package decodes all of these when its pre-send simulation fails.

On this page