Skip to content
Musaga-TechnologyPublic

About

An open, onchain C2PA manifest repository with a soft-binding resolver. Find who made an image from the image itself — after screenshots, crops and re-encoding. Built on Monad.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Grain

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

CI npm contracts verified

Demo video: link goes here

For judges

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

Contents

Try it in 60 seconds

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

The gap

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.


How it works

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]
Loading

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.


Why Monad

  1. The registry searches itself, and proves the answer. The fingerprint index lives on chain: anyone can find an image's candidates with queryBand and prove a match with FingerprintIndex.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.
  2. 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.
  3. 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.
  4. Nobody can retract your provenance, and block order settles who registered first.
  5. 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.

Accounts on Mera: one passkey, many keys

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.


How Envio powers Grain

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.

Why the chain alone isn't enough

Three properties of the contracts, each chosen for good reasons, make direct reads slow or impossible:

  1. 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_getLogs at 100 blocks per call, so finding one record's event means first estimating which 100 blocks to look in.
  2. The fingerprint index is eight separate buckets. Finding every record that might match an image means eight queryBand calls, then a records() read for every id they return.
  3. 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.

What the indexer builds

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.

Where it's used

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.

Why you don't have to trust 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 manifestHash the 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.

When it's behind, or down

  • 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.

Running it

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.


For AI assistants: the Grain MCP server

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.


For agents: one HTTP call

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.


For agents: the MetaMask Agent Wallet plugin

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. license buys the Grain Licence v1 (worldwide, non-exclusive use, personal or commercial, credit the creator, no reselling the file) through LicenseRegistry, 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-price stops 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. pay sends on Monad testnet by default, where the registry lives, through MetaMask's policy-gated executor; --chain-id pays on any other chain the wallet supports.
  • Agents that make images, not just use them. agent mints an ERC-8004 identity owned by the agent's MetaMask wallet. register watermarks, 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 5 closes the loop: after paying for an agent's image, the licensee leaves feedback for that agent on ERC-8004's ReputationRegistry.
  • Strict about money. pay acts 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.

Why creators come back

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 of LicenseGranted events, 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.

Measured, not asserted

Robustness — 21 real images × 11 transformations

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.

The registry

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.


Things we found, and fixed or stated

  • 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 list includes Monad Testnet (10143), yet two MetaMask services behind the wallet answer Invalid chainId (HTTP 400) for it: the gas-fee estimate API, and the Infura proxy mm uses as the chain's RPC, so the block tracker never starts. Every testnet transaction fails before it's sent. We traced both in mm's code and worked around them with its own mechanisms: the plugin supplies gas and fee caps read from Monad's RPC (mm skips its fee API when they're present), and adds one customEvmChains entry pointing chain 10143 at https://testnet-rpc.monad.xyz (mm checks 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().

Honest about conformance

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.


Run it yourself

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 CI

Registering 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

Credits

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

About

An open, onchain C2PA manifest repository with a soft-binding resolver. Find who made an image from the image itself — after screenshots, crops and re-encoding. Built on Monad.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages