Seed Protocol
@seedprotocol/publish

Overview and initPublish

What @seedprotocol/publish does, how to install it, its two entry points, every initPublish option, item.publish(), the PublishManager API that starts, retries and stops publishes, and publish wallets.

@seedprotocol/publish is the write path to the public networks. It takes an item from the SDK's local database, uploads its files to Arweave, and makes its EAS attestations from the user's account. It also handles wallets, cost estimates, revocation and a few tool attestations. The SDK and @seedprotocol/react don't publish anything on their own: the SDK's item.publish() and react's usePublishItem hand the item to this package, and fail with "Publishing is not configured" until initPublish has run.

import { initPublish, getPublishConfig, PublishManager } from '@seedprotocol/publish'

Seed Protocol publishes attestations to the OP Sepolia testnet only. Arweave has no testnet: uploads go to Arweave mainnet and cost real AR. See Costs and who pays.

Install

npm install @seedprotocol/sdk @seedprotocol/publish
npm install thirdweb # for @seedprotocol/publish/thirdweb

Publish declares these peer dependencies: @seedprotocol/sdk and @seedprotocol/react at exactly its own version, react and react-dom (18 or 19), @tanstack/react-query 5, and thirdweb 5. thirdweb is optional: the root entry point loads without it. You need it for @seedprotocol/publish/thirdweb (ConnectButton and the in-app wallets), and for useModularExecutor with a wallet that doesn't send as the ManagedAccount (Wallets). @tanstack/react-query isn't optional: the root entry point fails to load without it, on Node too. npm refuses an SDK at another version with ERESOLVE unable to resolve dependency tree. Don't get past that with --legacy-peer-deps: npm then installs the mismatched SDK and leaves out @seedprotocol/react. The Seed toolkit has the same-version rule for every package. npm may also print ERESOLVE overriding peer dependency warnings about ox, which permissionless depends on; the install completes.

On Node, run any process that loads the publish package with node --import @seedprotocol/sdk/node-eas-lodash (the SDK alone doesn't need it). See Using Seed in Node and Electron.

Entry points

ImportWhat's in it
@seedprotocol/publishinitPublish, PublishManager, the publish hooks, PublishProvider (no thirdweb), wallet adapters for EIP-1193, viem, Privy and ethers, createManagedAccountTxSender, createPermissionlessTxSender, publish automation and the session-key helpers (they take wallets you pass), cost estimates and upload limits, the tool attestations (DomainOwnership, PublishAuthorization, PublishedBy) and errors. Doesn't import thirdweb; the paths that need it load it on demand.
@seedprotocol/publish/thirdwebConnectButton, a PublishProvider that includes thirdweb's provider, fromThirdwebAccount, the in-app and ManagedAccount wallet helpers, the sign-out helpers (disconnectPublishWallet, markPublishWalletConnected, clearInAppPublishSession), and publish automation and the session-key helpers under the root entry's names, with wallets defaulting to the connected in-app ones.

Both entry points export PublishProvider and usePublishConfig. The two PublishProviders differ only in whether they wrap thirdweb's provider. See Components and hooks.

Importing the root entry point in a browser starts PublishManager straight away, and it resumes any publish the local database has marked in_progress. A publish it can't resume, such as one cut off while uploading through the upload API or Turbo, is marked interrupted instead, and the user publishes again. A publish another open tab is still running is left to that tab. On Node, nothing starts until you call PublishManager.ready().

initPublish

Registers the publish configuration and the executors behind the SDK's item.publish() and item.unpublish(). Call it once, at module level, before anything renders or publishes.

function initPublish(config: PublishConfig): void

It checks the config first, and throws without registering anything when:

  • payment contradicts another option (PublishPaymentConfigError);
  • useArweaveBundler is false (PublishPaymentConfigError): "useArweaveBundler: false is no longer supported. Publish uploads signed data items through the upload API's bundler routes (or Turbo with payment.storage: 'user'); remove the option.";
  • uploadApiBaseUrl is empty and payment.storage isn't 'user': "initPublish: uploadApiBaseUrl is required (the upload API that stores files on Arweave), unless payment.storage is "user".";
  • uploadApiBaseUrl is set but isn't an absolute http(s) URL or a path from the page's origin such as /seed-api: "initPublish: uploadApiBaseUrl must be an absolute http(s) URL or a path from the page's origin, got "<value>".";
  • the chain has no known EAS deployment and you didn't pass easContractAddress and schemaRegistryAddress;
  • SeedConfig.eas.chainId names a different chain from the publish chain.

It checks nothing else, so any other missing option surfaces later, when something needs it. Then it:

  • points the SDK's EAS reads (sync, schema lookups) at the publish chain. Until it runs, the SDK's EAS sync waits for it;
  • registers the publish executor (publishItem), so item.publish() and react's usePublishItem publish;
  • registers the revoke executor, so item.unpublish() works;
  • sets the publisher stamped on new drafts to the registered publish wallet's publisherAddress (or its signer's address);
  • starts a background worker in the leader tab that follows each upload until it lands in an Arweave L1 bundle (it polls every 45 seconds);
  • with useModularExecutor and modularAccountModuleContract, adds the executor address to the addresses the SDK syncs.

Example

src/publish.ts
import { initPublish, type PublishConfig } from '@seedprotocol/publish'

export const publishConfig: PublishConfig = {
  thirdwebClientId: import.meta.env.VITE_THIRDWEB_CLIENT_ID,
  uploadApiBaseUrl: import.meta.env.VITE_UPLOAD_API_BASE_URL,
  useModularExecutor: true,
  autoDeployManagedAccount: true,
}

initPublish(publishConfig)

This is the setup these docs recommend for an app that uses ConnectButton:

  • useModularExecutor: true has ConnectButton connect the in-app wallet and publish from the ManagedAccount it administers. The in-app wallet signs the data items, so item.publish() needs no dataItemSigner.
  • autoDeployManagedAccount: true is what deploys a new user's ManagedAccount in this setup, just before its first attestations. Without it, that user's first publish fails at creatingAttestations with MANAGED_ACCOUNT_NOT_DEPLOYED.

VITE_UPLOAD_API_BASE_URL is the address of an upload API your app can reach, such as a local Gateway Hyper sidecar at http://127.0.0.1:1984 or your own server's proxy. There's no public Seed upload API (Seed's gateway); without one, use payment: { storage: 'user' }.

Options

Connection and chain

NameTypeDefaultDescription
uploadApiBaseUrlstringBase URL of the upload API: an absolute http(s) URL, or a path from the page's origin such as /seed-api. Publish POSTs signed data items to ${uploadApiBaseUrl}/api/upload/arweave/batch. Required unless payment.storage is 'user'; getPublishConfig() returns '' when it's unset.
thirdwebClientIdstringthirdweb client ID. Needed for @seedprotocol/publish/thirdweb (ConnectButton, in-app wallets, thirdweb's bundler and paymaster). Without rpcUrl, chain reads go to thirdweb's RPC edge with this ID.
thirdwebSecretKeystringthirdweb secret key, for servers only. Authenticates thirdweb RPC and bundler calls from Node, where a domain-locked client ID gets 401. It doesn't build an RPC URL on its own: also pass rpcUrl or thirdwebClientId. Never ship it to a browser.
rpcUrlstringthirdweb RPC edgeJSON-RPC URL for the publish chain. Required when thirdwebClientId is unset. thirdweb wallets use it too when it's set.
chainviem ChainoptimismSepoliaThe chain to attest on. Leave it unset: Seed's contracts are on OP Sepolia.
easContractAddressstringfrom chain.idEAS contract. Needed on chains without a known EAS deployment.
schemaRegistryAddressstringfrom chain.idEAS SchemaRegistry. Same rule.
managedAccountFactoryAddressstringbuilt in for OP Sepoliathirdweb ManagedAccount factory. Only the managed-account flows use it.

thirdweb wallets (thirdweb)

NameTypeDefaultDescription
thirdweb.sponsorGasbooleantrue (false with payment.gas: 'user')Asks thirdweb's paymaster to pay gas for the ManagedAccount's UserOps and, in 'EIP7702' mode, the in-app wallet's transactions. It only requests sponsorship: the thirdweb project behind your client ID decides whether the paymaster pays. Set false where there's no paymaster (a local chain); the accounts then need ETH.
thirdweb.modularWalletMode'EIP7702' | 'EOA''EIP7702'How the in-app wallet (the ManagedAccount's admin) sends its own transactions, such as installing the Seed executor. 'EOA' sends plain transactions and needs ETH. A local chain needs 'EOA'.
thirdweb.bundlerUrlstringthirdweb's hosted bundlerERC-4337 bundler for the ManagedAccount. Required on a local chain.

Account mode for other wallets (EIP-1193, viem, Privy, ethers)

NameTypeDefaultDescription
accountMode'eoa' | 'eip7702''eip7702' when bundlerUrl is set, else 'eoa'How useSeedWallet and ensureWalletThenPublish send transactions. 'eoa': the wallet sends them and pays gas. 'eip7702': a permissionless EIP-7702 smart account sends UserOps through bundlerUrl. An undelegated EOA's first UserOp needs signAuthorization, which a plain injected wallet can't give; see The EIP-7702 sender.
bundlerUrlstringERC-4337 bundler (EntryPoint v0.8) for 'eip7702' mode. Rejected when it reports no v0.8 support. Fees come from its pimlico_getUserOperationGasPrice when it answers that method, else from the chain's estimate. Not used by thirdweb wallets.
paymasterUrlstringbundlerUrlERC-7677 paymaster for 'eip7702' mode. When unset, the bundler URL is used as the paymaster too. With payment.gas: 'user' no paymaster is used, and initPublish throws if this is set.

Attestation routing

NameTypeDefaultDescription
useDirectEasbooleanfalseAlways attest straight to EAS (attest/multiAttest from the publishing account) instead of through the Seed contract's multiPublish.
useModularExecutorbooleanfalseUse the two-wallet setup: ConnectButton connects the in-app wallet, and the ManagedAccount publishes through its Seed extension. A registered wallet that sends as the ManagedAccount skips the in-app flow (ManagedAccount without thirdweb). Also what routes a session key's publish through the executor.
modularAccountModuleContractstringThe executor's address (see Deployments). It has to be the executor the account's factory pins, which readFactorySeedExecutor() returns: enroll fails with EXECUTOR_MODULE_NOT_INSTALLED on any other, and the in-app flow logs a warning. When set, ConnectButton and the in-app flow's publish prep install it on the ManagedAccount if it's missing. Needed for publish automation.
modularAccountModuleDatastringDeprecated. Not read.
autoDeployManagedAccountbooleanfalseWith useModularExecutor, deploy the ManagedAccount through the factory when it has no code yet, instead of failing with MANAGED_ACCOUNT_NOT_DEPLOYED.
autoDeployEip7702ModularAccountbooleantrue with useModularExecutor, else falseWith useModularExecutor, bootstrap the in-app wallet's EIP-7702 account when it has no code yet.
deployManagedAccount(params: { managedAddress: string; managedSigningAccount: PublishWallet }) => Promise<void>thirdweb deploySmartAccountYour own deploy step for autoDeployManagedAccount.
onWalletSetupWarning(error: unknown) => voidCalled when an optional setup step after connecting fails, such as installing the executor.
maxAutomationExposureWeibigint10n ** 15n (0.001 ETH)Most ETH an account may hold, balance plus EntryPoint deposit, when enrollPublishAutomation runs. Above it enroll fails with AUTOMATION_ACCOUNT_FUNDED before sending, and a session-key publish logs a warning. A failed read of the balance fails enroll with MODULAR_SIGNER_ACTIVATION_FAILED.

checking picks the route. With useDirectEas, the publish attests straight to EAS and skips the checks below. Each item takes up to three transactions: an attest for a new Seed, an attest for a new Version, and one multiAttest for its properties. With useModularExecutor, it always calls multiPublish through the ManagedAccount, and autoDeployManagedAccount deploys the account first in creatingAttestations when it has no code. A wallet that sends as its publisherAddress, with a different signer and a kind other than 'eoa', skips that step and the in-app wallet, so its account must already be deployed. Otherwise the publisher address decides: an address with contract code publishes through multiPublish on the Seed contract inside the account, and one with no code attests straight to EAS. An EIP-7702-delegated EOA counts as having no code.

Attesting straight to EAS without useDirectEas needs a wallet that says it's an EOA (kind: 'eoa', see Wallets) and both sends and signs as the publisher. A wallet with no kind takes the registered publish wallet's kind when it has the same signer. checking refuses the publish otherwise:

  • The wallet has kind: 'smart' or 'modular': the publisher is an undeployed smart account. If the registered publish wallet publishes as that address and has a deploy function, checking calls it, waits until the RPC shows the account's code, and publishes through multiPublish. ConnectButton registers one for its smart wallet, so a new user's first publish deploys the account. Without one, it fails with ManagedAccountPublishError code MANAGED_ACCOUNT_NOT_DEPLOYED: "The publishing smart account <address> is not deployed on <chain> yet. Deploy it first (your wallet's setup flow, or deploySmartWalletContract from @seedprotocol/publish/thirdweb), then publish again."
  • It sends as another address, which EAS would record as the attester: "The publish wallet sends as <sender>, not as the publisher <address>, which has no contract deployed on <chain>. …"
  • It signs as another address: "The publish wallet signs as <signer>, not as the publisher <address>. Attesting directly to EAS would record the wrong attester."
  • It has no kind: "The publisher <address> has no contract on <chain>, and the publish wallet doesn't say whether it is an EOA or an undeployed smart account. Build it with fromEip1193Provider or fromEthersWallet, set kind: 'eoa' on the PublishWallet, or set useDirectEas to attest directly anyway."

When the code lookup itself fails, the publish fails with "Could not verify whether the publisher is a deployed contract on <chain>. Check your RPC connection and retry."

multiPublish needs this package and the deployed contracts to be the same version. Before sending, a contract publish runs assertMultiPublishRouted, and on contracts without the selector it sends (MULTI_PUBLISH_SELECTOR, 0xa1263421) it fails with ManagedAccountPublishError code MULTI_PUBLISH_NOT_ROUTED: "…The deployed contracts and this SDK are different versions; nothing was sent." The check passes when it can't run, and the simulation decides. Direct EAS calls no Seed contract and has no such requirement.

Arweave uploads

Each file goes up as an ANS-104 data item signed by the user's key, in one batch to the upload API, whose bundler pays; with payment.storage: 'user' the data items go straight to Turbo instead, paid from the signer's Turbo credits. See Uploads and ANS-104 data items.

NameTypeDefaultDescription
useArweaveBundlerbooleanDeprecated. true is ignored; false makes initPublish throw PublishPaymentConfigError.
dataItemSignerethers.Wallet | SeedSignerFallback data item signer, used when the publish has none of its own (see dataItemSigner below). A publish resumed after a reload always uses this one, because a per-publish signer isn't saved.
signDataItems(uploads: PublishUploadData[]) => Promise<ArweaveDataItemInfoResult[]>Fallback data item signing callback.
maxUploadBytesnumber104857600 (100 MiB)Most a publish may upload, summed over its files. See Upload limits. Infinity turns it off.
confirmUploadAboveBytesnumber26214400 (25 MiB)Above this many bytes, a publish needs confirmUpload: true. A low-memory device can need it below this.
uplinkBitsPerSecondnumber5000000Uplink speed for upload time estimates.
arweaveUploadTagsTransactionTag[]Tags added to every upload after Content-SHA-256 and Content-Type. Per-publish tags are appended after these.
htmlEmbeddedDataUriPolicy'materialize' | 'preserve''materialize'What a publish does with data:image/... URIs inside Html values: upload them as their own images (materialize) or leave them inline. A property's own policy wins.
htmlArweaveRefs'gateway' | 'ar''gateway'How a publish writes Arweave links into the Html it uploads. 'gateway': links on the public gateway, the SDK's arweaveDomain (arweave.net by default), which load in any browser; new links are https://<host>/raw/<id>, and existing ones keep their path form. 'ar': ar://<id>, with no host, which only Seed readers load. Html already published isn't rewritten. See Links in published content.
arweaveUploadVerificationBaseUrlstringuploadApiBaseUrlOrigin used to verify uploads (GET /api/upload/arweave/data/:id).
arweaveGraphqlUrlstringhttps://arweave.net/graphqlArweave GraphQL endpoint the worker tries first to find each upload's L1 bundle. If it doesn't answer, the worker tries the default read gateways and then falls back to the bundle ID the upload API's status endpoint reports.
gatewayTransport'http-gateway' | 'hyper' | 'hybrid'Deprecated. Not read.
gatewayHyperKeystringInformational. The Gateway Hyper sidecar is configured separately.

Who pays and approval

NameTypeDefaultDescription
payment{ gas?: 'sponsored' | 'user'; storage?: 'operator' | 'user'; userStorage?: PublishUserStorageConfig }States who pays gas and storage. Fills in the default of thirdweb.sponsorGas and makes initPublish throw PublishPaymentConfigError on a contradiction. See The payment option. Who pays in each wallet setup is on Costs and who pays.
authorizePublish(request: PublishAuthorizationRequest) => PublishAuthorization | boolean | void | Promise<…>Approves each publish before it spends anything. See Approving publishes. A per-publish authorizePublish replaces it.

Costs and hooks

NameTypeDefaultDescription
getTokenPrices() => Promise<{ ethUsd: number; arUsd: number }>Coinbase spot pricesUSD prices for cost estimates. Cached for 60 seconds.
onPublished(result: { seedLocalId: string; seedUid: string; versionUid?: string; attestationUids: string[] }) => void | Promise<void>Called when a publish succeeds, with the UIDs it made. Tool backends use it to make a PublishedBy attestation. Errors it throws are logged, not raised. A per-publish onPublished replaces it.

Approving publishes

authorizePublish runs as the first step of every publish (authorizing), before it registers schemas, uploads or attests anything. Use it to confirm the cost with the user, check a quota, or charge for the publish.

type AuthorizePublish = (request: {
  item: IItem<any>
  seedLocalId: string
  publishMode: 'patch' | 'new_version'
  payment: ResolvedPublishPayment // who will pay, as resolvePublishPayment returns it
  getEstimate: () => Promise<PublishCostEstimate> // fetches prices: call it only if you need it
}) => PublishAuthorization | boolean | void | Promise<PublishAuthorization | boolean | void>

type PublishAuthorization = { token?: string }
  • Return false, or throw, to refuse: the publish fails at authorizing with PublishNotAuthorizedError (a thrown error becomes its cause).
  • Return true, nothing, or {} to go ahead.
  • Return { token } to go ahead and send the token to the upload API as the X-Seed-Publish-Authorization header (PUBLISH_AUTHORIZATION_HEADER) on this publish's upload requests (/api/upload/arweave/batch), so its operator can meter or bill against it. It isn't sent to Turbo.

A publish resumed after a reload isn't asked again once it got past authorizing. One restored before that asks the config's hook, because a per-publish hook isn't saved.

src/publish.ts
import { initPublish, type PublishConfig } from '@seedprotocol/publish'

export const publishConfig: PublishConfig = {
  thirdwebClientId: import.meta.env.VITE_THIRDWEB_CLIENT_ID,
  uploadApiBaseUrl: import.meta.env.VITE_UPLOAD_API_BASE_URL,
  useModularExecutor: true,
  autoDeployManagedAccount: true,
  authorizePublish: async ({ getEstimate }) => {
    const { work } = await getEstimate()
    return window.confirm(`Publish ${work.attestationCount} attestations and ${work.uploadCount} uploads?`)
  },
}

initPublish(publishConfig)

getPublishConfig

Returns the config with defaults applied.

function getPublishConfig(): ResolvedPublishConfig

Throws "@seedprotocol/publish: Call initPublish() or ensure PublishProvider is mounted with config before using the publish package" when nothing has registered a config.

Returns

Every PublishConfig field you passed, plus these resolved ones:

FieldTypeDescription
chainviem Chainconfig.chain or OP Sepolia
easChainEasChainDeploymentThe EAS deployment for the chain, including easContractAddress, schemaRegistryAddress, indexerUrl and explorerUrl (https://optimism-sepolia.easscan.org on OP Sepolia)
easContractAddress, schemaRegistryAddressstringFrom easChain, with your overrides applied
thirdwebAccountFactoryAddressstring | undefinedmanagedAccountFactoryAddress, or the built-in factory for the chain
thirdweb{ bundlerUrl: string | undefined; sponsorGas: boolean; modularWalletMode: 'EIP7702' | 'EOA' }thirdweb options with defaults
accountMode'eoa' | 'eip7702'As described above
useDirectEas, useModularExecutor, autoDeployManagedAccount, autoDeployEip7702ModularAccountbooleanDefaults applied
arweaveUploadVerificationBaseUrl, arweaveGraphqlUrlstringDefaults applied

usePublishConfig() returns the raw PublishConfig from React context, without these defaults.

Example

import { getPublishConfig } from '@seedprotocol/publish'

export function attestationLink(seedUid: string) {
  const { easChain } = getPublishConfig()
  return `${easChain.explorerUrl}/attestation/view/${seedUid}`
}

item.publish() and publishItem

Once initPublish has run, the SDK's item.publish(options) publishes. It hands the item to publishItem, the executor initPublish registers, which starts a publish with the registered publish wallet and waits for it to finish. React's usePublishItem calls item.publish() too.

function publishItem(item: IItem<any>, options?: ItemPublishOptions): Promise<void>
function waitForPublishActor(actor: ActorRef<any, any>, seedLocalId: string): Promise<void>

options are CreatePublishOptions. publishItem waits for PublishManager.ready(), starts the publish through ensureWalletThenPublish (which hands it to ensureSmartWalletThenPublish with useModularExecutor, unless the registered wallet sends as its publisher), and resolves when the publish reaches success. Like every publish, it holds the item until the publish settles, so an item looked up without a hold doesn't unload mid-publish. It signs data items with the signer createPublish defaults to.

publishItem rejects with the errors below. item.publish() rejects with the same object and usePublishItem sets it as its error, so instanceof ManagedAccountPublishError and its code work there too. UploadTooLargeError and UploadNeedsConfirmError arrive as themselves, so instanceof and their estimate work too. Any other failure at a step and a failed attestation reject with a plain Error whose cause is the error the publish stopped on. The rejections item.publish() adds before the publish starts (ownership, already publishing) are on Item.

  • "No publish wallet is connected. Connect one (useSeedWallet().connect, or setPublishWallet) before publishing." or "The publish wallet has no publisher address." when nothing usable is registered. With useModularExecutor, an empty registry gives the second. These are plain Errors; a PublishManager.createPublish run with no wallet fails in checking with PublishWalletMissingError (code: 'PUBLISH_WALLET_MISSING') and the same text, which waitForPublishActor gives as cause;
  • a ManagedAccountPublishError (MANAGED_ACCOUNT_UNAVAILABLE) with useModularExecutor, when the in-app wallet or its ManagedAccount isn't connected;
  • "Publish of <id> did not start: it is already publishing in this tab or another.";
  • UploadTooLargeError (code: 'UPLOAD_TOO_LARGE') or UploadNeedsConfirmError (code: 'UPLOAD_NEEDS_CONFIRM') from the upload limits;
  • "Publish of <id> failed at <step>: …" when the publish ends in failure. For example at authorizing when authorizePublish refuses, or at checking with one of the refusals under Attestation routing in Options;
  • "Creating attestations for <id> failed: …. Uploads are kept; retry with PublishManager.retryAttestations('<id>')." when it stops in a recoverable attestation failure;
  • a PublishRetractionError (code: 'PUBLISH_RETRACTION_FAILED') when the attestations were made but revoking the retracted properties failed (retractionFailureRecoverable): "Revoking the retracted properties of <id> failed: …. The new attestations are in place; retry with PublishManager.retryAttestations('<id>') to send the revocation again.";
  • "Publish of <id> was stopped before it finished." when something stops it, such as stopPublish, or ConnectButton when another wallet takes over or the user disconnects.

waitForPublishActor is the waiting half on its own, for an actor from createPublish.

import type { Item } from '@seedprotocol/sdk'

// With the recommended config (useModularExecutor) and ConnectButton.
export async function publishPost(post: Item<any>) {
  // The in-app wallet signs the data items; the ManagedAccount attests.
  await post.publish()
}

PublishManager

The object that runs publishes. Each publish is a state machine for one item, keyed by its seedLocalId, and each step is saved to the local publish_processes table.

const PublishManager: {
  createPublish(
    item: Item<any>,
    address: string,
    account?: PublishWallet,
    options?: CreatePublishOptions,
  ): Promise<ActorRef<any, any> | undefined>
  retryAttestations(seedLocalId: string, account?: PublishWallet): void
  stopPublish(seedLocalId: string): void
  stopAll(): void
  ready(): Promise<void>
  getPublish(seedLocalId: string): ActorRef<any, any> | undefined
  query(seedLocalId: string): Snapshot | undefined
  getService(): ActorRef<any, any>
}

Simplified: the real return types are XState actor and snapshot types. PublishManager also has savePublish, addSubscription and removeSubscription, which the package uses internally.

PublishManager.createPublish

Starts publishing one item.

createPublish(item, address, account?, options?): Promise<ActorRef<any, any> | undefined>
NameTypeDefaultDescription
itemItem<any>requiredThe item to publish. useItem returns IItem, so cast with item as Item<any>.
addressstringrequiredThe publishing account, which becomes the attester. With ConnectButton, use getPublishWallet().publisherAddress.
accountPublishWalletthe registered publish walletSigns and sends. Build it with an adapter (fromThirdwebAccount, fromEip1193Provider, fromViemWalletClient, fromPrivyWallet, fromEthersWallet). Its kind decides whether it may attest straight to EAS (Attestation routing in Options): a plain { signer, txSender } object you wrote yourself has no kind, so it's rejected when the publisher has no contract code and useDirectEas is off. With useModularExecutor the publish always goes through multiPublish, and kind isn't checked.
optionsCreatePublishOptionsBelow.

It resolves as soon as the publish has started, with its actor. The publish carries on in the background; follow it with useItemPublishStatus. It resolves undefined, without starting anything, when:

  • address is empty;
  • the item is already publishing in this tab;
  • another tab holds the item's publish lock (a Web Lock named seed:publish:<seedLocalId>).

Calls made before the manager has finished restoring saved publishes are queued and run once it's ready.

A publish holds its item until it stops (success, failure or stopped), so an item you found without a hold doesn't unload after the grace period mid-upload. A publish waiting in a recoverable failure keeps the hold. An item that isn't in the cache takes none. See unload.

CreatePublishOptions

NameTypeDefaultDescription
publishMode'patch' | 'new_version''patch''patch' attests the properties changed since the last publish. 'new_version' makes a new Version and attests every property that has a value; it needs an already-published item.
dataItemSignerethers.Wallet | SeedSignersee belowSigns the ANS-104 data items. With payment.storage: 'user', its address's Turbo credits pay for them. It must be an EOA's key, not a smart account's. Without one, a publish uses config.dataItemSigner; else the registered publish wallet's dataItemSigner when that wallet publishes as address (ConnectButton registers one); else the account's own dataItemSigner, or its signer unless it's kind: 'smart'. See Uploads and ANS-104 data items.
signDataItems(uploads: PublishUploadData[]) => Promise<ArweaveDataItemInfoResult[]>config.signDataItemsSigns and uploads the data items yourself; used before dataItemSigner when both are set. Called twice per publish when an Html value has images to materialize.
maxUploadBytesnumberconfig.maxUploadBytes, then 100 MiBReplaces the limit for this publish.
confirmUploadbooleanfalseAccepts this publish's upload size, so it doesn't fail with UploadNeedsConfirmError. Doesn't lift maxUploadBytes.
arweaveUploadTagsTransactionTag[]Appended after config.arweaveUploadTags.
htmlEmbeddedDataUriPolicy'materialize' | 'preserve'config.htmlEmbeddedDataUriPolicy, then 'materialize'As in the config.
htmlArweaveRefs'gateway' | 'ar'config.htmlArweaveRefs, then 'gateway'As in the config.
onPublishedOnPublishedCallbackconfig.onPublishedReplaces the config's callback for this publish.
authorizePublishAuthorizePublishconfig.authorizePublishReplaces the config's hook for this publish. See Approving publishes.

Items with Html, Image or File values need a signer for their uploads, dataItemSigner or signDataItems, from the options or the config. Without either, the publish fails at creatingArweaveDataItems with "@seedprotocol/publish: uploading data items needs signDataItems or a dataItemSigner (pass one at createPublish or in the PublishProvider config). It must be an EOA's key: …".

A dataItemSigner you pass here is kept in memory only, not in the publish's publish_processes row. A publish resumed after a reload falls back to the config's dataItemSigner. Rows saved by 0.7.3 or earlier can still hold an arweaveJwk in a database a later release has opened since; clear completed run history to delete them.

What a publish does

  1. authorizing: calls authorizePublish, if you set one. Without it, this step passes straight through.
  2. checking: checks that the item is owned (isItemOwned), that the local database and RPC are on the publish chain, that no related item has been unpublished, that the upload limits allow the publish (from file sizes, before any file is read), and that there's a publisher address; when the item has retracted properties, checks that the wallet can revoke their attestations and fails if it can't (an automation session key, or a Seed the executor attested); fails with PublishWalletMissingError when the run has no wallet and none is registered; registers, with account, the missing EAS schemas this publish attests under (the model of the item and of each draft published with it, each property that has a value or whose empty value removes a live attestation, and string storage_transaction_id for a seed that uploads a file), checking them all before it sends anything; an automation session key can't register schemas, so a missing one fails the publish with EasSchemasNotRegisteredError; picks multiPublish or direct EAS; validates the item for that route; deploys or refuses an undeployed smart account (Attestation routing in Options), validating again after a deploy.
  3. Arweave steps, only when the item or a related item has Html, Image or File values: preparingHtmlEmbedded, creatingArweaveDataItems, uploadingViaBundler (which uploads to Turbo with payment.storage: 'user'). Html with materialized images then runs rewritingHtmlEmbedded and a second pass, on either attestation route: creatingArweaveDataItemsPhase2 and uploadingViaBundlerPhase2.
  4. creatingAttestations (multiPublish) or creatingAttestationsDirectToEas. multiPublish sends the item and every draft published with it in one transaction, so a failure attests none of them, and both sides of a relation cycle between drafts are attested together. With useModularExecutor, creatingAttestations first deploys the ManagedAccount when autoDeployManagedAccount is set and it has no code. Before sending, it simulates the call and estimates its gas from the sending account (the estimate plus 30%, or 5,000,000 when no estimate can be made). A publish that needs more gas than a block holds fails with ManagedAccountPublishError code PUBLISH_TOO_LARGE and nothing is sent; publish some of its related drafts on their own first. The direct-EAS route goes request by request and records each Seed locally as soon as it's attested, so a retry after a partial failure doesn't attest it again. On that route a relation back to an item the publish is still creating is left out for the item's next publish.
  5. revokingRetractedProperties: revokes the attestations of retracted properties that are still live, one multiRevoke per schema from the Seed's attester, and marks them retracted locally. With nothing retracted it passes straight through. An EOA publisher signs this as a second transaction.
  6. success or failure.

A failure before the attestations ends in failure. A failure while attesting waits in attestationFailureRecoverable (or attestationFailureRecoverableDirectToEas) with the uploads kept, for retryAttestations. A failure while revoking waits in retractionFailureRecoverable with the attestations made; retryAttestations sends the revocations again, and the next publish also sends any still live. A publish an older release saved in a step that no longer exists is marked interrupted when it's restored; publish the item again. Publishing and revoking items covers each case.

Example

import type { Item } from '@seedprotocol/sdk'
import { PublishManager, getPublishWallet } from '@seedprotocol/publish'

// With the recommended config (useModularExecutor) and ConnectButton,
// the registered wallet is the in-app wallet and publisherAddress is its ManagedAccount.
export async function publish(item: Item<any>) {
  const wallet = getPublishWallet()
  if (!wallet) throw new Error('Connect a wallet first.')
  const address = wallet.publisherAddress ?? wallet.signer.address
  const run = await PublishManager.createPublish(item, address, wallet)
  return run !== undefined // false: already publishing here or in another tab
}

PublishManager.retryAttestations

Retries the attestations of a publish waiting in attestationFailureRecoverable or attestationFailureRecoverableDirectToEas, without uploading again, or the revocations of one waiting in retractionFailureRecoverable.

retryAttestations(seedLocalId: string, account?: PublishWallet): void

Uses account, or the registered publish wallet when omitted. Returns nothing; follow the result with useItemPublishStatus. Does nothing (and logs a warning) when this tab isn't running a publish for that item.

PublishManager.stopPublish

Stops one publish and marks its row interrupted. Releases its publish lock.

stopPublish(seedLocalId: string): void

Nothing already uploaded or attested is undone.

PublishManager.stopAll

Stops every publish running in this tab and releases their locks.

stopAll(): void

Unlike stopPublish, it leaves their rows in_progress, so the next page load resumes them. ConnectButton calls it when the user disconnects and when another wallet takes over; see ConnectButton.

PublishManager.ready

Starts the manager if it hasn't started, and resolves once it has restored saved publishes and is accepting new ones.

ready(): Promise<void>

In the browser the manager starts on import, so you don't need it. On Node, call await PublishManager.ready() before createPublish. Restoring waits up to 60 seconds for the SDK's database, polling every 2 seconds; SEED_PUBLISH_RESTORE_WAIT_MS and SEED_PUBLISH_RESTORE_POLL_MS change those times.

PublishManager.getPublish and query

getPublish(seedLocalId) returns the running publish for an item, or the last one that finished in this tab, as an XState actor. undefined when there's neither. query(seedLocalId) returns that actor's current snapshot, or undefined; it only reads. Prefer the hooks in React.

Wallets

A publish needs a PublishWallet: a signer (signs messages and data items), a txSender (submits transactions) and, optionally, a kind. The adapters build one and mark it as genuine, which createPublish checks. Wallets lists the adapters, which sender each config selects, and how to publish from a ManagedAccount without thirdweb.

type PublishWalletKind = 'eoa' | 'smart' | 'modular'

type PublishWallet = {
  signer: SeedSigner
  txSender: SeedTxSender
  kind?: PublishWalletKind // 'eoa': a plain key; 'smart': a smart account; 'modular': an EOA that publishes as a ManagedAccount; unset: unknown
}

kind matters on one route: a publisher with no contract code attests straight to EAS only from a kind: 'eoa' wallet (Attestation routing in Options). An unknown kind is refused there, not guessed. 'modular' is an admin EOA that publishes as its ManagedAccount: the in-app EOA that ConnectButton (with useModularExecutor) and ensureSmartWalletThenPublish register, or a signer paired with createManagedAccountTxSender. It signs data items like an EOA, and is refused for direct EAS like 'smart'.

setPublishWallet(session) registers the wallet that publishes and revokes by default. It also sets the SDK's owned addresses to the session's ownedAddresses, signer, sender and publisherAddress (watched addresses are kept), and claims unclaimed drafts for publisherAddress. getPublishWallet() returns it (or null). clearPublishWallet() forgets it and clears the SDK's owned addresses, keeping watched ones; it returns a Promise<void>. After that, new drafts are signed-out drafts that the next connected wallet claims, and drafts stamped with the old wallet are read-only until it reconnects. ConnectButton and useSeedWallet call both for you.

type PublishWalletSession = PublishWallet & {
  ownedAddresses?: string[]
  publisherAddress?: string // the attester; defaults to signer.address
  adminAddress?: string // the smart account's admin EOA, when there is one
  dataItemSigner?: SeedSigner // an EOA key that signs data items for a smart account
  deploy?: () => Promise<void> // deploys the publisher's smart account when it has no code
}

dataItemSigner is the session's data item signer: a publish as the session's publisherAddress uses it unless you pass one or the config sets one (see dataItemSigner). deploy is what a first publish from an undeployed kind: 'smart' account calls in checking. ConnectButton sets all three extra fields; see ConnectButton.

Other exports

These are documented in the guides or on other pages:

ExportsWhere
PublishProvider, ConnectButton, useSeedWallet, useItemPublishStatus and the other hooksComponents and hooks
estimatePublishCost, watchPublishCost, repricePublishCostEstimate, usePublishCostEstimate, estimateUpload and its constants, UploadTooLargeError, UploadNeedsConfirmError, resolvePublishPayment, resolvePublishSender, PublishPaymentConfigError, InsufficientStorageCreditsError, getTurboBalanceWinc, getTurboPriceWinc, DEFAULT_TURBO_UPLOAD_URL, DEFAULT_TURBO_PAYMENT_URL and the PublishPaymentConfig typesCost estimates and payment
PublishNotAuthorizedError, PUBLISH_AUTHORIZATION_HEADER, type AuthorizePublish, type PublishAuthorization, type PublishAuthorizationRequestApproving publishes
Revocation (item.unpublish())Revocation
PublishedBy and PublishAuthorization attestations (attestPublishedBy, collectPublishedBatch, attestPublishAuthorization, assessPublishAuthorizationLive, the schema helpers), revokePublishedBy, revokePublishAuthorizationTool attestations
DomainOwnership attestationsDomain ownership
enrollPublishAutomation, revokePublishAutomation, ensureAutomationSessionKey, removeAutomationSessionKey, isAutomationSessionActive, ensureExecutorModuleInstalledPublishing and revoking items, and Session keys without thirdweb for the root entry's versions
fromViemWalletClient, fromPrivyWallet, fromEip1193Provider, fromWindowEthereum, fromEthersWallet, createManagedAccountTxSender, createPermissionlessTxSender, CALL_GAS_HEADROOM_BPSWallets
ensureEasSchemasForModels(modelNames, wallet, { schemaName?, followRefs?, dryRun?, managedAddress? })Registers, with no item, every EAS schema a publish of those models can attest under: each model, every property, the Image, File and Html models its storage-seed properties reach, string storage_transaction_id when it's needed, and with followRefs (default true) the models its relation and list properties name. schemaName picks the schema when two define a model of the same name. With dryRun it sends nothing and returns what's missing. Run it with the owner's wallet before unattended publishing. It needs initPublish and a loaded schema, and wallet must be a PublishWallet or SeedTxSender even for a dry run. Throws No model named <name> for an unknown model.
ensureEasSchemasForItem(item, wallet, { managedAddress? })Registers the missing schemas a publish of this item attests under, the same set as the checking step. It doesn't cover properties the item leaves empty, so it doesn't prepare automation for later items.
type EnsureEasSchemasResult, type EnsureEasSchemasOptions, type EnsureEasSchemasForModelsOptionsBoth functions check every schema before sending, then resolve to { missing, registered }: the definitions that weren't registered (such as string word_count) and the ones the call registered (empty for a dry run). With managedAddress and an active automation session key as wallet, a missing schema throws EasSchemasNotRegisteredError before any transaction (not with dryRun).
verifyPublishChain()Checks the RPC's chain ID and that EAS, the SchemaRegistry and any factory or executor have code. Throws PublishChainConfigError. Publishing and revoking call it; call it at startup to fail early.
assertMultiPublishRouted(target, { executorModule }), MULTI_PUBLISH_SELECTORThe version check a contract publish runs, for use in a deploy script: target is a ManagedAccount with executorModule: false, the executor with true. Throws MULTI_PUBLISH_NOT_ROUTED when target has no multiPublish with this selector.
seedTwinConfig(twinJson)Config for the local OP Sepolia fork. See Local twin.
ManagedAccountPublishError (with code, such as MANAGED_ACCOUNT_NOT_DEPLOYED, MANAGED_ACCOUNT_UNAVAILABLE, AUTOMATION_PREFLIGHT_FAILED, AUTOMATION_ACCOUNT_FUNDED, PUBLISH_TOO_LARGE or MULTI_PUBLISH_NOT_ROUTED), Eip7702ModularAccountPublishError, PublishTransactionRevertedError, AttestationVerificationError, MissingSignedDataItemsError (code: 'SIGNED_DATA_ITEMS_MISSING', a resumed publish whose signed data items are gone; publish again) and is... guards; EasSchemasNotRegisteredError (code: 'EAS_SCHEMAS_NOT_REGISTERED', missing lists every missing definition; run ensureEasSchemasForModels with the owner's wallet); PublishRetractionError; PublishWalletMissingError (code: 'PUBLISH_WALLET_MISSING')Thrown by publish steps. PublishRetractionError is what publishItem rejects with when revoking retracted properties fails. A failed publish's errorCode in publish_processes holds the code.
ensureWalletThenPublish(item, options?), ensureSmartWalletThenPublish(item, account, getAddress, options?) (/thirdweb)Wrappers around createPublish that pass your options through and wait for the publish to start, not finish (publishItem and item.publish() wait for it to finish). The publish holds the item (createPublish). ensureWalletThenPublish publishes with the registered wallet and its default data item signer; with useModularExecutor it hands over to ensureSmartWalletThenPublish, which needs thirdweb, unless that wallet sends as its publisher. ensureSmartWalletThenPublish with useModularExecutor publishes from the in-app wallet as kind: 'modular', so data items are signed by the registered session's dataItemSigner, else the in-app wallet; without it, by your dataItemSigner, else the registered wallet's, else the connected account when it's the smart wallet's admin EOA. Both resolve to { outcome: 'started', publish } (publish is the actor), { outcome: 'already_publishing' } when the item is already publishing in this tab or another, or no_address, managed_not_ready (with error), and for ensureWalletThenPublish no_wallet. ensureSmartWalletThenPublish without useModularExecutor can also return needs_deploy. The result's started is deprecated: a promise already resolved with publish.
publishItem, waitForPublishActoritem.publish() and publishItem
type PublishStartedDeprecated. The type of the result's started.

On this page