Know who published a contract before you sign it.
Try it · Demo · Docs · How it works · Live on Sepolia · Security model
Codemark checks that the contract or account upgrade you are about to approve was released by the team you think, under their ENS name, and that the team still stands behind it.
Paste a Sepolia contract address at codemark.hetairoi.xyz, or open the demo to see each result with a sample team. You don't need a wallet.
A wallet shows you 20 bytes and asks you to sign. In February 2025, Bybit's Safe signers approved a transaction that handed their wallet to an attacker's contract, because the web page that built the transaction had been swapped. Nothing on the signing screen could say "this is not code your team released".
An account upgrade (EIP-7702) is worse: one signature lets a contract control your whole account.
Every check ends in one of four verdicts: Verified, Released recently, Not verified or Don't sign, with the reason.
Below the verdict is the path it followed: publisher, release key, release, record, age. The step that failed is marked in red.
If a team's release key leaks, one transaction withdraws everything that key released, and every check after that shows it.
| You are | Use | What you need |
|---|---|---|
| Anyone about to sign | The lookup site at codemark.hetairoi.xyz | A browser. No wallet, no sign-up |
| A MetaMask user | The Codemark Snap, which checks every transaction before MetaMask asks you to confirm | MetaMask Flask for now |
| Running an AI agent that spends | The Codemark MCP server, which tells your agent to proceed, ask you or refuse before it signs | Bun and an MCP-capable agent. It holds no keys |
| A team shipping contracts | The CLI and the GitHub Action, which publish each release under your ENS name | Sepolia ETH. init registers the name and sets up the team Safe for you |
In your wallet. The Snap adds its result to the MetaMask confirmation. This is a real check of the demo router in MetaMask Flask.
For your agent. A real Claude Code session using the MCP server on Sepolia: proceed for a release from a team the user pinned, refuse for an account upgrade nobody released. No signer was attached, so nothing was signed.
The full guide explains every command. The short version:
Check a contract
Open codemark.hetairoi.xyz/app, paste an address, and read the verdict. Pin the publishers you trust; only they can mark a contract as verified.
Check every transaction in MetaMask
Install MetaMask Flask in a separate browser profile, open the lookup site and select Install the Snap. It installs npm:@marsian/codemark-snap.
Protect an agent that spends
Nobody reads the signing screen when an agent signs, so a web page can hand it a lookalike router or an attacker's account upgrade. The MCP server checks the contracts behind each approval, swap and account upgrade first. Get a pin with bunx @marsian/codemark-mcp pin pwnmeswap.eth, then add the server to your agent:
{
"mcpServers": {
"codemark": {
"command": "bunx",
"args": ["@marsian/codemark-mcp"],
"env": {
"CODEMARK_PINS": "pwnmeswap.eth=0x8a8d97f3F4d1235c62aE61cDB3644f574162d204",
"ARKIV_ACCESS_KEY": "your-key"
}
}
}
}Get a free key from the Arkiv hub. Without one, the server queries Arkiv's public endpoint, and once its query budget runs out, checks can time out and return refuse.
Then tell your agent to use the server, for example in its CLAUDE.md: "Before you sign anything, call Codemark's check_transaction or check_signature and do what its action says." In our three test runs, an agent that wasn't told to check never called the tool.
Publish your releases
You need Bun 1.4.2 or newer and three keys: a root key that owns the team Safe, a security key that can only withdraw things, and a CI key that can only add releases. Put them in .env in the folder you run the CLI from (CODEMARK_ROOT_KEY, CODEMARK_SEC_KEY, CODEMARK_CI_KEY), then:
bunx @marsian/codemark-cli init pwnmeswap --years 1 # register pwnmeswap.eth, deploy the Safe and its registry
bunx @marsian/codemark-cli add-key pwnmeswap.eth ci-1 --ci 0x... # let your CI address publish under ci-1
bunx @marsian/codemark-cli publish pwnmeswap.eth ci-1 --component router --version 3.1.0 --target 0x... # release a deployed contract
bunx @marsian/codemark-cli verify 0x... --pin pwnmeswap.eth # check it, the same way the site and the Snap doThere is nothing to install: bunx fetches the CLI when you run it.
If something goes wrong, pull withdraws one release and cut withdraws everything a key released, in one transaction each. The CLI reference lists every flag.
A publisher is an ENS name. Each signing key gets its own branch under that name, and each release is a name on that branch, recording the contract's address and code hash. A check walks that path on Sepolia, so the first step that fails is the one it shows you.
If a key leaks, the team's security key removes its branch in one transaction, and every release under it stops verifying. Everything on chain is stock ENSv2; there is no custom Solidity.
How it works in detail covers the name tree, what each key can do and what a check proves.
- Hierarchical registries. A key is a subtree, so one
unregisterends every name below it. ENSv1 is flat: deletingci-1.pwnmeswap.ethleavesrouter.ci-1.pwnmeswap.ethresolving. - EAC roles. Create-only CI key, remove-only security key, root-only delegates and reservations.
- Expiry. A key's 90-day TTL bounds everything below it.
- PermissionedResolver via VerifiableFactory. One resolver per release with root-only setters, which is what makes the "records are immutable to the CI key" proof possible.
- Universal Resolver
findResolver. An offset other than 0 means the name has no resolver of its own (unregistered, pulled, or under a cut key) and is rejected. - Token versions. Token ids change on every register and role change, so releases are keyed by labelhash and the current id is read from the registry.
Without ENSv2, a key couldn't be a subtree you revoke in one transaction, and teams would lose the name people already know them by.
Arkiv is an untrusted, searchable index. ENS can say whether a name is valid but cannot be searched by address. Arkiv adds discovery (which publisher claims an unknown address), withdrawal reasons (a pulled release otherwise looks never published), and the two proof block numbers per release. Rows are never trusted: each one is re-checked against ENS before it can change a verdict, and a missing or flooded index never turns anything green. Schemas, query examples and the trust model are in arkiv/README.md.
Two wallets wrote our entities on Rabdi: the CI key 0x47Fccf73d86473357c74c8ae393C7099Cdc06934 writes release rows, and the security key 0x703389487942788EC4AD8461A51e03a9F79F9186 writes incident rows.
This query lists who claims the demo router. See also: schema, friction, reach, why Arkiv and how we use it.
pwnmeswap.eth is registered on the canonical ENSv2 Sepolia deployment, owned by a 1-of-1 Safe (0x5A0b…A9b0). Browse it on explorer.ens.domains.
The withdrawn release shows what a pull looks like: open 0x65aF…DAE5 in the demo and it says Don't sign, with the reason read from its Arkiv incident row. The router and portal, under the same key, still verify. No cut has run live, because it would withdraw those too; cuts are tested on an Anvil fork.
Every transaction behind the demo team
| Step | Name | Transactions |
|---|---|---|
Publisher registry, .eth commit and register |
pwnmeswap.eth | 0x3a586172… 0xd866bc54… 0x30170433… |
CI key (ROLE_REGISTRAR for the CI address) |
ci-1.pwnmeswap.eth | 0x01e6b9b0… 0x1265ff6f… |
| Root-only delegates key | delegates.pwnmeswap.eth | 0xc6d15084… 0x67a669c3… |
| Router 3.1.0, published by CI | router-3-1-0.ci-1.pwnmeswap.eth | 0xe1b956a1… 0x2ae77cae… |
| Portal 2.0.0 (proxy, implementation pinned) | portal-2-0-0.ci-1.pwnmeswap.eth | 0x3ca5b578… 0x1e67f392… |
| EIP-7702 delegate 1.0.0, published by root | delegate-1-0-0.delegates.pwnmeswap.eth | 0x5213f6c6… 0x911e9e58… |
| Withdrawn demo release, published then pulled by root | withdrawn-1-0-0.ci-1.pwnmeswap.eth | 0xb1febb21… 0x5f43c019… |
The router, portal and withdrawn release are indexed on Arkiv's Rabdi testnet; the delegate has none, because delegates are only trusted when pinned. Check any of the three targets with bunx @marsian/codemark-cli verify <address> --pin pwnmeswap.eth, in the demo, or on the lookup page after you pin pwnmeswap.eth.
Read the records yourself, without any Codemark code
Ask the canonical Universal Resolver through plain viem. Save this as a .ts file in packages/lib/shared, where viem is installed, and run it with bun. The address sits under the Sepolia coin type (ENSIP-11), and the codemark text record holds the code hash:
import { createPublicClient, http, toCoinType } from "viem";
import { sepolia } from "viem/chains";
const client = createPublicClient({ chain: sepolia, transport: http("https://sepolia.gateway.tenderly.co") });
const name = "router-3-1-0.ci-1.pwnmeswap.eth";
await client.getEnsAddress({ name, coinType: toCoinType(sepolia.id) }); // 0x3bfa4769fb09eefc5a80d6e87c3b9c650f7ae48e
await client.getEnsText({ name, key: "codemark" }); // v1;k=contract;n=router;c=11155111;h=0xe7f9…71de;v=3.1.0Plain resolution cannot tell you whether the records could have been rewritten, whether the key is still trusted, or how old the release is. The checker adds those.
- It tells you who published something, not whether the code is safe.
- Both sides must adopt it: teams have to publish, and wallets have to run the check.
- The Snap runs only in MetaMask Flask for now, and it has no Arkiv access key. When Arkiv's public endpoint is rate limiting, a release shows a warning instead of Verified. It never shows a false pass.
- Mainnet is out of scope: Sepolia runs a different ENSv2 commit.
- The MCP server protects an honest agent that was handed a bad address. A hijacked agent can skip the check, and the check doesn't cover amounts or recipients.
The security model covers each kind of attacker and every known limit.
- How it works in detail
- CLI and GitHub Action reference
- Security model and known limits
- Developing Codemark: repository layout, the demo fork, tests and releases
Claude Code wrote most of the code, tests and docs across packages/ under our direction, including the WebGL landing scene. We set the problem, the security model and its limits, the product's wording and the visual direction, and reviewed the results. We also used Claude to generate and critique project ideas and to check our assumptions against the live Sepolia deployment. AGENTS.md and .claude/skills show the instructions it worked under.
MIT. .claude/skills/unslop is by Damir Sokolovsky under CC BY 4.0, as credited in that file.






