Seed Protocol
@seedprotocol/publish

Revocation

How item.unpublish() revokes an item's attestations through @seedprotocol/publish: the checks it runs, which wallet sends the revokes, their order, errors, retrying, and what it leaves live.

Revoking marks an item's EAS attestations as withdrawn. The call is item.unpublish() on the SDK's Item; @seedprotocol/publish supplies the part that sends the revokes, and registers it when initPublish runs. There's no revoke function to import from the publish package for items.

// Works once initPublish() from @seedprotocol/publish has run.
await item.unpublish()

Revoking doesn't delete anything: the revoked attestations stay readable on EAS and the files stay on Arweave. Permanence explains what's left behind.

item.unpublish

Revokes the item's Seed, Version and property attestations.

item.unpublish(): Promise<void>

It runs these checks, in order, before sending anything:

  1. The user owns the item (assertItemOwned): its publisher is one of the SDK's owned addresses.
  2. The item is published: it has a seedUid and a schemaUid.
  3. initPublish has run, so a revoke executor is registered.
  4. The local database and the RPC are on the publish chain: the database holds no attestations from another chain, and verifyPublishChain passes.
  5. The attester isn't the Seed executor module. Only a legacy setup recorded the module as the attester, and the module has no revoke.
  6. There's a wallet that can send as the attester: the registered publish wallet (getPublishWallet()) when it sends as the item's attester. Otherwise, when none is registered or the registered one is a different account, the thirdweb account connected in this page, resolved to the attester's account: the account itself when it is the attester, else its ManagedAccount when that is the attester and is deployed. With useModularExecutor, where ConnectButton registers the in-app wallet, that's the ManagedAccount that attested the item. If that can't be resolved, the registered wallet is used, and EAS rejects it.
  7. The wallet isn't an automation session key for the item's attester. Revoking is owner-only.

Then it sends one EAS multiRevoke transaction per schema, from the wallet's txSender, waiting for each receipt. Before each one it reads the attestations' state from EAS and leaves out any already revoked:

  1. the Seed attestation;
  2. the property attestations, one transaction per property schema;
  3. the Version attestations.

The Seed goes first because sync reads a property attestation revoked under a live Seed as a retracted value and drops it. With the Seed already revoked, a sync that runs between the transactions keeps the item's last values.

When every transaction has gone through, it stamps revoked_at on the item's seed, version and property rows in the local database and updates the item: item.isRevoked becomes true, item.revokedAt is set, and its latest version becomes the newest one that isn't revoked (a local draft, or none).

Only the account that made the attestations can revoke them; EAS rejects anyone else.

Example

import { useState } from 'react'
import { useItem } from '@seedprotocol/react'

export function UnpublishButton({ seedLocalId }: { seedLocalId: string }) {
  const { item } = useItem({ modelName: 'Post', seedLocalId })
  const [error, setError] = useState<string | null>(null)
  const [finished, setFinished] = useState(false)

  if (!item?.seedUid) return null
  if (item.isRevoked && finished) return <p>Unpublished</p>

  const label = error ? 'Retry unpublish' : item.isRevoked ? 'Finish unpublishing' : 'Unpublish'

  return (
    <>
      {item.isRevoked && !error && <p>Unpublished</p>}
      <button
        onClick={() => {
          setError(null)
          item
            .unpublish()
            .then(() => setFinished(true))
            .catch((err: unknown) => setError(err instanceof Error ? err.message : String(err)))
        }}
      >
        {label}
      </button>
      {error && <p role="alert">{error}</p>}
    </>
  )
}

Errors

MessageCause
Item is read-only: you do not own this item (OwnershipError)The item's publisher isn't an owned address
Item is not published. Cannot unpublish.No seedUid
Item has no schema UID. Cannot unpublish.No schemaUid
Revocation is not configured. Call initPublish()...initPublish hasn't run
Seed Protocol chain mismatch: the local database holds attestations from chain …The local database was filled from another chain
PublishChainConfigErrorThe RPC or contracts don't match the publish chain
Revocation not supported for items attested by the Seed executor module…The attester is the executor module itself, which only a legacy setup produced. Nobody can revoke these.
No wallet connected. Connect a wallet to revoke attestations.No publish wallet and no connected thirdweb account
No wallet connected. Use useSeedWallet / setPublishWallet, or @seedprotocol/publish/thirdweb ConnectButton.No publish wallet, and resolving the connected thirdweb account failed. The failure is the error's cause.
Automation session keys cannot revoke attestations...The registered wallet's signer is an active automation session key on the attesting account (checked only when modularAccountModuleContract is set)
Only the original attester can revoke attestations. Connect the wallet that published this item.EAS rejected a revoke with AccessDenied (by name or selector 0x4ca88867): the sending account isn't the attester
multiRevoke for schema <uid> reverted with AlreadyRevoked, and EAS could not be read…A batch reverted twice with AlreadyRevoked and EAS couldn't be read to drop the revoked ones. Retry.
Anything elseThe transaction error, unchanged

Retrying

A failure partway leaves the Seed revoked, some property or Version attestations live, and the item not marked revoked locally. queryBySchema already leaves the item out, and other devices drop it from their item lists at their next sync. A sync on this device can set item.isRevoked before you retry, so isRevoked doesn't mean the unpublish finished, and after a reload nothing on the item tells a complete unpublish from a partial one. That's why the example keeps its button: Retry unpublish after a failure, and Finish unpublishing beside "Unpublished" when isRevoked is set and this session hasn't seen the call succeed.

unpublish() doesn't check isRevoked, so calling it again is safe. It reads which attestations EAS already shows as revoked, leaves them out of each batch, and revokes the rest; a batch left empty isn't sent, so when everything is revoked no transaction goes out. It still runs every check above first, so it needs a wallet that can send as the attester (check 6) even then. A failed read counts as not revoked, so that attestation stays in the batch. If a batch still reverts with AlreadyRevoked (by name or selector 0x905e7107), it reads the state again and retries once; a batch of one counts as done. When it can't tell which attestations are revoked, it fails rather than skip them.

What isn't revoked

unpublish() revokes only what this device's database records for the item:

  • Related items stay published. Authors, and the storage items behind Html, Image and File values, have Seeds of their own. Unpublish them separately.
  • Attestations made elsewhere may stay live. The database has every attestation published from this device, but for data synced from EAS it keeps only the newest attestation per property per Version. Older edits published from other devices aren't revoked. Seed's readers still hide the item, because its Seed and Versions are revoked; anyone querying EAS directly sees those attestations as live.
  • Tool attestations stay. PublishedBy, DomainOwnership and PublishAuthorization attestations have their own attesters, who revoke them with the functions in Tool attestations.
  • Nothing on Arweave changes.

Republishing

There's no un-revoke. Publishing a revoked item again, with item.publish() or PublishManager.createPublish, creates a new Seed with a new UID and attests every property; the old Seed stays revoked. Publish it again covers what happens to items that point at it.

On this page