Developers
Architecture
Sur cette page
Three diagrams: what the pieces are, what happens when one image gets a passport, and what the contracts depend on.
System overview
Five people or programs touch the system and each one enters through a different door. The creator
binds a passkey (bindPasskey) and registers, and states how AI may use the picture. The generator
agent registers on the creator's behalf, with both signatures. An editing app appends edits through a
Privy session signer. Anyone verifying uploads a copy to the verify service and never touches the
chain at all. On chain, MoolamRegistry reads the P-256 precompile for passkeys, the ERC-8004 Identity Registry for
agent ownership, and VerifierPolicy for who may attest, while MoolamConsent reads the registry
for who holds a passport. Off chain, the Chainlink CRE workflow re-fetches and re-fingerprints,
Envio indexes the events, and the verify service reads passports out of Envio and statements
straight off the consent register.
Chainlink's verdicts reach the registry two ways. A verdict from Chainlink's network goes through the
Keystone Forwarder to MoolamReceiver, and only those can light a passport's trust mark. A test run in
Chainlink's simulator goes through the simulation forwarder to the SimulationReceiver pair, which also
checks that Moolam's CRE wallet signed the transaction. The sealed workflow reads a private copy through
a link the verify service mints.
The drawing reads from top to bottom. Rounded boxes are people and agents, solid boxes are contracts on Monad mainnet, and dashed boxes run off chain.
Main sequence: a generated image gets a passport and is verified
Everything before the register call is preparation that costs no gas: the fingerprint, the C2PA
manifest, the four IPFS pins, and the EIP-712 digest read back from the contract. Then two
signatures over that one digest, the creator's passkey and the agent owner's wallet, and one
transaction that checks both. The registration event is what wakes the Chainlink workflow, which
fetches the image itself, recomputes the fingerprint and writes the result back through Chainlink's
Keystone Forwarder to MoolamReceiver. On Chainlink's network a quorum of nodes must produce the
identical verdict before a report is signed, and the first one did on 2026-10-02: ten nodes, distance 0,
written 16 seconds after the passport was registered
(transaction 0x760dcb5e…2882).
A run in Chainlink's simulator with --broadcast is one machine and no consensus. It goes through the
simulation forwarder to a SimulationReceiver, which takes it only in a transaction Moolam's CRE wallet
signed. The passport page labels each verdict by the forwarder that delivered it.
Verification of a copy later runs entirely off chain: anyone uploads a file to the service, the service computes the eight rotated and mirrored fingerprints, searches the index, and returns the best passport with its attestations, its edit chain and a confidence score.
A statement about AI use, and the day query
A creator's answer to "may AI train on this" is written twice in the same minute and read back much later. The service writes it into the signed C2PA manifest and the pinned metadata before the passport exists, because a manifest is signed once and cannot be added to. The chain half waits until the registration is confirmed, then goes out as a second sponsored transaction from the creator's own wallet. If that second send fails, the passport stays registered and the statement can be written later from the passport page.
The second half of the drawing is the question a label inside a file cannot answer. A reader asks the register what was in force on a given day, and gets that day's entry rather than today's.
Contract dependency graph
MoolamRegistry is built out of OpenZeppelin v5.6.1 and reads two contracts it does not own. It
holds IVerifierPolicy as an immutable address set at deployment and Monad's ERC-8004 Identity
Registry the same way. Every receiver contract inherits Chainlink's ReceiverTemplate verbatim,
vendored under src/vendor/chainlink/ so it can be diffed against Chainlink's published source. The
SimulationReceiver pair in use since 2026-09-27 takes onReport only from Chainlink's simulation
forwarder and only in a transaction Moolam's CRE wallet signed. MoolamReceiver takes it from the
Keystone Forwarder and from the workflow it is pinned to, which is how Chainlink's network writes. It
came off the policy on 2026-09-26 and went back on the list on 2026-10-02 at 07:08:18 UTC, in
transaction 0x446c3051…ff9f.
Its sealed twin has been off the list since 2026-09-26. MoolamConsent depends on
nothing but an IERC721 view: it holds the registry address as an immutable set at deployment and
asks it ownerOf, which is the whole integration. The arrow points one way, so the registry does
not know the consent register exists.
How the app reads the chain
Every chain read a page makes happens on the app's server, not in the browser, so one endpoint
answers for every visitor. Two endpoints can be named. NEXT_PUBLIC_MONAD_RPC_URL is the public
one and is the only address the browser is ever given. MONAD_RPC_URL is optional and secret: a
dedicated endpoint, such as a QuickNode one, whose address carries its own access token. When it is
set, the server tries it first and falls back to the public endpoint, so a rate-limited minute on
either side is a slower page rather than an error. It is read on the server only, it must be an
https URL or the app refuses to start, and the one line a page prints when a read fails has every
address taken out of it first.
What is immutable and what can change
MoolamRegistryhas no upgrade path. Records, edits and attestations are only ever added. The owner can pause new writes and rescue tokens sent by mistake; the owner cannot edit or delete a passport.MoolamConsenthas no upgrade path and no owner at all. Statements are only ever appended, so a new one supersedes an old one without hiding it, and nobody can pause it or take it back.VerifierPolicyis the only mutable piece: the list of receivers allowed to attest, the dispute resolver, the treasury and the bond. Every change is queued and executes 24 hours later, publicly, except removing a receiver, which is immediate so a compromised verifier can be cut off. Two changes ran through that queue on 2026-10-02:MoolamReceiverwas listed again at 07:08:18 UTC, and the treasury was set to the burn address0x000000000000000000000000000000000000dEaDat 07:27 UTC (transaction 0x45700954…371d), so a rejected challenge's bond is burned.- The ERC-8004 registry is Monad's canonical deployment. Moolam only reads it.
The exact function signatures, events and errors are in the
API reference. ABIs live in packages/contracts/abi/.