An open, onchain C2PA manifest repository. Find out who made an image, from the image itself — after screenshots, crops and re-encoding.
Live: grain-on-monad.vercel.app · Monad testnet · built for Metropolis, Track 04
Demo video: link goes here
| Where it is | See it live | |
|---|---|---|
| Track 04: Trust, Identity & AI | an open C2PA manifest repository on Monad: how it works, recovering real C2PA credentials after platforms strip them (measured), AI-generated and by whom (ERC-8004), measured | catch a forgery |
| Monad | Why Monad: on-chain search and proof, ~93k gas per image, 0.53 s to confirm, parallel execution burst-tested | record 535 |
| MetaMask Agent Wallet | the plugin: verify, license, pay, and agents registering their own work under ERC-8004; two MetaMask bugs we traced | npm, a licence approved by email |
| Mera: One Passkey, Many Keys | Accounts on Mera: one prompt gives an identity, unlinkable pen names and a private-notes key | /me, /register |
| Envio | How Envio powers Grain: creator portfolios, earnings and the live feed that the contracts can't list, every answer checked against the chain | /c/grain-studio, /sealed |
- Try it in 60 seconds
- The gap
- How it works
- Why Monad
- Accounts on Mera: one passkey, many keys
- How Envio powers Grain
- For AI assistants: the Grain MCP server
- For agents: one HTTP call
- For agents: the MetaMask Agent Wallet plugin
- Why creators come back
- Measured, not asserted
- Things we found, and fixed or stated
- Honest about conformance
- Run it yourself
- Credits
| Watch it catch a forgery | /verify?sample=forged: someone's watermark stamped on a different picture, flagged |
| Check any image on the web | paste a link on /verify, or right-click it with the browser extension |
| Register your own work | /register: one passkey prompt, no wallet, up to ten images at once |
| One passkey, many keys | /me: your identity, pen names and private notes from a single prompt (Mera) |
| A creator's earnings | /c/grain-studio: licences bought by an AI agent through MetaMask, indexed by Envio |
| History nobody can rewrite | /sealed: the activity log, sealed on Monad, checked in your browser |
| From an AI assistant | add https://grain-on-monad.vercel.app/api/mcp as an MCP connector (how) |
| As an AI agent | one call, no wallet: /api/v1/verify?url=… (API); or with MetaMask: mm plugins install mm-plugin-grain, then mm grain verify <image> (plugin) |
| AI-generated, by whom | /r/535: registered by an AI agent through its MetaMask wallet, credited to its ERC-8004 identity |
C2PA solved the format of content provenance: a Content Credential is a signed manifest saying where a piece of media came from. But every screenshot and re-upload strips it. C2PA's answer is the durable Content Credential — an invisible watermark or a content fingerprint that survives the copy, looked up through a Soft Binding Resolution API in a Manifest Repository.
The spec defines the lookup. It does not say who runs the repository. Today that is a handful of private companies, and a record can be dropped or lost when one shuts down.
Grain is that repository: open, permissionless, on Monad. Nobody can delete your record, anyone can check an answer against it, and it works for people who have never touched crypto — one passkey prompt, no wallet, no seed phrase.
It is not NFTs. Records are found by the content itself, ten thousand copies of an image resolve to one record, and nothing is minted or traded. There is no ERC-721 anywhere in the project.
flowchart LR
A[Image dropped in] --> B[Fingerprint<br/>64-bit DCT hash]
A --> C[Watermark decode<br/>TrustMark]
B --> D[LSH candidates<br/>Envio indexer, chain fallback]
C --> E[Record the mark<br/>points to]
D --> F{resolve}
E --> F
F --> G[RESOLVED]
F --> H[UNCERTAIN]
F --> I[TAMPERED]
F --> J[NOT FOUND]
Two co-equal paths, run in parallel on every image. A TrustMark watermark carries the record id; a perceptual fingerprint finds the record by what the picture looks like. Neither is a fallback — they fail in opposite directions, so each catches what the other drops (measured below).
The anti-spoof check. A watermark payload is not authenticated: anyone can stamp any record id onto any image. So the manifest also stores the fingerprint, and if the mark says one thing and the picture says another, Grain reports TAMPERED instead of attributing someone else's work. That is what C2PA's own guidance prescribes.
AI-generated, and by whom. At registration the creator says how the image was made, stored as C2PA's own digitalSourceType (camera, made by hand, or AI-generated with any tool). An AI agent can go further and name its ERC-8004 identity in the manifest. Grain shows "AI-generated by agent · identity checked on chain" only when the record's signer controls that agent in Monad testnet's standard IdentityRegistry, re-checked on every view; a claim that doesn't check out is shown as unverified. Examples: record 534, by agent #2095, and record 535, registered by an agent through its own MetaMask wallet with mm grain register.
The durable layer for credentials that already exist. Images from OpenAI, Adobe Firefly and a growing number of cameras arrive carrying a signed C2PA manifest, and every copy loses it. When such an image is registered, Grain reads that manifest in the browser (Adobe's c2pa-web), validates it against the official C2PA trust list, and signs a summary into the record: who signed it, with what tool, when, whether it validated, and the SHA-256 of the exact original file. Any later copy -- a screenshot, a WhatsApp forward -- then leads back to it: "originally signed by OpenAI (ChatGPT), trusted at registration". Measured on real ChatGPT images through WhatsApp: 7 of 7 copies resolved, OpenAI's credential recovered for each, while c2patool found none in any copy.
Envio is the read path; the chain is the judge. Every lookup is answered by an Envio indexer and checked against the contracts — how, in detail, below.
Everything runs in the browser. Watermarking, fingerprinting and resolution happen on the person's device and the image is never uploaded; the browser reads Monad directly. The only server code is a small faucet that funds a new passkey account's first transactions.
- The registry searches itself, and proves the answer. The fingerprint index lives on chain: anyone can find an image's candidates with
queryBandand prove a match withFingerprintIndex.verify(), 8,687 gas, with no server involved. Envio makes reads fast, but the chain alone can always answer. That only stays practical with room to grow: at a million records the busiest fingerprint bucket costs about 11.5M gas to read in one call, under a tenth of Monad's 150M-gas block. - One record per image is only viable where writes are cheap. Each registration stores a record and eight index entries: ~93k gas once the index is warm. Manifests live in event data rather than storage, so a 16 KB manifest costs only 2.7× a 256 B one.
- Fast enough to disappear into a product. Measured on Monad testnet: blocks about every 0.3 s, and a median 0.53 s from sending a transaction to its confirmed receipt. The registration screen shows the real figure every time ("Confirmed on Monad in 0.6 s"). Watermarking on the device is the slow part, not the chain.
- Nobody can retract your provenance, and block order settles who registered first.
- Built for parallel execution, and measured. We burst-tested registration on Monad testnet: under v1's shared id counter, 20 simultaneous creators land 1 per round; GrainRegistryV2, which gives each creator its own id range, lands all 20 in two blocks. The live registry handles a collision today by retrying with the next id automatically, so no creator sees a failure. v2 removes collisions entirely: it's built, tested, and burst-tested on Monad testnet. Moving the live registry onto it is the first post-hackathon milestone, because every existing watermark points into v1. Method and results in docs/PARALLEL.md.
Identity is built entirely on Mera, Monad's passkey account layer: no seed phrase, no browser extension, no custody backend. One passkey, one prompt, many keys. A single passkey assertion returns one PRF output, and everything is derived from it in the browser, then wiped:
| Key | What it's for |
|---|---|
Identity (m/44'/60'/0'/0/0) |
the creator's account; signs every manifest and transaction |
Pen names (m/44'/60'/1'/0/n) |
publish under another name; each is its own account, unlinkable on chain to your identity or to each other |
Private-notes key (HKDF, grain.v1.private-notes) |
an optional note on each record (where it was shot, the client), sealed with AES-256-GCM before it leaves the device; only your passkey can read it |
Pen-link key (HKDF, grain.v1.pen-link) |
never signs anything: it makes each pen name's secret tag. A pen name records keccak256(tag ‖ identity) on chain, which is noise to everyone. To prove one pen name is yours, to a client, an editor, a court, share a proof link: anyone's browser checks it against Monad. Your other pen names stay unlinkable. |
Signing keys run as Mera secp256k1 signing sessions through Mera's toViemAccount, so each key is zeroed when its session ends. Nothing derived is ever stored: at Your work, one prompt on any device recovers your identity, finds every pen name by asking the chain, shows each one's records and earnings, and opens your private notes. The identity path goes through BIP-39, so the same account imports into MetaMask or Rabby: nobody is locked into Grain to control their own identity.
A registry is only useful if you can search it. The contracts are built to store provenance cheaply and to prove an answer; they are not built to search. Envio HyperIndex is the layer that turns Grain's onchain events into something you can query in one round trip — and every screen that reads the registry is served by it.
Three properties of the contracts, each chosen for good reasons, make direct reads slow or impossible:
- Manifests live in event data, not storage. That is what makes a 16 KB manifest cost only 2.7× a 256 B one. But it means reading a manifest back is a log search — and Monad's public RPC caps
eth_getLogsat 100 blocks per call, so finding one record's event means first estimating which 100 blocks to look in. - The fingerprint index is eight separate buckets. Finding every record that might match an image means eight
queryBandcalls, then arecords()read for every id they return. - Some questions have no onchain answer at all. "What has @grain-studio registered?" and "What was registered in the last hour?" would mean reading every record ever made. There is no mapping to read, by design: indexes like those would make every registration more expensive for a question only readers ask.
The indexer (packages/indexer) follows five events across the three contracts and maintains four entities:
| Event | Becomes |
|---|---|
GrainRegistry.ManifestRegistered |
a Record (fingerprint, creator, the full CBOR manifest, block, tx hash) plus eight FingerprintBand rows |
GrainRegistry.ManifestSuperseded / ManifestRevoked |
the record's supersededBy / revoked, copied onto its band rows |
CreatorRegistry.ProfileSet |
a Creator with handle, licence price and record count |
LicenseRegistry.LicenseGranted |
a License |
The band rows are the core design decision. The natural schema is an array of eight band values on each record, searched with an array-overlap operator. Instead, each band value is its own row keyed band:value (for example 3:217), with revoked denormalised onto it. A fingerprint search becomes one indexed key IN (8 keys) lookup — a plain B-tree hit that returns the candidate records, their fingerprints and their creators' handles in a single GraphQL response.
| Screen | Served by Envio | Without Envio |
|---|---|---|
| Verify — "who made this image?" | the candidate search: one indexed query, returning each candidate's record in the same response | 8 band reads + a read per candidate; batched into a multicall it is about as fast, so speed is not the main reason |
Record page /r/:id |
the manifest, block and transaction in one query | an estimated block window, then a getLogs search |
Creator page /c/:handle |
every record under a name, newest first | not possible without scanning every record |
| Landing — "Recently registered" | the latest records and registry totals, live | not possible without scanning every record |
| Register | — | writes go straight to the chain |
The last two rows are why Envio is part of the product rather than an optimisation: a creator's portfolio and a live feed of the registry simply don't exist without it.
An indexer is a database someone runs, and Grain's whole argument is that provenance shouldn't rest on a database you have to trust. So the index proposes and the chain decides:
- Record pages re-read the record from contract storage — creator, fingerprint, revocation — and take only the manifest from Envio. That manifest is hashed and compared with the
manifestHashthe contract stores before anything is shown. A wrong or tampered index cannot change what a record page says; it can only make the check fail, and the page shows that check. - Every verify result has "verify on chain". One click calls
FingerprintIndex.verify()from the visitor's browser and shows the distance the contract computes between the record and the image in front of them. It never touches the indexer. - The record page says where its data came from — "Found via: Envio indexer, checked against the contract" — so the trust model is visible, not asserted.
- Lag. An index trails the chain by a few seconds. If Envio has no candidate close to an image, verify asks the chain before saying "no record", so an image registered moments ago is never reported as unregistered. A match returns straight from the index; only the not-found path pays for the check.
- Outage. Every query has a 4-second timeout and falls back: verify and record pages go to the chain and keep working, and the creator page says the listing is temporarily unavailable rather than claiming the creator has no work.
Envio Cloud deploys the indexer from this repository's envio branch (root directory packages/indexer). It reads Monad testnet through HyperSync, starting from the contracts' deployment block. The app reads the endpoint from NEXT_PUBLIC_ENVIO_GRAPHQL_URL; locally, pnpm --filter @grain/indexer dev runs the same indexer against the same contracts. Measurements and schema notes are in docs/INDEXER.md.
Any assistant that speaks the Model Context Protocol, such as Claude, ChatGPT, Cursor or an agent framework, can ask Grain who made an image before it reposts, attributes or pays for it. Point it at one URL; there is no install, account or key:
https://grain-on-monad.vercel.app/api/mcp
| Tool | What the assistant gets |
|---|---|
verify_image |
who made an image (by URL or inline), forged credentials flagged, and the distance Monad's contract returned |
get_record |
a record's creator, how it was made (photo, artwork, AI-generated, or which ERC-8004 agent), its Monad block, whether its signed manifest checks out, and its licence price and terms |
get_creator |
a creator's images and what licences of their work have earned (via Envio) |
check_pen_proof |
whether a pen-name proof checks out against Monad |
get_licence_terms |
what a Grain licence lets you do |
The server's instructions tell the assistant to check before it attributes, and never to credit a TAMPERED image to the creator it imitates. Every tool runs the website's own code, so an assistant and a person get the same answer.
claude mcp add --transport http grain https://grain-on-monad.vercel.app/api/mcp # Claude Code
In Claude or ChatGPT, add it as a custom connector with the same URL. Clients that only speak stdio can use npx -y mcp-remote https://grain-on-monad.vercel.app/api/mcp.
Checking an image needs no wallet, no account and no SDK. Any agent, in any framework, can ask:
curl "https://grain-on-monad.vercel.app/api/v1/verify?url=https://grain-on-monad.vercel.app/samples/forged.jpg"
curl -X POST https://grain-on-monad.vercel.app/api/v1/verify --data-binary @photo.jpg
{
"state": "TAMPERED",
"summary": "Forged credential: the image carries @grain-studio's watermark, but the picture does not match what they registered. Do not attribute it to them.",
"record": { "recordId": "511", "creatorHandle": "grain-studio", "url": "https://grain-on-monad.vercel.app/r/511" },
"watermark": "found",
"onChainDistance": 32,
"candidatesFrom": "indexer"
}state is RESOLVED, UNCERTAIN, TAMPERED or NOT_FOUND; madeWith carries how the image was made, including a checked ERC-8004 agent ("AI-generated by Grain Demo Agent (ERC-8004 agent #2105, checked on chain)"). It runs the plugin's resolver unchanged, so the website, mm grain verify and this endpoint give the same answer, and onChainDistance is what FingerprintIndex.verify() returned on Monad, so a caller can check the answer without trusting our server. Uploads up to 4 MB, links up to 15 MB; the image is checked in memory and never stored. About 1.5 s per check once warm.
Paying a creator needs a wallet. That's what the plugin is for.
Agents now choose images to post, buy and build on, and they can't tell where an image came from. mm-plugin-grain adds Grain to MetaMask's agent CLI:
mm grain verify <image> who made this image? (JSON verdict, forged credentials flagged)
mm grain license <image|record> license it at the creator's price, through LicenseRegistry
mm grain pay <image|record> <n> pay that person any amount, through the agent's MetaMask wallet
mm grain record <id> look up a record
mm grain creator <handle> everything a creator has registered (via Envio)
mm grain agent <name> give this agent an ERC-8004 identity
mm grain register <image> register an image it generated, credited to that identity
- Same answer as the website. The plugin bundles Grain's own resolver and the site's TrustMark port, so the CLI and the site cannot disagree about an image. Each verdict carries the distance
FingerprintIndex.verify()returned on chain. - Find the human, then license from them. Creators can set a licence price when they register.
licensebuys the Grain Licence v1 (worldwide, non-exclusive use, personal or commercial, credit the creator, no reselling the file) throughLicenseRegistry, which forwards every wei to the creator and records the licence on chain; Envio indexes it, and the record page shows the price and the count.--max-pricestops an unattended agent overpaying. - Or just pay them. A creator's Grain account is an ordinary EOA, so the same address receives on any EVM chain.
paysends on Monad testnet by default, where the registry lives, through MetaMask's policy-gated executor;--chain-idpays on any other chain the wallet supports. - Agents that make images, not just use them.
agentmints an ERC-8004 identity owned by the agent's MetaMask wallet.registerwatermarks, fingerprints, signs and registers an image the agent generated, declared AI-generated and credited to that identity, and refuses an agent id the wallet doesn't control.license --rate 5closes the loop: after paying for an agent's image, the licensee leaves feedback for that agent on ERC-8004's ReputationRegistry. - Strict about money.
payacts only on a clean match or an explicit record number. A forged watermark, an uncertain match or a withdrawn record is refused with the reason.
A registry people visit once doesn't build coverage. So Grain gives creators reasons to return, and every one of them also brings in new people:
- Earnings. A creator's page (
/c/:handle) shows MON earned and licences sold, each linked to its transaction. The figures are sums ofLicenseGrantedevents, served by Envio. - Activity. Every time someone checks a copy of their work, the creator sees it: checks this week, and forged copies caught. "3 new since your last visit" is the reason to come back. A check reports only a record number and a verdict, never the image.
- Sealed on Monad. The activity log is sealed on chain by
ActivityAnchor, an append-only contract: each seal commits to the keccak256 of every event since the last one, and none can ever be replaced. At /sealed, anyone's own browser downloads the log, hashes it and compares it with the seal it reads from Monad. Delete, edit or backdate one entry and the check fails, so not even Grain can rewrite a creator's history. - Share cards. Every record and creator link unfolds into a card naming the creator, with the image's fingerprint drawn large. Sharing your record is how other people find Grain.
- Batches. Up to ten images with one passkey prompt, and the marked copies come back as one zip.
- The browser extension. Right-click any image on the web, then "Who made this image?" (
extensions/chrome, 32 lines, whose only permission is the right-click menu). Or paste any image link on the check page.
| Transformation | Watermark recovered | Fingerprint distance (median / max) | Resolves |
|---|---|---|---|
| None | 100% | 0 / 0 | 100% |
| Screenshot | 100% | 0 / 0 | 100% |
| JPEG quality 40 | 76% | 0 / 2 | 100% |
| JPEG quality 20 | 43% | 0 / 4 | 100% |
| Downscale 50% / 25% | 100% | 0 / 2 | 100% |
| Crop 10% | 100% | 12 / 28 | 100% |
| Gaussian blur | 100% | 0 / 2 | 100% |
| Social filter | 43% | 2 / 8 | 95% |
| Screenshot + JPEG 40 | 71% | 0 / 2 | 100% |
| Crop 25% | 19% | 24 / 34 | 19% |
JPEG 20 defeats the watermark but not the fingerprint; a 10% crop defeats the fingerprint but not the watermark. That is the case for two paths, in one table. A 25% crop defeats both — no threshold fixes it, because at that distance the picture genuinely is a different picture to a global hash. We say so rather than claim 100%. Full method in docs/ROBUSTNESS.md.
| Records on chain | 530+: 500 from a public photo corpus, labelled @seed-corpus; the rest registered through the app |
| LSH recall, 1,000 records, ≤7 bits flipped | 100% — the pigeonhole guarantee behind the 8 × 8 band geometry |
| Verify, end to end in the browser | ~2 s |
| First answer on a cold first visit | 5.8 s — the fingerprint answers while the watermark model downloads |
| Candidate fan-out through the Envio indexer | 90 ms vs 980 ms for 8 separate chain reads (25 Sep); against the same 8 reads batched into one multicall, about even (≈200 ms each, 11 Oct). Envio's value is the answers the chain can't give, below |
Gas figures in docs/GAS.md.
- Adobe's TrustMark implementations disagree. Grain's watermarks have to be readable by any TrustMark decoder, so we checked against all three Adobe ships. For the BCH_SUPER error-correction scheme, Adobe's Python reference and JavaScript library produce identical parity, but the Rust crate does not: it pads 40 data bits to 6 bytes where Python uses 5, and its leftover-byte loop drops Python's
pidx += 1. So BCH_SUPER marks don't read across implementations. Grain ships BCH_5, which they all agree on: our browser encoder's codewords are bit-identical to Adobe's Python reference, and the Rust CLI reads them. - The tamper threshold in our own spec was wrong. At the planned value of 12, a third of legitimate 10% crops were flagged as stolen credentials. Measured against 210 unrelated image pairs, 16 catches every genuine transfer while nearly eliminating false alarms.
- MetaMask's Agent Wallet lists Monad testnet but can't send on it out of the box.
mm chains listincludes Monad Testnet (10143), yet two MetaMask services behind the wallet answerInvalid chainId(HTTP 400) for it: the gas-fee estimate API, and the Infura proxymmuses as the chain's RPC, so the block tracker never starts. Every testnet transaction fails before it's sent. We traced both inmm's code and worked around them with its own mechanisms: the plugin suppliesgasand fee caps read from Monad's RPC (mmskips its fee API when they're present), and adds onecustomEvmChainsentry pointing chain 10143 athttps://testnet-rpc.monad.xyz(mmchecks custom chains before its proxy). The first licence through MetaMask's server wallet went through on that path:0xc9e76b7b…6b17. Details in plugins/mm-grain. - The onchain index has a ceiling. Over 913 real photos, the busiest LSH bucket held ~5× the average. Projected to a million records, reading it costs ~11.5 M gas in one call — inside a 150 M block limit, but within an order of magnitude. The production path is search off chain through the indexer, then prove the winner on chain with
verify().
Grain is C2PA-compatible, not C2PA-certified. It implements the soft-binding assertion structure and the Soft Binding Resolution API shape; it does not run the c2pa-rs signing stack with certificates from the C2PA trust list. Our fingerprint algorithm, grain.phash.v1, is ours and not on the C2PA approved list — manifests say so with algId: 0. TrustMark Q is on it, as algId: 4.
Register before you publish. Grain can't stop someone registering a picture they didn't make; no registry can check authorship at upload time. If two accounts register the same picture, Grain credits the earliest, because the chain's timestamp is the only neutral referee. So the rule for creators is simple: register before you post. A copier can only copy what's already public, so a creator who registers first always wins, and a copier who registers later leaves a permanent, timestamped claim against themselves.
Two things that aren't fully local. Checking an image by link fetches that public image through Grain's server, because most sites don't let a browser read their images directly; the route is locked to public addresses, 15 MB and 10 seconds, and stores nothing. And the activity creators see is reported by visitors' browsers: the creator is looked up from the chain, so a report can't name someone else, but no chain can see a check that happened privately in a browser, so a report itself can't be proven. What the chain does guarantee is that the log can't change after it's sealed.
Who pays on mainnet. A passkey account starts with no funds, and the no-wallet promise means a photographer can't top it up, so on testnet a small faucet pays for a new creator's first transactions. On mainnet we'll sponsor them through a gas sponsor (paymaster), capped per account; at ~93k gas a registration, that's affordable for thousands of creators. It's funded at first by ecosystem grants, then by paid high-volume tiers of the verify API and MCP server for platforms, newsrooms and AI companies. People check for free, creators register for free, and every licence is paid 100% to the creator. The sponsor isn't built yet; testnet runs on the faucet.
All five contracts are verified on Sourcify, so their source is readable on the explorer.
| Contract — Monad testnet, chain 10143 | Address |
|---|---|
| GrainRegistry | 0x180eC6A4FaF1a081e3eE3Fb4540fd3Df6d1A4c20 |
| FingerprintIndex | 0x68A3c6A0654af33b9f0E6Ba52ccc44B0C10e7203 |
| CreatorRegistry | 0x783159d464B15180935222b342aEDc3Cf5ff2DE7 |
| LicenseRegistry | 0x71260B7e406566D0fd880320b58537eE27895B1E |
| ActivityAnchor | 0x4F728dd37Bc38Db5C594ff17c8FC2dbc7Ed28e56 |
pnpm install
./scripts/fetch-web-models.sh # TrustMark models + WASM runtime, served by the app
pnpm --filter @grain/web dev
node --experimental-strip-types --test packages/grain-core/test/*.test.ts # 47 tests
cd packages/contracts && forge test # 56 tests: 46 unit, 6 fuzz (1,000 inputs each), 4 invariants (4,096 random actions each)
cd packages/contracts && forge snapshot --check --no-match-path test/Properties.t.sol # gas regressions, also in CIRegistering needs a passkey that supports the WebAuthn PRF extension — Safari with iCloud Keychain, Chrome with Google Password Manager, Android, or 1Password. Without one, Grain offers to keep a key in the browser instead, and says what that gives up before you choose it.
| docs/ROBUSTNESS.md | the robustness matrix and thresholds |
| docs/GAS.md | gas, and the LSH scaling ceiling |
| docs/INDEXER.md | the Envio indexer |
| docs/THREAT_MODEL.md | what Grain proves and doesn't, every attack we know of, and its status |
| docs/SURVIVAL.md | real ChatGPT images through WhatsApp: C2PA gone from every copy, recovered by Grain |
| docs/PARALLEL.md | registration under Monad's parallel execution, burst-tested |
| packages/grain-core | fingerprint, manifest, resolution — isomorphic TypeScript |
| packages/contracts | the five contracts |
| apps/web | the app, including the browser TrustMark port |
| plugins/mm-grain | the MetaMask Agent Wallet plugin |
| extensions/chrome | the browser extension |
TrustMark models and BCH library by Adobe, MIT. Seed corpus photographs via Lorem Picsum / Unsplash — only their fingerprints are stored, never the images. Illustrations by unDraw.
MIT © 2026 Jerry Musaga, Musaga Technology