aipou-mcp-server 0.2.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/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # AIPOU MCP Server
2
+
3
+ `aipou-mcp-server` records private, signed receipts for authorized AI tasks. It supports local receipt collection and optional claims for validated work on Base.
4
+
5
+ It does not detect hidden AI use, prove task quality, replace payment rails, or require raw prompts and outputs to leave the user's machine.
6
+
7
+ ## Fast Local Adoption Test
8
+
9
+ If you are evaluating AIPOU for an agent framework, start with the lifecycle adapter example:
10
+
11
+ ```bash
12
+ npm install
13
+ npm run build -w mcp-server
14
+ cd examples/lifecycle-adapter
15
+ npm install
16
+ npm run demo
17
+ ```
18
+
19
+ The demo launches this MCP server over stdio, creates a local signed receipt with an ephemeral wallet, and prints the `receiptId` reference a framework can attach to metadata. It does not claim rewards or move funds.
20
+
21
+ ## Run locally from this repository
22
+
23
+ The npm package metadata is prepared, but the first public npm publish must be performed by the repository owner. Until `aipou-mcp-server` is published, run the MCP server from a local checkout:
24
+
25
+ ```bash
26
+ npm install
27
+ npm run build -w mcp-server
28
+ node mcp-server/dist/index.js
29
+ ```
30
+
31
+ For development:
32
+
33
+ ```bash
34
+ npm run dev -w mcp-server
35
+ ```
36
+
37
+ ## Run after npm publication
38
+
39
+ After the package is published to npm, MCP clients can launch it with:
40
+
41
+ ```bash
42
+ npx -y aipou-mcp-server
43
+ ```
44
+
45
+ Required configuration:
46
+
47
+ ```text
48
+ AIPOU_AGENT_PRIVATE_KEY=<dedicated farming wallet key>
49
+ AIPOU_CONTRACT_ADDRESS=0x55f0Cc5e51A1284D20337d6cbb18938C8A1ABCbB
50
+ AIPOU_CLAIMS_ADDRESS=0x4ca4C98fB784D20EdC8E2A7F531dAab4c6e53058
51
+ ```
52
+
53
+ Use a new dedicated farming wallet, never a primary wallet. Do not commit the private key. Optional claims and settlement occur only after an explicit user request.
54
+
55
+ Do not configure `AIPOU_VALIDATOR_PRIVATE_KEY` on a user installation. That key is only for the separate protocol validator service. The local Ed25519 collector key and receipt metadata are stored unencrypted under `AIPOU_DATA_DIR`; restrict that directory with operating-system permissions and use encrypted backups.
56
+
57
+ The default network is Base mainnet. `AIPOU_RPC_URL` and `AIPOU_DATA_DIR` can be set when a custom RPC endpoint or receipt directory is needed.
58
+
59
+ Publication checklist: [docs/npm-publication.md](https://github.com/0xddneto/AI-Proof-of-Us/blob/main/docs/npm-publication.md).
60
+
61
+ ## Documentation
62
+
63
+ - [Local Receipt Mode](https://github.com/0xddneto/AI-Proof-of-Us/tree/main/examples/local-receipt-mode)
64
+ - [Lifecycle Adapter Example](https://github.com/0xddneto/AI-Proof-of-Us/tree/main/examples/lifecycle-adapter)
65
+ - [Evidence Boundaries](https://github.com/0xddneto/AI-Proof-of-Us/blob/main/docs/evidence-boundaries.md)
66
+ - [Claim Validation Policy](https://github.com/0xddneto/AI-Proof-of-Us/blob/main/docs/claim-validation-policy.md)
67
+ - [Tokenomics](https://github.com/0xddneto/AI-Proof-of-Us/blob/main/docs/tokenomics.md)
68
+ - [Security Policy](https://github.com/0xddneto/AI-Proof-of-Us/blob/main/SECURITY.md)
69
+
70
+ License: MIT
@@ -0,0 +1,2 @@
1
+ export declare function canonicalJson(value: unknown): string;
2
+ export declare function sha256Hex(value: string): string;
@@ -0,0 +1,18 @@
1
+ import { createHash } from "node:crypto";
2
+ export function canonicalJson(value) {
3
+ if (value === null || typeof value !== "object") {
4
+ return JSON.stringify(value);
5
+ }
6
+ if (Array.isArray(value)) {
7
+ return `[${value.map(canonicalJson).join(",")}]`;
8
+ }
9
+ const record = value;
10
+ return `{${Object.keys(record)
11
+ .filter((key) => record[key] !== undefined)
12
+ .sort()
13
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
14
+ .join(",")}}`;
15
+ }
16
+ export function sha256Hex(value) {
17
+ return `0x${createHash("sha256").update(value).digest("hex")}`;
18
+ }
@@ -0,0 +1,2 @@
1
+ import type { SettlementResult } from "./types.js";
2
+ export declare function settleRewards(maxReceipts?: number): Promise<SettlementResult>;
package/dist/claims.js ADDED
@@ -0,0 +1,150 @@
1
+ import { MerkleTree } from "merkletreejs";
2
+ import { AbiCoder, Contract, JsonRpcProvider, Wallet, concat, getAddress, getBytes, keccak256, parseUnits } from "ethers";
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { collectorFingerprint, verifyCollectorPayload } from "./collector.js";
6
+ import { verifyTaskAuthorization } from "./identity.js";
7
+ import { DAY_MS, filterEligibleReceipts, settlementPolicyFromEnv } from "./policy.js";
8
+ import { deriveTrustTier } from "./providers.js";
9
+ import { markBatchSettled, settledReceiptsSince, unsettledReceipts } from "./store.js";
10
+ const claimsAbi = [
11
+ "function token() view returns (address)",
12
+ "function validator() view returns (address)",
13
+ "function approvedRoots(bytes32 root) view returns (bool)",
14
+ "function publishRoot(bytes32 root)",
15
+ "function claimBatch(bytes32 root, address[] accounts, uint256[] amounts, bytes32[] receiptIds, bytes32[][] proofs)"
16
+ ];
17
+ const tokenAbi = ["function emissionController() view returns (address)"];
18
+ function claimsAddress() {
19
+ const value = process.env.AIPOU_CLAIMS_ADDRESS;
20
+ if (!value)
21
+ throw new Error("AIPOU_CLAIMS_ADDRESS is required to settle rewards");
22
+ return getAddress(value);
23
+ }
24
+ async function validatorPrivateKey() {
25
+ const keyFile = process.env.AIPOU_VALIDATOR_KEY_FILE;
26
+ if (keyFile) {
27
+ const key = (await readFile(path.resolve(keyFile), "utf8")).trim();
28
+ if (!key)
29
+ throw new Error(`AIPOU_VALIDATOR_KEY_FILE ${keyFile} is empty`);
30
+ return key;
31
+ }
32
+ const envKey = process.env.AIPOU_VALIDATOR_PRIVATE_KEY;
33
+ if (envKey) {
34
+ console.error("aipou-mcp: AIPOU_VALIDATOR_PRIVATE_KEY in the shared environment is deprecated; " +
35
+ "move it to a separate file and set AIPOU_VALIDATOR_KEY_FILE so farming agents never load the validator key");
36
+ return envKey;
37
+ }
38
+ throw new Error("AIPOU_VALIDATOR_KEY_FILE (or legacy AIPOU_VALIDATOR_PRIVATE_KEY) is required only on the protocol validator server");
39
+ }
40
+ async function validatorWallet() {
41
+ const provider = new JsonRpcProvider(process.env.AIPOU_RPC_URL || "https://mainnet.base.org");
42
+ return new Wallet(await validatorPrivateKey(), provider);
43
+ }
44
+ async function trustedCollectorFingerprints() {
45
+ const file = process.env.AIPOU_TRUSTED_COLLECTORS_FILE;
46
+ if (!file)
47
+ throw new Error("AIPOU_TRUSTED_COLLECTORS_FILE is required on the protocol validator server");
48
+ const content = JSON.parse(await readFile(path.resolve(file), "utf8"));
49
+ if (!Array.isArray(content.fingerprints))
50
+ throw new Error("Trusted collectors file must contain a fingerprints array");
51
+ return new Set(content.fingerprints);
52
+ }
53
+ async function verifyReceipt(receipt, trustedCollectors) {
54
+ const fingerprint = collectorFingerprint(receipt.collectorPublicKey);
55
+ if (!trustedCollectors.has(fingerprint)) {
56
+ throw new Error(`Untrusted collector ${fingerprint} for ${receipt.receiptId}`);
57
+ }
58
+ const { collectorSignature, batchRoot, claimTransaction, ...signedPayload } = receipt;
59
+ if (!verifyCollectorPayload(signedPayload, collectorSignature, receipt.collectorPublicKey)) {
60
+ throw new Error(`Invalid collector signature for ${receipt.receiptId}`);
61
+ }
62
+ const session = {
63
+ nonce: receipt.nonce,
64
+ wallet: receipt.wallet,
65
+ provider: receipt.provider,
66
+ model: receipt.model,
67
+ taskHash: receipt.taskHash,
68
+ client: receipt.client,
69
+ issuedAt: receipt.authorizationIssuedAt,
70
+ chainId: receipt.authorizationChainId,
71
+ verifyingContract: receipt.authorizationContract,
72
+ walletAuthorization: receipt.walletAuthorization,
73
+ completedAt: receipt.recordedAt
74
+ };
75
+ if (!verifyTaskAuthorization(session))
76
+ throw new Error(`Invalid wallet authorization for ${receipt.receiptId}`);
77
+ const derivedTier = await deriveTrustTier(session, {
78
+ nonce: receipt.nonce,
79
+ inputTokens: receipt.inputTokens,
80
+ outputTokens: receipt.outputTokens,
81
+ durationSeconds: receipt.durationSeconds,
82
+ outputHash: receipt.outputHash,
83
+ providerEvidence: receipt.providerEvidence
84
+ }, receipt.providerEvidence);
85
+ if (derivedTier !== receipt.trustTier)
86
+ throw new Error(`Invalid trust tier for ${receipt.receiptId}`);
87
+ }
88
+ export async function settleRewards(maxReceipts = 25) {
89
+ const candidates = await unsettledReceipts(maxReceipts);
90
+ if (candidates.length === 0)
91
+ throw new Error("There are no unsettled receipts");
92
+ const policy = settlementPolicyFromEnv();
93
+ const recentlySettled = await settledReceiptsSince(Date.now() - DAY_MS);
94
+ const { eligible: receipts, skipped } = filterEligibleReceipts(candidates, recentlySettled, policy);
95
+ if (receipts.length === 0) {
96
+ const reasons = skipped.map((item) => `${item.receiptId.slice(0, 10)}…: ${item.reason}`).join("; ");
97
+ throw new Error(`No receipts pass the settlement policy. Skipped: ${reasons}`);
98
+ }
99
+ const trustedCollectors = await trustedCollectorFingerprints();
100
+ for (const receipt of receipts)
101
+ await verifyReceipt(receipt, trustedCollectors);
102
+ const accounts = receipts.map((receipt) => receipt.wallet);
103
+ const amounts = receipts.map((receipt) => parseUnits(receipt.estimatedReward, 18));
104
+ const receiptIds = receipts.map((receipt) => receipt.receiptId);
105
+ const coder = AbiCoder.defaultAbiCoder();
106
+ // Leaves are double-hashed (inner keccak, then keccak again) so an interior
107
+ // Merkle node can never be replayed as a valid leaf (second-preimage guard).
108
+ const leaves = receipts.map((receipt, index) => {
109
+ const inner = keccak256(coder.encode(["address", "uint256", "bytes32"], [receipt.wallet, amounts[index], receipt.receiptId]));
110
+ return Buffer.from(getBytes(keccak256(concat([inner]))));
111
+ });
112
+ const tree = new MerkleTree(leaves, (value) => Buffer.from(getBytes(keccak256(value))), { sortLeaves: true, sortPairs: true });
113
+ const root = tree.getHexRoot();
114
+ const proofs = leaves.map((leaf) => tree.getHexProof(leaf));
115
+ const signer = await validatorWallet();
116
+ const address = claimsAddress();
117
+ const claims = new Contract(address, claimsAbi, signer);
118
+ const configuredValidator = getAddress(await claims.validator());
119
+ if (configuredValidator !== signer.address) {
120
+ throw new Error(`Validator mismatch: contract expects ${configuredValidator}, server uses ${signer.address}`);
121
+ }
122
+ const tokenAddress = await claims.token();
123
+ const token = new Contract(tokenAddress, tokenAbi, signer.provider);
124
+ if (getAddress(await token.emissionController()) !== address) {
125
+ throw new Error("AIPOUClaims is not the token emission controller");
126
+ }
127
+ const publish = await claims.publishRoot(root);
128
+ const publishReceipt = await publish.wait();
129
+ let rootApproved = false;
130
+ for (let attempt = 0; attempt < 10 && !rootApproved; attempt += 1) {
131
+ rootApproved = await claims.approvedRoots(root);
132
+ if (!rootApproved)
133
+ await new Promise((resolve) => setTimeout(resolve, 3_000));
134
+ }
135
+ if (!rootApproved)
136
+ throw new Error("Published Merkle root was not visible through the RPC after 30 seconds");
137
+ const claim = await claims.claimBatch(root, accounts, amounts, receiptIds, proofs);
138
+ const claimReceipt = await claim.wait();
139
+ if (!publishReceipt || !claimReceipt)
140
+ throw new Error("Settlement transaction receipt is unavailable");
141
+ const batch = {
142
+ root,
143
+ receiptIds,
144
+ publishTransaction: publishReceipt.hash,
145
+ claimTransaction: claimReceipt.hash,
146
+ settledAt: new Date().toISOString()
147
+ };
148
+ await markBatchSettled(batch);
149
+ return { ...batch, skippedReceipts: skipped };
150
+ }
@@ -0,0 +1,4 @@
1
+ export declare function getCollectorPublicKey(): Promise<string>;
2
+ export declare function collectorFingerprint(publicKey: string): string;
3
+ export declare function signCollectorPayload(payload: unknown): Promise<string>;
4
+ export declare function verifyCollectorPayload(payload: unknown, signature: string, publicKey: string): boolean;
@@ -0,0 +1,68 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { canonicalJson } from "./canonical.js";
7
+ const execFileAsync = promisify(execFile);
8
+ // mode 0o600 is a no-op on NTFS, so on Windows the private key would keep the
9
+ // permissive inherited ACL. Strip inheritance and grant only the current user.
10
+ // Best-effort: an ACL failure must not block receipt collection.
11
+ async function restrictToCurrentUser(filePath) {
12
+ if (process.platform !== "win32")
13
+ return;
14
+ const user = process.env.USERNAME;
15
+ if (!user)
16
+ return;
17
+ const principal = process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\${user}` : user;
18
+ try {
19
+ await execFileAsync("icacls", [filePath, "/inheritance:r", "/grant:r", `${principal}:F`]);
20
+ }
21
+ catch {
22
+ // keep the inherited ACL rather than failing key creation
23
+ }
24
+ }
25
+ function dataDir() {
26
+ return path.resolve(process.env.AIPOU_DATA_DIR || ".aipou");
27
+ }
28
+ function privateKeyPath() {
29
+ return path.join(dataDir(), "collector-ed25519-private.pem");
30
+ }
31
+ function publicKeyPath() {
32
+ return path.join(dataDir(), "collector-ed25519-public.pem");
33
+ }
34
+ async function ensureCollectorKeys() {
35
+ await mkdir(dataDir(), { recursive: true });
36
+ try {
37
+ const [privateKey, publicKey] = await Promise.all([
38
+ readFile(privateKeyPath(), "utf8"),
39
+ readFile(publicKeyPath(), "utf8")
40
+ ]);
41
+ return { privateKey, publicKey };
42
+ }
43
+ catch (error) {
44
+ if (error.code !== "ENOENT")
45
+ throw error;
46
+ }
47
+ const pair = generateKeyPairSync("ed25519");
48
+ const privateKey = pair.privateKey.export({ format: "pem", type: "pkcs8" }).toString();
49
+ const publicKey = pair.publicKey.export({ format: "pem", type: "spki" }).toString();
50
+ await writeFile(privateKeyPath(), privateKey, { encoding: "utf8", mode: 0o600, flag: "wx" });
51
+ await restrictToCurrentUser(privateKeyPath());
52
+ await writeFile(publicKeyPath(), publicKey, { encoding: "utf8", flag: "wx" });
53
+ return { privateKey, publicKey };
54
+ }
55
+ export async function getCollectorPublicKey() {
56
+ return (await ensureCollectorKeys()).publicKey;
57
+ }
58
+ export function collectorFingerprint(publicKey) {
59
+ const der = createPublicKey(publicKey).export({ format: "der", type: "spki" });
60
+ return `sha256:${createHash("sha256").update(der).digest("hex")}`;
61
+ }
62
+ export async function signCollectorPayload(payload) {
63
+ const { privateKey } = await ensureCollectorKeys();
64
+ return sign(null, Buffer.from(canonicalJson(payload)), privateKey).toString("base64");
65
+ }
66
+ export function verifyCollectorPayload(payload, signature, publicKey) {
67
+ return verify(null, Buffer.from(canonicalJson(payload)), publicKey, Buffer.from(signature, "base64"));
68
+ }
@@ -0,0 +1,17 @@
1
+ export interface TokenContractConfig {
2
+ name: string;
3
+ symbol: string;
4
+ address: string | null;
5
+ decimals: number;
6
+ chainId: number;
7
+ chainName: string;
8
+ rpcUrl: string;
9
+ blockExplorer: string;
10
+ explorerUrl: string | null;
11
+ claimsAddress?: string | null;
12
+ claimsExplorerUrl?: string | null;
13
+ source: "env" | "deployment_file" | "unconfigured";
14
+ }
15
+ export declare const aipouTokenAbi: string[];
16
+ export declare const aipouClaimsAbi: string[];
17
+ export declare function getTokenContractConfig(): Promise<TokenContractConfig>;
@@ -0,0 +1,107 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export const aipouTokenAbi = [
4
+ "function name() view returns (string)",
5
+ "function symbol() view returns (string)",
6
+ "function decimals() view returns (uint8)",
7
+ "function totalSupply() view returns (uint256)",
8
+ "function balanceOf(address account) view returns (uint256)",
9
+ "function emissionController() view returns (address)",
10
+ "function mintUsageReward(address to, uint256 amount)"
11
+ ];
12
+ export const aipouClaimsAbi = [
13
+ "function validator() view returns (address)",
14
+ "function approvedRoots(bytes32 root) view returns (bool)",
15
+ "function claimedReceipts(bytes32 receiptId) view returns (bool)",
16
+ "function claim(bytes32 root, address account, uint256 amount, bytes32 receiptId, bytes32[] proof)"
17
+ ];
18
+ async function configuredClaimsAddress() {
19
+ if (process.env.AIPOU_CLAIMS_ADDRESS)
20
+ return process.env.AIPOU_CLAIMS_ADDRESS;
21
+ const claimsPath = path.resolve(process.env.AIPOU_CLAIMS_DEPLOYMENT_FILE || "deployments/base-claims.json");
22
+ try {
23
+ const deployment = JSON.parse(await readFile(claimsPath, "utf8"));
24
+ return deployment.address || null;
25
+ }
26
+ catch (error) {
27
+ if (error.code === "ENOENT")
28
+ return null;
29
+ throw error;
30
+ }
31
+ }
32
+ function envConfig() {
33
+ const address = process.env.AIPOU_CONTRACT_ADDRESS;
34
+ if (!address) {
35
+ return null;
36
+ }
37
+ const blockExplorer = process.env.AIPOU_BLOCK_EXPLORER || "https://basescan.org";
38
+ return {
39
+ name: "AI Proof of Use",
40
+ symbol: "AIPOU",
41
+ address,
42
+ decimals: 18,
43
+ chainId: Number(process.env.AIPOU_CHAIN_ID || 8453),
44
+ chainName: process.env.AIPOU_CHAIN_NAME || "Base Mainnet",
45
+ rpcUrl: process.env.AIPOU_RPC_URL || "https://mainnet.base.org",
46
+ blockExplorer,
47
+ explorerUrl: `${blockExplorer}/token/${address}`,
48
+ source: "env"
49
+ };
50
+ }
51
+ async function deploymentFileConfig() {
52
+ const deploymentPath = path.resolve(process.env.AIPOU_DEPLOYMENT_FILE || "deployments/base.json");
53
+ try {
54
+ const deployment = JSON.parse(await readFile(deploymentPath, "utf8"));
55
+ const address = deployment.token?.address;
56
+ if (!address) {
57
+ return null;
58
+ }
59
+ const blockExplorer = deployment.chain?.explorer || "https://basescan.org";
60
+ return {
61
+ name: deployment.token?.name || "AI Proof of Use",
62
+ symbol: deployment.token?.symbol || "AIPOU",
63
+ address,
64
+ decimals: deployment.token?.decimals || 18,
65
+ chainId: deployment.chain?.id || 8453,
66
+ chainName: deployment.chain?.name || "Base Mainnet",
67
+ rpcUrl: process.env.AIPOU_RPC_URL || "https://mainnet.base.org",
68
+ blockExplorer,
69
+ explorerUrl: `${blockExplorer}/token/${address}`,
70
+ source: "deployment_file"
71
+ };
72
+ }
73
+ catch (error) {
74
+ if (error.code === "ENOENT") {
75
+ return null;
76
+ }
77
+ throw error;
78
+ }
79
+ }
80
+ export async function getTokenContractConfig() {
81
+ let config;
82
+ const fromEnv = envConfig();
83
+ if (fromEnv) {
84
+ config = fromEnv;
85
+ }
86
+ else {
87
+ const fromFile = await deploymentFileConfig();
88
+ config = fromFile || {
89
+ name: "AI Proof of Use",
90
+ symbol: "AIPOU",
91
+ address: null,
92
+ decimals: 18,
93
+ chainId: 8453,
94
+ chainName: "Base Mainnet",
95
+ rpcUrl: "https://mainnet.base.org",
96
+ blockExplorer: "https://basescan.org",
97
+ explorerUrl: null,
98
+ source: "unconfigured"
99
+ };
100
+ }
101
+ const claimsAddress = await configuredClaimsAddress();
102
+ return {
103
+ ...config,
104
+ claimsAddress,
105
+ claimsExplorerUrl: claimsAddress ? `${config.blockExplorer}/address/${claimsAddress}` : null
106
+ };
107
+ }
@@ -0,0 +1,18 @@
1
+ import { Wallet } from "ethers";
2
+ import type { TaskSession } from "./types.js";
3
+ export declare const authorizationTypes: {
4
+ TaskAuthorization: {
5
+ name: string;
6
+ type: string;
7
+ }[];
8
+ };
9
+ export declare function agentWallet(): Wallet;
10
+ export declare function createTaskSession(input: {
11
+ provider: string;
12
+ model: string;
13
+ taskHash: string;
14
+ client: string;
15
+ chainId: number;
16
+ verifyingContract: string;
17
+ }): Promise<TaskSession>;
18
+ export declare function verifyTaskAuthorization(session: TaskSession): boolean;
@@ -0,0 +1,59 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { Wallet, getAddress, hexlify, isAddress, verifyTypedData } from "ethers";
3
+ export const authorizationTypes = {
4
+ TaskAuthorization: [
5
+ { name: "wallet", type: "address" },
6
+ { name: "nonce", type: "bytes32" },
7
+ { name: "taskHash", type: "bytes32" },
8
+ { name: "provider", type: "string" },
9
+ { name: "model", type: "string" },
10
+ { name: "issuedAt", type: "uint256" }
11
+ ]
12
+ };
13
+ export function agentWallet() {
14
+ const privateKey = process.env.AIPOU_AGENT_PRIVATE_KEY;
15
+ if (!privateKey) {
16
+ throw new Error("AIPOU_AGENT_PRIVATE_KEY is required; use a dedicated farming wallet, never a primary wallet");
17
+ }
18
+ return new Wallet(privateKey);
19
+ }
20
+ export async function createTaskSession(input) {
21
+ if (!isAddress(input.verifyingContract))
22
+ throw new Error("AIPOU_CLAIMS_ADDRESS must be a valid contract address");
23
+ const wallet = agentWallet();
24
+ const nonce = hexlify(randomBytes(32));
25
+ const issuedAt = Math.floor(Date.now() / 1000);
26
+ const domain = {
27
+ name: "AI Proof of Us",
28
+ version: "1",
29
+ chainId: input.chainId,
30
+ verifyingContract: getAddress(input.verifyingContract)
31
+ };
32
+ const value = {
33
+ wallet: wallet.address,
34
+ nonce,
35
+ taskHash: input.taskHash,
36
+ provider: input.provider,
37
+ model: input.model,
38
+ issuedAt
39
+ };
40
+ const walletAuthorization = await wallet.signTypedData(domain, authorizationTypes, value);
41
+ return { ...input, wallet: wallet.address, nonce, issuedAt, walletAuthorization };
42
+ }
43
+ export function verifyTaskAuthorization(session) {
44
+ const domain = {
45
+ name: "AI Proof of Us",
46
+ version: "1",
47
+ chainId: session.chainId,
48
+ verifyingContract: session.verifyingContract
49
+ };
50
+ const value = {
51
+ wallet: session.wallet,
52
+ nonce: session.nonce,
53
+ taskHash: session.taskHash,
54
+ provider: session.provider,
55
+ model: session.model,
56
+ issuedAt: session.issuedAt
57
+ };
58
+ return verifyTypedData(domain, authorizationTypes, value, session.walletAuthorization) === session.wallet;
59
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
package/dist/index.js ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { z } from "zod";
6
+ import { settleRewards } from "./claims.js";
7
+ import { collectorFingerprint, getCollectorPublicKey } from "./collector.js";
8
+ import { aipouClaimsAbi, aipouTokenAbi, getTokenContractConfig } from "./contract.js";
9
+ import { agentWallet } from "./identity.js";
10
+ import { beginTask, completeTask, exportReceipts } from "./receipts.js";
11
+ import { estimateReward } from "./rewards.js";
12
+ const bytes32 = z.string().regex(/^0x[a-fA-F0-9]{64}$/);
13
+ const usageCounts = {
14
+ inputTokens: z.number().int().min(0).max(10_000_000),
15
+ outputTokens: z.number().int().min(0).max(10_000_000),
16
+ durationSeconds: z.number().int().min(0).max(86_400)
17
+ };
18
+ const server = new McpServer({ name: "aipou-mcp", version: "0.2.0" }, {
19
+ instructions: "For meaningful AI tasks, call begin_ai_task before work and complete_ai_task after work using hashes, never raw prompts or outputs. " +
20
+ "Use the dedicated farming identity only. Never reveal private keys or local collector state. " +
21
+ "Call settle_ai_rewards only after the user explicitly asks to claim or settle their AIPOU; it submits two on-chain transactions. " +
22
+ "The host client and its user always keep the final say on transaction confirmation policy."
23
+ });
24
+ server.tool("get_aipou_contract", "Return the configured AIPOU token contract address, Base network details, explorer URL, and minimal ABI.", {}, async () => {
25
+ const contract = await getTokenContractConfig();
26
+ return {
27
+ content: [{ type: "text", text: JSON.stringify({ ...contract, tokenAbi: aipouTokenAbi, claimsAbi: aipouClaimsAbi }, null, 2) }]
28
+ };
29
+ });
30
+ server.tool("get_aipou_identity", "Return the dedicated farming wallet address and public Ed25519 collector key. Private keys are never returned.", {}, async () => {
31
+ const wallet = agentWallet();
32
+ const collectorPublicKey = await getCollectorPublicKey();
33
+ return {
34
+ content: [{
35
+ type: "text",
36
+ text: JSON.stringify({ wallet: wallet.address, collectorPublicKey, collectorFingerprint: collectorFingerprint(collectorPublicKey) }, null, 2)
37
+ }]
38
+ };
39
+ });
40
+ server.tool("estimate_ai_reward", "Estimate the client-signed AIPOU reward before a task is completed. The final tier is derived by the validator.", usageCounts, async (input) => ({
41
+ content: [{
42
+ type: "text",
43
+ text: JSON.stringify({ estimatedReward: estimateReward({ ...input, trustTier: "client_signed" }), unit: "AIPOU" }, null, 2)
44
+ }]
45
+ }));
46
+ server.tool("begin_ai_task", "Create a unique task nonce and EIP-712 authorization signed by the dedicated farming wallet.", {
47
+ provider: z.string().min(1).max(64),
48
+ model: z.string().min(1).max(128),
49
+ taskHash: bytes32,
50
+ client: z.string().min(1).max(64)
51
+ }, async (input) => {
52
+ const contract = await getTokenContractConfig();
53
+ const verifyingContract = process.env.AIPOU_CLAIMS_ADDRESS;
54
+ if (!verifyingContract)
55
+ throw new Error("AIPOU_CLAIMS_ADDRESS is required before tasks can begin");
56
+ const session = await beginTask({
57
+ ...input,
58
+ chainId: contract.chainId,
59
+ verifyingContract
60
+ });
61
+ return { content: [{ type: "text", text: JSON.stringify(session, null, 2) }] };
62
+ });
63
+ server.tool("complete_ai_task", "Complete a unique task, derive its trust tier, reject duplicate evidence, and create an Ed25519-signed receipt.", {
64
+ nonce: bytes32,
65
+ ...usageCounts,
66
+ outputHash: bytes32,
67
+ providerEvidence: z.object({
68
+ keyId: z.string().min(1).max(128),
69
+ signature: z.string().min(32)
70
+ }).optional()
71
+ }, async (input) => {
72
+ const receipt = await completeTask(input);
73
+ return { content: [{ type: "text", text: JSON.stringify(receipt, null, 2) }] };
74
+ });
75
+ server.tool("export_ai_receipts", "Export stored signed AI task receipts, optionally filtered by wallet.", { wallet: z.string().regex(/^0x[a-fA-F0-9]{40}$/).optional() }, async ({ wallet }) => {
76
+ const receipts = await exportReceipts(wallet);
77
+ return { content: [{ type: "text", text: JSON.stringify({ count: receipts.length, receipts }, null, 2) }] };
78
+ });
79
+ server.tool("settle_ai_rewards", "After an explicit user claim request, validate eligible receipts against the settlement policy, publish a Merkle root, and mint the included rewards. Submits two on-chain transactions; the host's own confirmation policy applies.", { maxReceipts: z.number().int().min(1).max(100).default(25) }, async ({ maxReceipts }) => {
80
+ const batch = await settleRewards(maxReceipts);
81
+ return { content: [{ type: "text", text: JSON.stringify(batch, null, 2) }] };
82
+ });
83
+ const transport = new StdioServerTransport();
84
+ await server.connect(transport);
package/dist/lock.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function withFileLock<T>(lockPath: string, operation: () => Promise<T>): Promise<T>;
package/dist/lock.js ADDED
@@ -0,0 +1,42 @@
1
+ import { rm, stat, writeFile } from "node:fs/promises";
2
+ // State mutations finish in well under a second, so a lock older than this
3
+ // belongs to a crashed process and can be reclaimed safely.
4
+ const STALE_LOCK_MS = 15_000;
5
+ const ACQUIRE_TIMEOUT_MS = 10_000;
6
+ async function acquire(lockPath) {
7
+ const deadline = Date.now() + ACQUIRE_TIMEOUT_MS;
8
+ for (;;) {
9
+ try {
10
+ await writeFile(lockPath, `${process.pid}\n`, { encoding: "utf8", flag: "wx" });
11
+ return;
12
+ }
13
+ catch (error) {
14
+ if (error.code !== "EEXIST")
15
+ throw error;
16
+ }
17
+ try {
18
+ const info = await stat(lockPath);
19
+ if (Date.now() - info.mtimeMs > STALE_LOCK_MS) {
20
+ await rm(lockPath, { force: true });
21
+ continue;
22
+ }
23
+ }
24
+ catch {
25
+ // The holder released the lock between attempts; retry immediately.
26
+ continue;
27
+ }
28
+ if (Date.now() > deadline) {
29
+ throw new Error(`Timed out waiting for lock ${lockPath}; remove it manually if no other AIPOU process is running`);
30
+ }
31
+ await new Promise((resolve) => setTimeout(resolve, 25 + Math.floor(Math.random() * 75)));
32
+ }
33
+ }
34
+ export async function withFileLock(lockPath, operation) {
35
+ await acquire(lockPath);
36
+ try {
37
+ return await operation();
38
+ }
39
+ finally {
40
+ await rm(lockPath, { force: true });
41
+ }
42
+ }