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:
| Function | Selector |
|---|---|
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:
- For every request, it creates the Seed attestation if
seedUidis zero, and the Version attestation ifversionUidis zero. - For every request, it writes the Seed UID into the slots named by
propertiesToUpdate(see Cross-references). - For every request, it points each property attestation without a
refUIDat the request's Version, forces it revocable, and sends the request's properties to EAS in onemultiAttest.
function multiPublish(PublishRequestData[] memory requests)
external payable returns (bytes32[] memory)Parameters
| Name | Type | Description |
|---|---|---|
requests | PublishRequestData[] | 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:
| Caller | How it happens |
|---|---|
| An account admin | The admin's address calls the account directly |
| The account's EntryPoint | A 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 itself | execute(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
| Attestation | Schema | refUID | Data | When |
|---|---|---|---|---|
| Seed | seedSchemaUid (the model's schema, such as bytes32 post) | none | abi.encode(seedSchemaUid) | seedUid is zero |
| Version | versionSchemaUid (the bytes32 version base schema) | the Seed | abi.encode(versionSchemaUid) | versionUid is zero |
| Properties | each entry's schema in listOfAttestations | the Version, unless the entry already has a non-zero refUID | as the client encoded it | always, 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.
| Field | Type | Description |
|---|---|---|
localId | string | The client's ID for the item. Carried through and never read by the contract. |
seedUid | bytes32 | Existing Seed UID, or zero to create one |
seedSchemaUid | bytes32 | Schema for a new Seed: the model's bytes32 <model> schema |
versionUid | bytes32 | Existing Version UID, or zero to create one |
versionSchemaUid | bytes32 | Schema for a new Version: the bytes32 version base schema |
seedIsRevocable | bool | Ignored. Everything is revocable. |
listOfAttestations | MultiAttestationRequest[] | The property attestations, grouped by schema, in EAS's own MultiAttestationRequest shape (schema plus an array of AttestationRequestData) |
propertiesToUpdate | PropertyToUpdateWithSeed[] | Cross-references to fill with this request's Seed UID |
PropertyToUpdateWithSeed
| Field | Type | Description |
|---|---|---|
publishIndex | uint256 | Index in requests of the request to write into. Any request in the batch, this one included. |
propertySchemaUid | bytes32 | The property schema in that request whose single data entry receives the Seed UID |
listElement | bool | false: the entry is one bytes32. true: the entry is exactly abi.encode(bytes32[]) and one element receives the UID. |
elementIndex | uint256 | The 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
| Field | Type |
|---|---|
schemaUid | bytes32 |
attestationUid | bytes32 |
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.
| Event | When |
|---|---|
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
| Error | From | Cause |
|---|---|---|
Unauthorized(address caller) | SeedAccountAuth | The caller isn't an admin, the EntryPoint or the account itself |
AttestationFailed(bytes32 schemaUid) | Extension | EAS returned a zero UID for a Seed or Version |
InvalidEAS(address eas) | Extension | Constructor only: the EAS address has no code |
PublishValueMismatch(uint256 required, uint256 sent) | SeedPublishLib | msg.value isn't the sum of the attestations' value fields; see Value |
PublishIndexOutOfBounds, EmptyAttestationData, PropertyToUpdateNotFound, AmbiguousPropertyToUpdate, InvalidRelationEncoding, ElementIndexOnSingleRelation, InvalidListEncoding, ListElementOutOfBounds, PlaceholderMismatch | SeedPublishLib | A 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.
Related
- Contracts overview: how the extension fits with the executor
- Executor and router extension: the session-key path, with the same
multiPublish - Cross-references:
propertiesToUpdatein depth - Working with EAS: what the attestations look like on EAS
- Security and session keys
Deployments
Live Seed Protocol contract addresses on OP Sepolia, how each was checked on-chain, who holds the factory roles, when the current contracts went live, and older addresses that are no longer used.
Executor and router extension
Reference for SeedProtocolExecutor, the ERC-7579 executor module session keys publish through, and SeedExecutorRouterExtension, which lets a thirdweb ManagedAccount host it: install, functions, events, errors and supported accounts.