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:
- The user owns the item (
assertItemOwned): its publisher is one of the SDK's owned addresses. - The item is published: it has a
seedUidand aschemaUid. initPublishhas run, so a revoke executor is registered.- The local database and the RPC are on the publish chain: the database holds no attestations from another chain, and
verifyPublishChainpasses. - The attester isn't the Seed executor module. Only a legacy setup recorded the module as the attester, and the module has no revoke.
- 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. WithuseModularExecutor, whereConnectButtonregisters 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. - 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:
- the Seed attestation;
- the property attestations, one transaction per property schema;
- 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
| Message | Cause |
|---|---|
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 |
PublishChainConfigError | The 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 else | The 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,DomainOwnershipandPublishAuthorizationattestations 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.
Related
- Publishing and revoking items: the same button with confirmation
- Permanence: what revoking leaves behind
- Identity and signing: why only the owner revokes
- Item:
isRevoked,revokedAtandgetSeedPublishState - Tool attestations: revoking PublishedBy and PublishAuthorization attestations
- Overview and initPublish
Cost estimates and payment
estimatePublishCost, usePublishCostEstimate and watchPublishCost for pricing a publish before it runs, the upload limits, and the payment option that states who pays gas and storage, including user-paid storage through Turbo.
Domain ownership
Prove that a user controls a domain with a DNS TXT challenge, record it as a seedprotocol.domainOwnership EAS attestation from a tool wallet, recheck it against the domain registry, and revoke it.