Public records
The contract’s public registry is permissionless. Any wallet can write to it directly — no Monolith account, no API key, no application, no fee.
It exists because the curated registry is written by Monolith on behalf of Artifacts. The public registry is the open counterpart: a place for anyone to anchor a fingerprint-to-manifest binding on the same chain, readable by the same tooling, without going through Monolith at all.
Everything here is on the same contract described in the
overview — address
0xfE9daFC133eD3e9726D4B2c24b8cC3c3AaDD8225 on chain 101010.
Registering an entry
function registerPublicManifest(
bytes32 fingerprint,
bytes32 manifestHash,
string calldata userId,
string[] calldata endpoints,
string calldata manifestId
) external;| Argument | Type | Notes |
|---|---|---|
fingerprint | bytes32 | Required, non-zero. SHA-256 of the asset’s exact bytes |
manifestHash | bytes32 | Required, non-zero. A hash committing to the manifest or metadata you’re binding |
userId | string | Optional here — may be empty. Free-form; not verified by anyone |
endpoints | string[] | Soft Binding Resolution API URLs. Leave empty to fall back to the contract’s getDefaultEndpoints() |
manifestId | string | The manifest identifier this fingerprint resolves to |
The sender’s address is recorded as registeredBy. One fingerprint can be
registered once — a second write for the same fingerprint reverts with
PublicManifestAlreadyExists.
ethers
import { readFile } from 'node:fs/promises'
import { createHash } from 'node:crypto'
import { JsonRpcProvider, Wallet, Contract } from 'ethers'
const RPC = 'https://rpc.stabilityprotocol.com/zgt/try-it-out'
const { address, abi } = await fetch(
'https://api.joinmonolith.com/c2pa/contract'
).then(r => r.json())
const bytes = await readFile('my-art.png')
const fingerprint =
'0x' + createHash('sha256').update(bytes).digest('hex')
const provider = new JsonRpcProvider(RPC, 101010)
const wallet = new Wallet(process.env.PRIVATE_KEY, provider)
const registry = new Contract(address, abi, wallet)
const tx = await registry.registerPublicManifest(
fingerprint,
manifestHash, // your 0x-prefixed bytes32
'', // userId — optional
['https://example.com/c2pa'], // resolution endpoints
'urn:uuid:0f9a…', // manifestId
{ gasPrice: 0 } // the chain is gasless
)
const receipt = await tx.wait()
console.log(receipt.hash)The write emits two events — PublicManifestRegistered and
PublicSoftBindingRegistered. The second is the one soft-binding
resolvers watch; see Soft binding.
Reading it back
| Function | Returns |
|---|---|
publicManifestExists(bytes32) | bool |
getPublicManifestByBinding(bytes32) | (string[] endpoints, string manifestId) |
getPublicManifestRecord(bytes32) | (bytes32 manifestHash, string userId, string[] endpoints, string manifestId, uint256 timestamp) |
getPublicManifestRegisteredBy(bytes32) | address |
getTotalPublicManifests() | uint256 |
ethers
const [manifestHash, userId, endpoints, manifestId, timestamp] =
await registry.getPublicManifestRecord(fingerprint)
const registrant =
await registry.getPublicManifestRegisteredBy(fingerprint)Reads revert with PublicManifestNotFound when the fingerprint has no
public entry. Check publicManifestExists first, or catch the revert.
Resolution precedence
Most of the time you don’t care which registry an answer came from — you
just want to resolve a fingerprint. resolveByBindingDetailed searches
both and tells you which one answered:
function resolveByBindingDetailed(bytes32 fingerprint)
external view
returns (
uint8 source,
string[] memory endpoints,
string memory manifestId,
bytes32 manifestHash,
address registeredBy,
uint256 timestamp
);source | Meaning |
|---|---|
1 | Curated — the entry Monolith wrote for an Artifact |
2 | Public — an entry written directly by a wallet |
The curated registry always wins. If a fingerprint exists in both, you
get the curated entry and source is 1. If it exists in neither, the
call reverts with ManifestNotFound.
resolveByBinding is the same lookup without the provenance fields, for
when you only need the endpoints and manifest id.
Removing an entry
function deletePublicManifest(bytes32 fingerprint, string calldata reason) external;Callable by the address that registered the entry, or by Monolith. The
record is removed from storage outright and PublicManifestDeleted is
emitted with the supplied reason. The transaction itself stays in the
chain’s history, but the registry no longer resolves that fingerprint, and
it becomes available for registration again.
What a public entry proves
A public entry proves that this address wrote this binding at this time. It does not prove authorship, ownership, or that the registrant has ever seen the underlying file.
There is no fee, no rate limit, no quota, and no identity check on the public registry. Anyone can register any fingerprint they can compute — including one for a file they had no part in making — and the first write for a fingerprint is the one that sticks. Treat a public entry as a signed assertion by a wallet, and judge it on whether you trust that wallet.
Two things follow for anyone consuming these records:
- Check
registeredBy. It is the only identity in a public entry. Everything else,userIdincluded, is free-form text chosen by whoever sent the transaction. - Treat
endpointsas untrusted input. They are arbitrary strings supplied by the registrant and can point anywhere. Validate and sandbox them exactly as you would any third-party URL before fetching.
Curated-first resolution is what keeps this from affecting Monolith Artifacts: a public entry can never shadow the curated record for the same fingerprint.
When writes revert
| Error | Cause |
|---|---|
PublicManifestAlreadyExists(bytes32) | The fingerprint already has a public entry |
PublicManifestNotFound(bytes32) | Read or delete for a fingerprint with no public entry |
ZeroFingerprint() | fingerprint was bytes32(0) |
ZeroManifestHash() | manifestHash was bytes32(0) |
ArrayTooLong(uint256,uint256) | Too many endpoints — the limit is on-chain |
EmptyEndpoint() | One of the endpoints strings was empty |
NotAuthorizedToDelete() | Delete attempted by an address that didn’t register the entry |
PublicRegistryPaused() | The public registry is not currently accepting writes |
ContractPaused() | The contract as a whole is not currently accepting writes |
isPublicRegistryPaused() is a free read, so you can check before
sending a write rather than discovering it from a revert.
Where to go next
- Soft binding — what these bindings are for and how resolution works end-to-end
- Interface — every function, event, and revert