mm-plugin-grain
v0.1.4
Published
Grain for the MetaMask Agent Wallet: find out who made an image, then license it or pay them
Maintainers
Readme
mm-plugin-grain
Grain for the MetaMask Agent Wallet. Before an agent reposts, publishes or pays for an image, it can ask who made it, from the image itself, even after screenshots, crops and re-encoding. Then it can license the image at the creator's price, or pay them directly. And an agent that generates images can register them, credited to its own ERC-8004 identity.
mm grain verify <image> who made this image?
mm grain license <image|record> license it at the creator's price, on chain
mm grain pay <image|record> <n> pay them any amount, through your MetaMask wallet
mm grain record <id> look up a record
mm grain creator <handle> everything a creator has registered
mm grain agent <name> give this agent an ERC-8004 identity
mm grain register <image> register an image it generated, credited to that identityWhy an agent needs this
Agents increasingly pick images to post, buy or build on, and they have no way to tell where an image came from. Content credentials (C2PA) are stripped by the first screenshot. Grain is an open onchain registry that finds them again: an invisible TrustMark watermark and a perceptual fingerprint, cross-checked so that a watermark copied onto someone else's picture is reported as forged rather than believed.
This plugin gives an agent the same answer the website gives, as JSON, and closes the loop with MetaMask: find the human who made it, then license it from them.
Commands
mm grain verify <image>
A local PNG/JPEG path or an http(s) URL. No sign-in or wallet needed.
$ mm grain verify https://grain-on-monad.vercel.app/samples/forged.jpg --json
{
"ok": true,
"data": {
"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.",
"watermark": "found",
"candidatesFrom": "indexer",
"onChainDistance": 32,
...
}
}| state | Meaning | What an agent should do |
|---|---|---|
| RESOLVED | Made by the creator in record | attribute it, or pay them |
| UNCERTAIN | Heavily edited; probably that creator | attribute with care |
| TAMPERED | A watermark copied onto a different picture | do not attribute it |
| NOT_FOUND | Not registered, or edited past recognition | treat as unknown |
onChainDistance comes from FingerprintIndex.verify() on the contract itself, not from the indexer, so the agent never has to trust Grain's servers for the answer.
The first run downloads the 45 MB watermark decoder once into ~/.cache/grain/models. --fast skips it and matches by fingerprint alone, which also means it cannot detect forged watermarks.
mm grain license <image|record>
Licenses the image through Grain's LicenseRegistry on Monad testnet, at the price the creator set when they registered. The contract forwards the full amount to the creator (there is no protocol fee) and records the licence on chain, where Grain's Envio indexer picks it up. Needs mm login, wallet-submit and wallet-read.
$ mm grain license https://grain-on-monad.vercel.app/samples/reposted.jpg --max-price 0.05 --dry-run
Licensing record 511 costs 0.01 MON under the Grain Licence v1 (https://grain-on-monad.vercel.app/licence), paid in full to @grain-studio. Nothing was sent.- What it buys: the Grain Licence v1: worldwide, non-exclusive use in any medium, personal or commercial; credit the creator where practical; don't resell the file or claim you made it. The approval MetaMask sends names the licence, and the result carries
termsso the agent knows exactly what it bought. --max-pricerefuses if the creator asks more. Use it for any unattended agent.- Same strictness as
pay: forged, uncertain and withdrawn matches are refused, and so is a creator with no price set (the error suggestspayinstead). --rate 1-5: if the image was generated by an ERC-8004 agent whose identity checks out, leave that agent feedback on the standard ReputationRegistry once the licence confirms. A second approval. An agent can't rate itself.mm grain record <id>shows the price and how many licences have been granted.
mm grain pay <image|record> <amount>
Finds the creator, then sends them the amount through MetaMask's policy-gated wallet executor. Needs mm login, and the wallet-submit and wallet-read capabilities, which you approve at install (wallet-read lets it add the Monad testnet RPC entry described below).
$ mm grain pay ./downloaded.jpg 0.5 --dry-run
Would send 0.5 on chain 10143 to @grain-studio (0x5c71…d7C8), the creator of record 511. Nothing was sent.- Strict on purpose. It pays only on a clean
RESOLVEDmatch or an explicit record number. A forged or uncertain match is refused with the reason, and so is a record its creator has withdrawn. - Any chain your wallet supports. A Grain creator's account is an ordinary EOA derived from their passkey, so the same address receives on every EVM chain. Payments default to Monad testnet (10143), where the registry lives;
--chain-id 143pays on Monad mainnet, or any other chain your wallet supports. --dry-runshows the exact transaction without sending it.
mm grain agent <name> and mm grain register <image>
For agents that generate images. agent mints an ERC-8004 identity on Monad testnet's standard IdentityRegistry (0x8004A818…BD9e), owned by this MetaMask wallet, with its registration file embedded as a data: URI. register does what the website does: embeds an invisible watermark carrying the next record id, fingerprints the marked image, has the wallet sign the manifest, and registers it, declared AI-generated (C2PA trainedAlgorithmicMedia) and naming the agent. It saves the marked copy, which is the one to publish.
$ mm grain agent "Grain Demo Agent" --description "Generates illustrations"
Created ERC-8004 agent #2105 "Grain Demo Agent", owned by 0x6bA5…fF60.
$ mm grain register ./generated.png --agent-id 2105 --title "Provenance Generation"
Registered record 535, credited to ERC-8004 agent #2105.
Watermarked copy: generated-grain.png (publish this one, not the original)registerrefuses an agent id this wallet doesn't control, so an agent can't credit its work to someone else's identity. Anyone checking the image sees "AI-generated by Grain Demo Agent · ERC-8004 agent #2105 · identity checked on chain" (record 535), and the site re-checks that on every view.- The first run downloads the 17 MB watermark encoder once. Two approvals: signing the manifest, then the transaction.
mm grain record <id> and mm grain creator <handle>
Lookups. creator lists a creator's records through Grain's Envio indexer, because the contracts alone cannot list them.
Install
mm config set experimentalPlugins true
mm plugins install mm-plugin-grainOn npm 12, mm plugins install fails with PLUGIN_NOT_FOUND for every npm package, not just this one: mm reads npm view <package> --json, which npm 12 now returns as an array where mm expects an object. Run the install with npm 11 (only the install needs it):
PATH="$(dirname "$(npx -y -p npm@11 which npm)"):$PATH" mm plugins install mm-plugin-grain
# or simply: npm install -g npm@11From this repository:
cd plugins/mm-grain && npm install && npm run build
npm run link-host # use the installed mm's own CLI package, not a copy (see link-host.mjs)
mm config set experimentalAllowUnverifiedInstalls true
mm plugins install "file:$PWD" --accept-permissionsSkip link-host and the plugin loads a second copy of the CLI, which crashes with window.addEventListener is not a function. Installs from npm don't need it; mm links its own copy for those.
Monad testnet and MetaMask's wallet
mm lists Monad Testnet (10143) as a supported chain, but two MetaMask services it relies on reject it with Invalid chainId (HTTP 400): the gas-fee estimate API, and the Infura proxy it uses as the chain's RPC. So out of the box, no transaction can be sent on 10143. Grain works around both using mm's own mechanisms:
- Fees: on testnet the plugin sets
gas,maxFeePerGasandmaxPriorityFeePerGasitself, read from Monad's RPC (gas estimate plus 25%; Monad charges the whole limit).mmskips its fee API when these are present. - RPC: before the first real submission on testnet, the plugin adds one entry to the wallet's
customEvmChains, Monad Testnet withrpcTarget: https://testnet-rpc.monad.xyz.mmchecks custom chains before its default proxy. It's added once and only if missing, and the command says when it does it. A dry run never changes anything.
First licence through this path: 0xc9e76b7b…6b17, approved by email through MetaMask's server wallet.
How it works
The plugin bundles Grain's own resolver (packages/grain-core) and the website's browser TrustMark port, run on ONNX Runtime's WASM backend in Node. The verdict logic is the same code the website runs, so the CLI and the site cannot disagree about an image. Candidate lookups go to the Envio indexer and fall back to Monad's RPC.
| Variable | Default |
|---|---|
| GRAIN_INDEXER_URL | Grain's Envio Cloud endpoint |
| GRAIN_RPC_URL | https://testnet-rpc.monad.xyz |
| GRAIN_SITE | https://grain-on-monad.vercel.app (model downloads, record links) |
MIT
