AI agents
This page is the entry point for automated consumers of the protocol: agents, indexers, keeper bots, and retrieval pipelines. It links out to the full reference rather than restating it. Everything below is a public read or a permissionless call; nothing here needs an API key.
The protocol is a set of contracts on Ethereum mainnet (chain id 1). All state is on-chain. The surfaces on this page are conveniences layered over that state: a manifest that names every contract, an indexer that mirrors events into a queryable database, cached JSON endpoints that back the site, and this documentation itself as a retrieval corpus.
Machine-readable surfaces
| Surface | URL | What it is |
|---|---|---|
| Protocol manifest | /protocol-manifest.json |
Canonical origin, addresses, ABI paths, and docs links per contract |
| Contract ABIs | /abis/<ContractName>.json |
One plain ABI array per contract |
| LLM orientation | /llms.txt |
A compact map of the whole reference, with absolute links |
| Docs search index | /docs-search-index.json |
Every docs section as a retrievable record |
| Indexer | GraphQL, env-configured | Events mirrored into queryable tables (reference) |
| REST endpoints | /api/* |
Cached JSON the site reads (reference) |
Start from /protocol-manifest.json: its origin
field is the canonical site (https://permanentcollection.art), and every
contract entry carries the address, a relative ABI path, and its docs page.
Recipe 1: read headline protocol state
Cheapest first. The indexer's protocolCounter(id: "global") row holds the
running totals in one query:
{
protocolCounter(id: "global") {
collectedCount
vaultedCount
proofsMinted
totalEthBurned
totalTokensBurned
totalSwapVolumeWei
}
}For the live bid and the count without an indexer, read the chain directly:
cast call 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 "collectedCount()(uint256)" --rpc-url https://ethereum-rpc.publicnode.com
cast call 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 "isComplete()(bool)" --rpc-url https://ethereum-rpc.publicnode.com
cast call 0xC8ED01ffd957f5a62b1526d58E309c8bf2BB4A4c "bidBalance()(uint256)" --rpc-url https://ethereum-rpc.publicnode.comThe site also serves /api/live-bid and
/api/stats (cached JSON) for the same numbers. Full read surface:
read the collection state.
Recipe 2: check whether a Punk is eligible
A Punk is eligible for the live bid when it carries an uncollected,
not-pending trait. The one call that answers it is
PermanentCollection.canonicalTargetOf(punkId), which returns the
protocol-derived target trait or reverts NoEligibleTarget:
cast call 0x59607d4d92a57EAa8544b2AdE7b014F8785AAf34 "canonicalTargetOf(uint16)(uint8)" 6529 --rpc-url https://ethereum-rpc.publicnode.comThe /api/eligibility?punkId=<id> endpoint resolves
the whole record in one request: canonicalTargetId, uncollectedBits,
pendingBits (in-flight, therefore ineligible), alreadyRecorded, and the
soleCarrier constraint. Full flow:
accept the live bid.
Recipe 3: find live return auctions
Every accepted Punk sits in a 72-hour return auction until it clears or is vaulted. The indexer lists the open ones, soonest-ending first:
{
returnAuctions(where: {settled: false}, orderBy: "endsAt", orderDirection: "asc") {
items { punkId targetTraitId reserveWei highBidWei highBidder endsAt }
}
}On-chain, ReturnAuctionModule.isLive(punkId), getSale(punkId), and
isSettleable(punkId) answer the same per Punk. See
ReturnAuctionModule and
bid on a return auction.
Recipe 4: watch protocol events
Two ways in. For historical or aggregate views, query the indexer: each table names the events that feed it. For live reaction, subscribe to logs with viem. The events index lists every event and its contract.
import {createPublicClient, webSocket, parseAbiItem} from 'viem';
import {mainnet} from 'viem/chains';
const client = createPublicClient({chain: mainnet, transport: webSocket()});
client.watchEvent({
address: '0x2ced1CA0BC7996c3dFB3D4E685d9d9E7405A7aCe',
event: parseAbiItem(
'event ReturnAuctionStarted(uint16 indexed punkId, uint128 acquisitionCost, uint128 reserveWei, uint64 startedAt, uint64 endsAt)',
),
onLogs: (logs) => {
for (const log of logs) console.log('new auction', log.args.punkId);
},
});Load the exact event signature for any contract from its ABI at
/abis/<ContractName>.json rather than hand-writing it.
Recipe 5: run keeper actions
Four permissionless calls keep the protocol moving; three pay the caller a bounded reward. Check readiness off-chain, simulate, then send:
LiveBidAdapter.sweep()meters the buffer into the live bidBuybackBurner.executeStep(minOut)buys and burns $111 in paced stepsReturnAuctionModule.settle(punkId)closes an auction past its deadlineProtocolFeePhaseAdapter.sweep()forwards the protocol fee leg
Each call's timing, reward, and readiness reads are in run a keeper.
Recipe 6: load the protocol manifest and ABIs
Fetch the manifest once, then instantiate any contract from it:
import {createPublicClient, getContract, http} from 'viem';
import {mainnet} from 'viem/chains';
const ORIGIN = 'https://permanentcollection.art';
const manifest = await fetch(`${ORIGIN}/protocol-manifest.json`).then((r) => r.json());
const entry = manifest.contracts.Patron;
const abi = await fetch(`${ORIGIN}${entry.abi}`).then((r) => r.json());
const client = createPublicClient({chain: mainnet, transport: http('https://ethereum-rpc.publicnode.com')});
const patron = getContract({address: entry.address, abi, client});
const liveBidWei = await patron.read.bidBalance();The same pattern works for any contract in the manifest. Details and the full ABI list: ABIs and the protocol manifest.
Recipe 7: use the public REST endpoints
The site exposes cached JSON at /api/* for the reads its own pages need:
/api/live-bid, /api/stats, /api/eligibility, /api/auction-bids,
/api/punks-with-trait, and more. These serve the frontend and are not a
versioned API contract; on-chain reads and the indexer are the canonical
interfaces. Response shapes and parameters:
REST endpoints.
Recipe 8: use the docs search index as retrieval context
/docs-search-index.json is this reference split
into retrievable records, one per section. Each record is:
{
"path": "/docs/contracts/patron",
"page": "Patron",
"heading": "acceptBid",
"anchor": "acceptbid",
"text": "…the first ~300 characters of the section…"
}Embed or keyword-match over heading + text, then resolve a citation URL as
${origin}${path} (append #${anchor} when set). For a smaller map of the
same content, /llms.txt lists every page with an absolute link
and a one-line description.