r402-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rsynthlabs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # r402-mcp
2
+
3
+ mcp server for [r402](https://github.com/rsynthlabs/r402) · verify $R execution proofs anchored on base mainnet · local: recompute the payload hash, recover the signer, and read the ExecutionLog contract over public rpc
4
+
5
+ two tools: `verify_execution` (free, local verify) · `query_signers` (free, direct rpc).
6
+
7
+ ## install
8
+
9
+ claude code:
10
+
11
+ ```
12
+ claude mcp add r402 -- npx -y r402-mcp
13
+ ```
14
+
15
+ claude desktop (`claude_desktop_config.json`):
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "r402": {
21
+ "command": "npx",
22
+ "args": ["-y", "r402-mcp"]
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ ## environment
29
+
30
+ | var | required | what |
31
+ |---|---|---|
32
+ | `BASE_RPC_URL` | no | base mainnet rpc for `verify_execution` and `query_signers`. default `https://mainnet.base.org` |
33
+
34
+ no private key, no payment. the server never reads `.env` by itself. env comes from the mcp client config, or `node --env-file=.env` for local runs.
35
+
36
+ ## tools
37
+
38
+ ### verify_execution
39
+
40
+ price: free. verifies an execution proof locally and never relays a remote verdict:
41
+
42
+ 1. recompute `keccak256(canonical_bytes(payload))` (SCHEMA.md §2 canonicalization).
43
+ 2. recover the eip-191 signer from `signature` over that hash.
44
+ 3. read the `ExecutionLog` anchor (`0xd5A9DAF8F2134b61b73cEfaF5c9094EA162f1a1c`) for `txHash` over rpc and decode `ExecutionRecorded(signer, payloadHash)`.
45
+ 4. `verified` is true only when the recomputed hash equals the anchored hash and the recovered signer equals the anchored signer.
46
+
47
+ params:
48
+ - `payload` · the off-chain execution payload json (v0.1 schema) that was signed and anchored
49
+ - `signature` · 0x-prefixed 65-byte eip-191 signature over the payload hash
50
+ - `txHash` · 0x-prefixed 32-byte tx hash of the anchor on base
51
+
52
+ a complete demo triple lives in [`examples/demo-anchor.json`](examples/demo-anchor.json) (verify it yourself: [`examples/verify-demo.md`](examples/verify-demo.md)).
53
+
54
+ <!-- demo-anchor:input:begin -->
55
+ ```
56
+ verify_execution {
57
+ "payload": {
58
+ "version": "0.1.0",
59
+ "agent_id": 20617,
60
+ "robot_id": "sim-lerobot-pusht-ep1",
61
+ "episode_id": "ep_2026-06-19T15-04-10Z_pusht1",
62
+ "task": "push T to goal (lerobot/pusht reference replay)",
63
+ "started_at": "2026-06-19T15:04:10Z",
64
+ "ended_at": "2026-06-19T15:04:28Z",
65
+ "duration_seconds": 18.4,
66
+ "frames": 521,
67
+ "metrics": {
68
+ "rmse": 3.214,
69
+ "jerk": 1581043,
70
+ "end_variance": 0
71
+ },
72
+ "score": 0.88,
73
+ "outcome": "SUCCESS"
74
+ },
75
+ "signature": "0x30b36f9ec1bbb18a40247e52568a41c9c424adec7c5209cc7a4da9b47ca35be645b1243a430bf63845d1eb12858b321f2924b5264371a543358ba29f9a8d5c911b",
76
+ "txHash": "0xee10ef6a112f05d9d3761aa055ab258f1afffd54f8c02212366e96a1da0915bb"
77
+ }
78
+ ```
79
+ <!-- demo-anchor:input:end -->
80
+
81
+ verified (recomputed hash and recovered signer both match the anchor):
82
+
83
+ <!-- demo-anchor:result:begin -->
84
+ ```json
85
+ {
86
+ "verified": true,
87
+ "signer": "0x82960f3322a7B1d2a2e756Efcbd4d1D56B613314",
88
+ "payloadHash": "0x1d646db564305a51e5e617245a54bd6fc3bd457cb82488397ba38e1e1c559f26",
89
+ "signature": "0x30b36f9ec1bbb18a40247e52568a41c9c424adec7c5209cc7a4da9b47ca35be645b1243a430bf63845d1eb12858b321f2924b5264371a543358ba29f9a8d5c911b",
90
+ "block": 47568429,
91
+ "timestamp": 1781926205,
92
+ "txHash": "0xee10ef6a112f05d9d3761aa055ab258f1afffd54f8c02212366e96a1da0915bb"
93
+ }
94
+ ```
95
+ <!-- demo-anchor:result:end -->
96
+
97
+ not verified (hash mismatch, signer mismatch, no anchor event in the tx, or reverted tx):
98
+
99
+ ```json
100
+ { "verified": false, "reason": "recovered signer does not match the on-chain anchor" }
101
+ ```
102
+
103
+ ### query_signers
104
+
105
+ price: free. reads `ExecutionRecorded` events of the executionlog contract (`0xd5A9DAF8F2134b61b73cEfaF5c9094EA162f1a1c`) directly over rpc, from deploy block to tip.
106
+
107
+ params: `limit` · max signers to return, 1-100, default 50
108
+
109
+ ```
110
+ query_signers { "limit": 10 }
111
+ ```
112
+
113
+ ```json
114
+ {
115
+ "signers": [
116
+ { "address": "0x156d727f372d06132526612b7d34ce1693365bf3", "anchor_count": 8, "first_seen_block": 46166512 }
117
+ ],
118
+ "total_anchors": 14
119
+ }
120
+ ```
121
+
122
+ ## dev
123
+
124
+ ```
125
+ pnpm install
126
+ pnpm test
127
+ pnpm build
128
+ node --env-file=.env dist/server.js
129
+ ```
130
+
131
+ ## license
132
+
133
+ mit
@@ -0,0 +1,3 @@
1
+ import { type Hex } from 'viem';
2
+ export declare function canonicalize(payload: Record<string, unknown>): string;
3
+ export declare function payloadHash(payload: Record<string, unknown>): Hex;
@@ -0,0 +1,46 @@
1
+ import { keccak256, stringToBytes } from 'viem';
2
+ // float-typed leaves of the v0.1 execution-payload schema (SCHEMA.md §1). js has no
3
+ // int/float distinction, so to reproduce python's json.dumps bytes these fields keep a
4
+ // trailing .0 when integer-valued (2434753 -> 2434753.0, 0 -> 0.0); int fields
5
+ // (agent_id, frames) must not. schema-aware, not generic. see SCHEMA.md §2.
6
+ const FLOAT_FIELDS = new Set([
7
+ 'duration_seconds',
8
+ 'score',
9
+ 'metrics.rmse',
10
+ 'metrics.jerk',
11
+ 'metrics.end_variance',
12
+ ]);
13
+ function encode(value, path) {
14
+ if (value === null)
15
+ return 'null';
16
+ if (typeof value === 'string')
17
+ return JSON.stringify(value);
18
+ if (typeof value === 'boolean')
19
+ return value ? 'true' : 'false';
20
+ if (typeof value === 'number') {
21
+ let s = String(value);
22
+ if (FLOAT_FIELDS.has(path) && !/[.eE]/.test(s))
23
+ s += '.0';
24
+ return s;
25
+ }
26
+ if (Array.isArray(value))
27
+ return '[' + value.map((v) => encode(v, path)).join(',') + ']';
28
+ const keys = Object.keys(value)
29
+ // additive v0.2: drop an unset policy block so v0.1 payloads hash identically.
30
+ .filter((k) => !(k === 'policy' && value[k] === null))
31
+ .sort();
32
+ return ('{' +
33
+ keys
34
+ .map((k) => JSON.stringify(k) + ':' + encode(value[k], path ? `${path}.${k}` : k))
35
+ .join(',') +
36
+ '}');
37
+ }
38
+ // SCHEMA.md §2: sorted keys at every depth, no whitespace, utf-8, no trailing newline.
39
+ // byte-identical to rsynth.payload.canonical_bytes.
40
+ export function canonicalize(payload) {
41
+ return encode(payload, '');
42
+ }
43
+ export function payloadHash(payload) {
44
+ return keccak256(stringToBytes(canonicalize(payload)));
45
+ }
46
+ //# sourceMappingURL=canonical.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical.js","sourceRoot":"","sources":["../src/canonical.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAY,MAAM,MAAM,CAAC;AAE1D,oFAAoF;AACpF,uFAAuF;AACvF,+EAA+E;AAC/E,4EAA4E;AAC5E,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC;IAC3B,kBAAkB;IAClB,OAAO;IACP,cAAc;IACd,cAAc;IACd,sBAAsB;CACvB,CAAC,CAAC;AAIH,SAAS,MAAM,CAAC,KAAW,EAAE,IAAY;IACvC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAClC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IAChE,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QACtB,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,CAAC,IAAI,IAAI,CAAC;QAC1D,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACzF,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;QAC7B,+EAA+E;SAC9E,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;SACrD,IAAI,EAAE,CAAC;IACV,OAAO,CACL,GAAG;QACH,IAAI;aACD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;aACjF,IAAI,CAAC,GAAG,CAAC;QACZ,GAAG,CACJ,CAAC;AACJ,CAAC;AAED,uFAAuF;AACvF,oDAAoD;AACpD,MAAM,UAAU,YAAY,CAAC,OAAgC;IAC3D,OAAO,MAAM,CAAC,OAAe,EAAE,EAAE,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAgC;IAC1D,OAAO,SAAS,CAAC,aAAa,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC"}
@@ -0,0 +1,8 @@
1
+ export declare const SERVER_NAME = "r402-mcp";
2
+ export declare const SERVER_VERSION = "0.1.0";
3
+ export declare const TX_HASH_RE: RegExp;
4
+ export declare const SIGNATURE_RE: RegExp;
5
+ export declare const EXECUTION_LOG_ADDRESS: "0xd5A9DAF8F2134b61b73cEfaF5c9094EA162f1a1c";
6
+ export declare const EXECUTION_LOG_DEPLOY_BLOCK = 46166486n;
7
+ export declare const GET_LOGS_CHUNK_BLOCKS = 10000n;
8
+ export declare const DEFAULT_BASE_RPC_URL = "https://mainnet.base.org";
@@ -0,0 +1,11 @@
1
+ export const SERVER_NAME = 'r402-mcp';
2
+ export const SERVER_VERSION = '0.1.0';
3
+ export const TX_HASH_RE = /^0x[0-9a-fA-F]{64}$/;
4
+ // 65-byte (r, s, v) eip-191 signature.
5
+ export const SIGNATURE_RE = /^0x[0-9a-fA-F]{130}$/;
6
+ export const EXECUTION_LOG_ADDRESS = '0xd5A9DAF8F2134b61b73cEfaF5c9094EA162f1a1c';
7
+ export const EXECUTION_LOG_DEPLOY_BLOCK = 46166486n;
8
+ // public base rpcs cap eth_getLogs ranges around 10k blocks.
9
+ export const GET_LOGS_CHUNK_BLOCKS = 10000n;
10
+ export const DEFAULT_BASE_RPC_URL = 'https://mainnet.base.org';
11
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;AACtC,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AAEtC,MAAM,CAAC,MAAM,UAAU,GAAG,qBAAqB,CAAC;AAChD,uCAAuC;AACvC,MAAM,CAAC,MAAM,YAAY,GAAG,sBAAsB,CAAC;AAEnD,MAAM,CAAC,MAAM,qBAAqB,GAAG,4CAAqD,CAAC;AAC3F,MAAM,CAAC,MAAM,0BAA0B,GAAG,SAAW,CAAC;AACtD,6DAA6D;AAC7D,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAO,CAAC;AAC7C,MAAM,CAAC,MAAM,oBAAoB,GAAG,0BAA0B,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { DEFAULT_BASE_RPC_URL, SERVER_NAME, SERVER_VERSION, SIGNATURE_RE, TX_HASH_RE, } from './constants.js';
6
+ import { verifyExecution } from './verify.js';
7
+ import { querySigners } from './signers.js';
8
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
9
+ function ok(result) {
10
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
11
+ }
12
+ function fail(err) {
13
+ const message = err instanceof Error ? err.message : String(err);
14
+ return { content: [{ type: 'text', text: message }], isError: true };
15
+ }
16
+ const PAYLOAD_SCHEMA = z.object({
17
+ version: z.string(),
18
+ agent_id: z.number().int(),
19
+ robot_id: z.string(),
20
+ episode_id: z.string(),
21
+ task: z.string(),
22
+ started_at: z.string(),
23
+ ended_at: z.string(),
24
+ duration_seconds: z.number(),
25
+ frames: z.number().int(),
26
+ metrics: z.object({
27
+ rmse: z.number(),
28
+ jerk: z.number(),
29
+ end_variance: z.number(),
30
+ }),
31
+ score: z.number().min(0).max(1),
32
+ outcome: z.enum(['SUCCESS', 'PARTIAL', 'FAIL']),
33
+ });
34
+ server.registerTool('verify_execution', {
35
+ description: 'verify an r402 execution proof locally: recompute keccak256 of the canonical payload, recover the eip-191 signer from the signature, and read the executionlog anchor (0xd5A9DAF8F2134b61b73cEfaF5c9094EA162f1a1c) on base by tx hash over public rpc. free, no payment, no relayed verdict. returns verified true only when the recomputed hash and recovered signer both match the on-chain anchor.',
36
+ inputSchema: {
37
+ payload: PAYLOAD_SCHEMA.describe('the off-chain execution payload json (v0.1 schema) that was signed and anchored'),
38
+ signature: z
39
+ .string()
40
+ .regex(SIGNATURE_RE)
41
+ .describe('0x-prefixed 65-byte eip-191 signature over the payload hash'),
42
+ txHash: z
43
+ .string()
44
+ .regex(TX_HASH_RE)
45
+ .describe('0x-prefixed 32-byte transaction hash of the anchor on base mainnet'),
46
+ },
47
+ annotations: { readOnlyHint: true, openWorldHint: true },
48
+ }, async ({ payload, signature, txHash }) => {
49
+ try {
50
+ return ok(await verifyExecution({ payload, signature, txHash }, { rpcUrl: process.env.BASE_RPC_URL || DEFAULT_BASE_RPC_URL }));
51
+ }
52
+ catch (err) {
53
+ return fail(err);
54
+ }
55
+ });
56
+ server.registerTool('query_signers', {
57
+ description: 'list addresses that have anchored ExecutionLog records on Base, with anchor counts and first-seen block. free; reads ExecutionRecorded events directly over rpc.',
58
+ inputSchema: {
59
+ limit: z
60
+ .number()
61
+ .int()
62
+ .min(1)
63
+ .max(100)
64
+ .default(50)
65
+ .describe('max signers to return (1-100, default 50)'),
66
+ },
67
+ annotations: { readOnlyHint: true, openWorldHint: true },
68
+ }, async ({ limit }) => {
69
+ try {
70
+ return ok(await querySigners({ limit, rpcUrl: process.env.BASE_RPC_URL || DEFAULT_BASE_RPC_URL }));
71
+ }
72
+ catch (err) {
73
+ return fail(err);
74
+ }
75
+ });
76
+ async function main() {
77
+ const transport = new StdioServerTransport();
78
+ await server.connect(transport);
79
+ console.error(`${SERVER_NAME} ${SERVER_VERSION} ready on stdio`);
80
+ }
81
+ main().catch((err) => {
82
+ console.error(err instanceof Error ? err.message : String(err));
83
+ process.exit(1);
84
+ });
85
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ export declare const EXECUTION_RECORDED_EVENT: {
2
+ readonly name: "ExecutionRecorded";
3
+ readonly type: "event";
4
+ readonly inputs: readonly [{
5
+ readonly type: "address";
6
+ readonly name: "signer";
7
+ readonly indexed: true;
8
+ }, {
9
+ readonly type: "bytes32";
10
+ readonly name: "payloadHash";
11
+ readonly indexed: true;
12
+ }, {
13
+ readonly type: "uint256";
14
+ readonly name: "blockTimestamp";
15
+ }];
16
+ };
17
+ export interface ExecutionRecordedLog {
18
+ blockNumber: bigint;
19
+ args: {
20
+ signer: `0x${string}`;
21
+ payloadHash: `0x${string}`;
22
+ blockTimestamp: bigint;
23
+ };
24
+ }
25
+ export interface SignersRpcClient {
26
+ getBlockNumber(): Promise<bigint>;
27
+ getLogs(args: {
28
+ address: `0x${string}`;
29
+ event: typeof EXECUTION_RECORDED_EVENT;
30
+ fromBlock: bigint;
31
+ toBlock: bigint;
32
+ strict: true;
33
+ }): Promise<ExecutionRecordedLog[]>;
34
+ }
35
+ export interface SignerRow {
36
+ address: string;
37
+ anchor_count: number;
38
+ first_seen_block: number;
39
+ }
40
+ export interface QuerySignersResult {
41
+ signers: SignerRow[];
42
+ total_anchors: number;
43
+ }
44
+ export declare function makePublicRpcClient(rpcUrl: string): SignersRpcClient;
45
+ export declare function querySigners(opts?: {
46
+ limit?: number;
47
+ client?: SignersRpcClient;
48
+ rpcUrl?: string;
49
+ }): Promise<QuerySignersResult>;
@@ -0,0 +1,52 @@
1
+ import { createPublicClient, http, parseAbiItem } from 'viem';
2
+ import { base } from 'viem/chains';
3
+ import { DEFAULT_BASE_RPC_URL, EXECUTION_LOG_ADDRESS, EXECUTION_LOG_DEPLOY_BLOCK, GET_LOGS_CHUNK_BLOCKS, } from './constants.js';
4
+ export const EXECUTION_RECORDED_EVENT = parseAbiItem('event ExecutionRecorded(address indexed signer, bytes32 indexed payloadHash, uint256 blockTimestamp)');
5
+ export function makePublicRpcClient(rpcUrl) {
6
+ const client = createPublicClient({ chain: base, transport: http(rpcUrl) });
7
+ return {
8
+ getBlockNumber: () => client.getBlockNumber(),
9
+ getLogs: (args) => client.getLogs(args),
10
+ };
11
+ }
12
+ export async function querySigners(opts = {}) {
13
+ const limit = Math.min(100, Math.max(1, Math.trunc(opts.limit ?? 50)));
14
+ const client = opts.client ?? makePublicRpcClient(opts.rpcUrl ?? DEFAULT_BASE_RPC_URL);
15
+ const tip = await client.getBlockNumber();
16
+ const bySigner = new Map();
17
+ let total = 0;
18
+ for (let from = EXECUTION_LOG_DEPLOY_BLOCK; from <= tip; from += GET_LOGS_CHUNK_BLOCKS) {
19
+ const chunkEnd = from + GET_LOGS_CHUNK_BLOCKS - 1n;
20
+ const logs = await client.getLogs({
21
+ address: EXECUTION_LOG_ADDRESS,
22
+ event: EXECUTION_RECORDED_EVENT,
23
+ fromBlock: from,
24
+ toBlock: chunkEnd < tip ? chunkEnd : tip,
25
+ strict: true,
26
+ });
27
+ for (const log of logs) {
28
+ total += 1;
29
+ const address = log.args.signer.toLowerCase();
30
+ const entry = bySigner.get(address);
31
+ if (!entry) {
32
+ bySigner.set(address, { count: 1, firstSeen: log.blockNumber });
33
+ }
34
+ else {
35
+ entry.count += 1;
36
+ if (log.blockNumber < entry.firstSeen)
37
+ entry.firstSeen = log.blockNumber;
38
+ }
39
+ }
40
+ }
41
+ const signers = [...bySigner.entries()]
42
+ .map(([address, entry]) => ({
43
+ address,
44
+ anchor_count: entry.count,
45
+ first_seen_block: Number(entry.firstSeen),
46
+ }))
47
+ .sort((a, b) => b.anchor_count - a.anchor_count ||
48
+ (a.address < b.address ? -1 : a.address > b.address ? 1 : 0))
49
+ .slice(0, limit);
50
+ return { signers, total_anchors: total };
51
+ }
52
+ //# sourceMappingURL=signers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signers.js","sourceRoot":"","sources":["../src/signers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import { type Hex, type Log } from 'viem';
2
+ export type VerifyToolResult = {
3
+ verified: true;
4
+ signer: string;
5
+ payloadHash: string;
6
+ signature: string;
7
+ block: number;
8
+ timestamp: number;
9
+ txHash: string;
10
+ } | {
11
+ verified: false;
12
+ reason: string;
13
+ };
14
+ export interface VerifyInput {
15
+ payload: Record<string, unknown>;
16
+ signature: string;
17
+ txHash: string;
18
+ }
19
+ interface AnchorReceipt {
20
+ status: 'success' | 'reverted';
21
+ blockNumber: bigint;
22
+ logs: Log[];
23
+ }
24
+ export interface AnchorRpcClient {
25
+ getTransactionReceipt(args: {
26
+ hash: Hex;
27
+ }): Promise<AnchorReceipt>;
28
+ }
29
+ export declare function makeAnchorRpcClient(rpcUrl: string): AnchorRpcClient;
30
+ export declare function verifyExecution(input: VerifyInput, opts?: {
31
+ rpcUrl?: string;
32
+ client?: AnchorRpcClient;
33
+ }): Promise<VerifyToolResult>;
34
+ export {};
package/dist/verify.js ADDED
@@ -0,0 +1,92 @@
1
+ import { createPublicClient, http, isAddressEqual, parseEventLogs, recoverMessageAddress, TransactionReceiptNotFoundError, } from 'viem';
2
+ import { base } from 'viem/chains';
3
+ import { DEFAULT_BASE_RPC_URL, EXECUTION_LOG_ADDRESS, SIGNATURE_RE, TX_HASH_RE } from './constants.js';
4
+ import { EXECUTION_RECORDED_EVENT } from './signers.js';
5
+ import { payloadHash } from './canonical.js';
6
+ export function makeAnchorRpcClient(rpcUrl) {
7
+ const client = createPublicClient({ chain: base, transport: http(rpcUrl) });
8
+ return {
9
+ getTransactionReceipt: (args) => client.getTransactionReceipt(args),
10
+ };
11
+ }
12
+ async function readAnchor(client, txHash) {
13
+ let receipt;
14
+ try {
15
+ receipt = await client.getTransactionReceipt({ hash: txHash });
16
+ }
17
+ catch (err) {
18
+ if (err instanceof TransactionReceiptNotFoundError) {
19
+ return { ok: false, reason: 'no transaction found for txHash' };
20
+ }
21
+ throw err;
22
+ }
23
+ if (receipt.status === 'reverted') {
24
+ return { ok: false, reason: 'anchor transaction reverted' };
25
+ }
26
+ // parseEventLogs matches by event topic only, not by emitting address, so filter to
27
+ // our ExecutionLog instance: a tx emitting ExecutionRecorded from a different contract
28
+ // must not count. mirrors rsynth.fetch._verify.
29
+ const decoded = parseEventLogs({
30
+ abi: [EXECUTION_RECORDED_EVENT],
31
+ eventName: 'ExecutionRecorded',
32
+ logs: receipt.logs,
33
+ }).filter((log) => isAddressEqual(log.address, EXECUTION_LOG_ADDRESS));
34
+ if (decoded.length === 0) {
35
+ return {
36
+ ok: false,
37
+ reason: 'no ExecutionRecorded event from the ExecutionLog contract in this tx',
38
+ };
39
+ }
40
+ // v0.1 invariant: one record() call per tx -> one ExecutionRecorded per receipt.
41
+ const event = decoded[0];
42
+ return {
43
+ ok: true,
44
+ signer: event.args.signer,
45
+ payloadHash: event.args.payloadHash,
46
+ block: Number(receipt.blockNumber),
47
+ timestamp: Number(event.args.blockTimestamp),
48
+ };
49
+ }
50
+ // local verification (SCHEMA.md §2-5): recompute keccak256(canonical payload), recover the
51
+ // EIP-191 signer, read the on-chain anchor over public rpc, and decide locally. no remote
52
+ // verdict is relayed and no payment is made.
53
+ export async function verifyExecution(input, opts = {}) {
54
+ const { payload, signature, txHash } = input;
55
+ // boundary guards: reject malformed inputs before any rpc call.
56
+ if (!TX_HASH_RE.test(txHash)) {
57
+ throw new Error('txHash must be a 0x-prefixed 32-byte transaction hash');
58
+ }
59
+ if (!SIGNATURE_RE.test(signature)) {
60
+ throw new Error('signature must be a 0x-prefixed 65-byte hex string');
61
+ }
62
+ const client = opts.client ?? makeAnchorRpcClient(opts.rpcUrl ?? DEFAULT_BASE_RPC_URL);
63
+ const recompute = payloadHash(payload);
64
+ const recovered = await recoverMessageAddress({
65
+ message: { raw: recompute },
66
+ signature: signature,
67
+ });
68
+ const anchor = await readAnchor(client, txHash);
69
+ if (!anchor.ok) {
70
+ return { verified: false, reason: anchor.reason };
71
+ }
72
+ const hashMatch = recompute.toLowerCase() === anchor.payloadHash.toLowerCase();
73
+ const signerMatch = isAddressEqual(recovered, anchor.signer);
74
+ if (hashMatch && signerMatch) {
75
+ return {
76
+ verified: true,
77
+ signer: anchor.signer,
78
+ payloadHash: anchor.payloadHash,
79
+ signature,
80
+ block: anchor.block,
81
+ timestamp: anchor.timestamp,
82
+ txHash,
83
+ };
84
+ }
85
+ return {
86
+ verified: false,
87
+ reason: !hashMatch
88
+ ? 'payload hash does not match the on-chain anchor'
89
+ : 'recovered signer does not match the on-chain anchor',
90
+ };
91
+ }
92
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ {
2
+ "payload": {
3
+ "version": "0.1.0",
4
+ "agent_id": 20617,
5
+ "robot_id": "sim-lerobot-pusht-ep1",
6
+ "episode_id": "ep_2026-06-19T15-04-10Z_pusht1",
7
+ "task": "push T to goal (lerobot/pusht reference replay)",
8
+ "started_at": "2026-06-19T15:04:10Z",
9
+ "ended_at": "2026-06-19T15:04:28Z",
10
+ "duration_seconds": 18.4,
11
+ "frames": 521,
12
+ "metrics": {
13
+ "rmse": 3.214,
14
+ "jerk": 1581043,
15
+ "end_variance": 0
16
+ },
17
+ "score": 0.88,
18
+ "outcome": "SUCCESS"
19
+ },
20
+ "payloadHash": "0x1d646db564305a51e5e617245a54bd6fc3bd457cb82488397ba38e1e1c559f26",
21
+ "signature": "0x30b36f9ec1bbb18a40247e52568a41c9c424adec7c5209cc7a4da9b47ca35be645b1243a430bf63845d1eb12858b321f2924b5264371a543358ba29f9a8d5c911b",
22
+ "signer": "0x82960f3322a7B1d2a2e756Efcbd4d1D56B613314",
23
+ "txHash": "0xee10ef6a112f05d9d3761aa055ab258f1afffd54f8c02212366e96a1da0915bb",
24
+ "blockNumber": 47568429,
25
+ "timestamp": 1781926205
26
+ }
@@ -0,0 +1,43 @@
1
+ # verify the demo anchor
2
+
3
+ a complete `{payload, signature, txHash}` triple anchored on base mainnet · recompute it
4
+ yourself, no trust in this repo required.
5
+
6
+ the triple lives in [`demo-anchor.json`](./demo-anchor.json). `payloadHash` · `signature` ·
7
+ `signer` are deterministic (recompute them offline); `txHash` · `blockNumber` · `timestamp`
8
+ are filled when the anchor is minted (`pnpm mint:demo --broadcast`).
9
+
10
+ ## verify it
11
+
12
+ call `verify_execution` with the `payload` · `signature` · `txHash` from `demo-anchor.json`:
13
+
14
+ ```
15
+ verify_execution {
16
+ "payload": { ...demo-anchor.json payload... },
17
+ "signature": "<demo-anchor.json signature>",
18
+ "txHash": "<demo-anchor.json txHash>"
19
+ }
20
+ ```
21
+
22
+ expected (recomputed hash and recovered signer both match the anchor):
23
+
24
+ ```json
25
+ {
26
+ "verified": true,
27
+ "signer": "<demo-anchor.json signer>",
28
+ "payloadHash": "<demo-anchor.json payloadHash>",
29
+ "signature": "<demo-anchor.json signature>",
30
+ "block": "<demo-anchor.json blockNumber>",
31
+ "timestamp": "<demo-anchor.json timestamp>",
32
+ "txHash": "<demo-anchor.json txHash>"
33
+ }
34
+ ```
35
+
36
+ ## recompute it yourself
37
+
38
+ `verified` is not relayed from anywhere · the tool derives it locally:
39
+
40
+ - `keccak256(canonical_bytes(payload)) == payloadHash` (canonicalization: README §verify_execution).
41
+ - the eip-191 signer recovered from `signature` over that hash `== signer`.
42
+ - the `ExecutionLog` anchor read for `txHash` over public base rpc carries that same
43
+ `(signer, payloadHash)`.
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "r402-mcp",
3
+ "version": "0.1.0",
4
+ "description": "mcp server for the r402 execution-proof verifier on base mainnet",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "private": false,
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
11
+ "bin": {
12
+ "r402-mcp": "dist/server.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "examples"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "prepublishOnly": "npm run build",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "typecheck": "tsc --noEmit",
24
+ "mint:demo": "tsx scripts/mint-demo-anchor.ts"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.29.0",
28
+ "viem": "^2.21.0",
29
+ "zod": "^4.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^22.0.0",
33
+ "tsx": "^4.22.4",
34
+ "typescript": "^5.6.0",
35
+ "vitest": "^2.1.0"
36
+ }
37
+ }