mint-day 0.3.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,166 @@
1
+ # mint.day
2
+
3
+ Agents mint NFTs now.
4
+
5
+ ## Quick start
6
+
7
+ **1. Install** (zero config)
8
+
9
+ ```json
10
+ {
11
+ "mcpServers": {
12
+ "mint-day": {
13
+ "command": "npx",
14
+ "args": ["-y", "mint-day"]
15
+ }
16
+ }
17
+ }
18
+ ```
19
+
20
+ Works with Claude Code, Cursor, Windsurf, OpenClaw, and any MCP client.
21
+
22
+ **2. Mint**
23
+
24
+ Tell your agent: "Proof I completed the security audit for 0xABC"
25
+
26
+ **3. Verify**
27
+
28
+ ```
29
+ mint_check({ address: "0xABC..." })
30
+ ```
31
+
32
+ That's it. Your agent classifies the intent, builds the token, and returns a ready-to-sign transaction.
33
+
34
+ ## Signing transactions
35
+
36
+ mint.day can sign and submit transactions directly, or return calldata for your own signer.
37
+
38
+ ### Option A: Built-in signing (recommended)
39
+
40
+ Save a private key and mint.day handles everything:
41
+
42
+ ```bash
43
+ mkdir -p ~/.mint-day
44
+ node -e "const w = require('ethers').Wallet.createRandom(); console.log(w.privateKey); console.error('Address: ' + w.address)" 2>&1 | head -1 > ~/.mint-day/credentials
45
+ chmod 600 ~/.mint-day/credentials
46
+ ```
47
+
48
+ Fund the address with a small amount of ETH on Base (gas is < $0.01 per mint). Restart the MCP server.
49
+
50
+ You can also set `PRIVATE_KEY` in your MCP env config instead of using the file.
51
+
52
+ ### Option B: Bring your own signer
53
+
54
+ Don't set a private key. mint.day returns calldata. Submit it with Coinbase AgentKit, Lit Protocol, Privy, or any EVM wallet.
55
+
56
+ ## Tools
57
+
58
+ ### `mint`
59
+
60
+ Create a permanent, verifiable on-chain record on Base.
61
+
62
+ **Natural language:**
63
+
64
+ ```
65
+ mint({ description: "Proof I completed the security audit for 0xABC" })
66
+ ```
67
+
68
+ **Structured** (skips classification):
69
+
70
+ ```
71
+ mint({
72
+ description: "Task completion attestation",
73
+ tokenType: "Attestation",
74
+ recipient: "0xABC...",
75
+ soulbound: true
76
+ })
77
+ ```
78
+
79
+ **With an image:**
80
+
81
+ ```
82
+ mint({
83
+ description: "My agent identity",
84
+ tokenType: "Identity",
85
+ image: "data:image/png;base64,..."
86
+ })
87
+ ```
88
+
89
+ Every mint returns a preview first. Confirm with `mintId` to execute:
90
+
91
+ ```
92
+ mint({ mintId: "a3f8c1e90b2d" })
93
+ ```
94
+
95
+ With built-in signing, you get back a tx hash and explorer link. Without it, you get calldata.
96
+
97
+ ### `mint_check`
98
+
99
+ Look up tokens by address, transaction hash, or get global stats.
100
+
101
+ ```
102
+ mint_check({ address: "0xABC..." })
103
+ mint_check({ txHash: "0x230b..." })
104
+ mint_check({}) // global stats
105
+ ```
106
+
107
+ ### `mint_resolve`
108
+
109
+ Resolve an agent's on-chain identity. Returns their Identity token with ERC-8004 agent card metadata.
110
+
111
+ ```
112
+ mint_resolve({ address: "0xABC..." })
113
+ ```
114
+
115
+ ## Token types
116
+
117
+ | Type | Default | Use for |
118
+ |------|---------|---------|
119
+ | Identity | transferable | Agent ID card, on-chain registration |
120
+ | Attestation | soulbound | Proof of action, task completion |
121
+ | Credential | soulbound | Reputation anchor, certification |
122
+ | Receipt | transferable | Payment record between two parties |
123
+ | Pass | transferable | API access, capability unlock |
124
+
125
+ All types support `image` and `animation_url` for visual NFTs (PFPs, art, collectibles).
126
+
127
+ ## Architecture
128
+
129
+ ```
130
+ Agent -> mint tool -> classifier (Groq Llama) -> metadata builder -> calldata
131
+ |
132
+ [if PRIVATE_KEY] sign + submit -> tx hash <----|
133
+ [if no key] return calldata <-------------|
134
+ |
135
+ MintFactory.sol (Base)
136
+ ```
137
+
138
+ ## Contracts
139
+
140
+ - **Base Mainnet**: [`0xbf12d372444dcf69df9316d961439f6b5919e8d0`](https://basescan.org/address/0xbf12d372444dcf69df9316d961439f6b5919e8d0)
141
+ - **Base Sepolia**: [`0xa52450397f312c256Bd68B202C0CF90387Ea0E67`](https://sepolia.basescan.org/address/0xa52450397f312c256Bd68B202C0CF90387Ea0E67)
142
+
143
+ ## Configuration
144
+
145
+ All optional. mint.day works with zero config.
146
+
147
+ | Env var | Default | Purpose |
148
+ |---------|---------|---------|
149
+ | `PRIVATE_KEY` | `~/.mint-day/credentials` | Signs and submits transactions |
150
+ | `BASE_RPC_URL` | `https://mainnet.base.org` | RPC endpoint |
151
+ | `CHAIN_ID` | `8453` | Base Mainnet |
152
+ | `MINT_FACTORY_ADDRESS` | `0xbf12d3...` | Contract address |
153
+ | `CLASSIFY_URL` | hosted endpoint | Intent classifier |
154
+
155
+ ## Development
156
+
157
+ ```bash
158
+ npm install
159
+ npm run dev # run with tsx
160
+ npm run build # compile to dist/
161
+ npm start # run compiled
162
+ ```
163
+
164
+ ## License
165
+
166
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,78 @@
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 { ethers } from "ethers";
5
+ import { readFileSync, existsSync } from "fs";
6
+ import { join } from "path";
7
+ import { homedir } from "os";
8
+ import dotenv from "dotenv";
9
+ import { CalldataService } from "./services/calldata.js";
10
+ import { mintSchema, handleMint } from "./tools/mint.js";
11
+ import { mintCheckSchema, handleMintCheck } from "./tools/mint-check.js";
12
+ import { mintResolveSchema, handleMintResolve } from "./tools/mint-resolve.js";
13
+ dotenv.config();
14
+ // Base mainnet
15
+ const MAINNET_CONTRACT = "0x12a1c11a0b2860f64e7d8df20989f97d40de7f2c";
16
+ const MAINNET_RPC = "https://mainnet.base.org";
17
+ const MAINNET_CHAIN = 8453;
18
+ // Sponsored gas wallet (Base mainnet, pre-funded with small ETH balance)
19
+ // Used for zero-config mints when no PRIVATE_KEY is set
20
+ const SPONSORED_KEY = process.env.SPONSORED_KEY || "0x1c9acdf008d822c2b0093d9194c6115b94e9c0003c2fd1a1ff03abff2c77a22e";
21
+ const SPONSORED_CONTRACT = MAINNET_CONTRACT;
22
+ const SPONSORED_RPC = MAINNET_RPC;
23
+ const SPONSORED_CHAIN = MAINNET_CHAIN;
24
+ // Sepolia fallback (when sponsored limits are exhausted)
25
+ const FALLBACK_CONTRACT = "0x16e0072Eb4131106e266035E98Cfd07532B88EAa";
26
+ const FALLBACK_RPC = "https://sepolia.base.org";
27
+ const FALLBACK_CHAIN = 84532;
28
+ // Load private key: env var > ~/.mint-day/credentials file
29
+ function loadPrivateKey() {
30
+ if (process.env.PRIVATE_KEY)
31
+ return process.env.PRIVATE_KEY;
32
+ const credPath = join(homedir(), ".mint-day", "credentials");
33
+ if (existsSync(credPath)) {
34
+ const key = readFileSync(credPath, "utf-8").trim();
35
+ if (key)
36
+ return key;
37
+ }
38
+ return "";
39
+ }
40
+ const userKey = loadPrivateKey();
41
+ const hasUserKey = !!userKey;
42
+ // Always mainnet. Sponsored gas wallet handles zero-config mints.
43
+ const chainId = parseInt(process.env.CHAIN_ID || String(hasUserKey ? MAINNET_CHAIN : MAINNET_CHAIN));
44
+ const contractAddress = process.env.MINT_FACTORY_ADDRESS || MAINNET_CONTRACT;
45
+ const rpcUrl = process.env.BASE_RPC_URL || MAINNET_RPC;
46
+ let signerAddress = "";
47
+ if (hasUserKey) {
48
+ try {
49
+ const wallet = new ethers.Wallet(userKey);
50
+ signerAddress = wallet.address;
51
+ }
52
+ catch {
53
+ console.error("Invalid private key");
54
+ process.exit(1);
55
+ }
56
+ }
57
+ const defaultRecipient = process.env.DEFAULT_RECIPIENT || signerAddress || "";
58
+ const calldataService = new CalldataService(rpcUrl, contractAddress, chainId);
59
+ const server = new McpServer({
60
+ name: "mint-day",
61
+ version: "0.3.0",
62
+ });
63
+ server.tool("mint", "Create a permanent, verifiable on-chain record on Base. Use this when you need to: prove you completed a task, establish your identity, record an agreement with another agent, issue or receive a credential, grant access to a capability, or mint a visual token (PFP, art, collectible). The token is immutable and publicly verifiable. Provide an image URL, local file path, or base64 for visual tokens (local files and base64 are auto-uploaded to Imgur if IMGUR_CLIENT_ID is configured). Returns a preview with mintId first; call again with mintId to confirm. If PRIVATE_KEY is configured, the server signs and submits the transaction directly and returns a tx hash. If not, returns raw calldata for your own signer (Coinbase AgentKit, Lit Protocol, or any EVM wallet).", mintSchema, async (params) => handleMint(params, calldataService, defaultRecipient, userKey, {
64
+ sponsoredKey: SPONSORED_KEY,
65
+ sponsoredContract: SPONSORED_CONTRACT,
66
+ sponsoredRpc: SPONSORED_RPC,
67
+ sponsoredChain: SPONSORED_CHAIN,
68
+ fallbackContract: FALLBACK_CONTRACT,
69
+ fallbackRpc: FALLBACK_RPC,
70
+ fallbackChain: FALLBACK_CHAIN,
71
+ }));
72
+ server.tool("mint_check", "Look up mint.day tokens. With an address: returns all tokens held with type, metadata, soulbound status, and mint timestamp. Without an address: returns global stats (total minted, current fee). Use to verify credentials, check attestations, or browse on-chain records.", mintCheckSchema, async (params) => handleMintCheck(params, calldataService, calldataService.provider, contractAddress, chainId));
73
+ server.tool("mint_resolve", "Resolve an agent's on-chain identity. Returns their Identity token with ERC-8004 agent card metadata: did, capabilities, endpoints, and image. Use this before transacting with another agent to verify who they are.", mintResolveSchema, async (params) => handleMintResolve(params, calldataService.provider, contractAddress, chainId));
74
+ async function main() {
75
+ const transport = new StdioServerTransport();
76
+ await server.connect(transport);
77
+ }
78
+ main().catch(console.error);
@@ -0,0 +1,12 @@
1
+ import { ethers } from "ethers";
2
+ import { MintIntent, CalldataResult } from "../types.js";
3
+ export declare class CalldataService {
4
+ private contract;
5
+ private contractAddress;
6
+ private chainId;
7
+ readonly provider: ethers.JsonRpcProvider;
8
+ constructor(rpcUrl: string, contractAddress: string, chainId: number);
9
+ buildMintCalldata(intent: MintIntent): Promise<CalldataResult>;
10
+ totalMinted(): Promise<string>;
11
+ mintFee(): Promise<string>;
12
+ }
@@ -0,0 +1,61 @@
1
+ import { ethers } from "ethers";
2
+ import { TOKEN_TYPE_NAMES } from "../types.js";
3
+ import { encodeMetadata } from "./metadata.js";
4
+ const MINT_FACTORY_ABI = [
5
+ "function mint(address to, string uri, uint8 tokenType, bool soulbound) payable returns (uint256)",
6
+ "function mintFee() view returns (uint256)",
7
+ "function totalMinted() view returns (uint256)",
8
+ ];
9
+ export class CalldataService {
10
+ contract;
11
+ contractAddress;
12
+ chainId;
13
+ provider;
14
+ constructor(rpcUrl, contractAddress, chainId) {
15
+ this.provider = new ethers.JsonRpcProvider(rpcUrl);
16
+ this.contract = new ethers.Contract(contractAddress, MINT_FACTORY_ABI, this.provider);
17
+ this.contractAddress = contractAddress;
18
+ this.chainId = chainId;
19
+ }
20
+ async buildMintCalldata(intent) {
21
+ const tokenURI = encodeMetadata(intent.metadata);
22
+ const calldata = this.contract.interface.encodeFunctionData("mint", [
23
+ intent.recipient,
24
+ tokenURI,
25
+ intent.tokenType,
26
+ intent.soulbound,
27
+ ]);
28
+ const mintFee = await this.contract.mintFee();
29
+ // Dynamic gas estimation with 20% buffer, fallback to safe static value
30
+ let estimatedGas = 600000;
31
+ try {
32
+ const estimate = await this.provider.estimateGas({
33
+ to: this.contractAddress,
34
+ data: calldata,
35
+ value: mintFee,
36
+ });
37
+ estimatedGas = Math.ceil(Number(estimate) * 1.2);
38
+ }
39
+ catch {
40
+ // Estimation can fail if caller has no funds; use safe default
41
+ }
42
+ return {
43
+ to: this.contractAddress,
44
+ calldata,
45
+ value: mintFee.toString(),
46
+ estimatedGas,
47
+ chainId: this.chainId,
48
+ metadata: intent.metadata,
49
+ tokenType: TOKEN_TYPE_NAMES[intent.tokenType],
50
+ soulbound: intent.soulbound,
51
+ };
52
+ }
53
+ async totalMinted() {
54
+ const total = await this.contract.totalMinted();
55
+ return total.toString();
56
+ }
57
+ async mintFee() {
58
+ const fee = await this.contract.mintFee();
59
+ return ethers.formatEther(fee);
60
+ }
61
+ }
@@ -0,0 +1,2 @@
1
+ import { MintIntent } from "../types.js";
2
+ export declare function classifyIntent(text: string, fallbackRecipient: string): Promise<MintIntent>;
@@ -0,0 +1,81 @@
1
+ import { TokenType, TOKEN_TYPE_NAMES } from "../types.js";
2
+ const CLASSIFY_URL = process.env.CLASSIFY_URL || "https://agent-mint-nine.vercel.app/api/classify";
3
+ function extractAddress(text) {
4
+ const match = text.match(/0x[a-fA-F0-9]{40}/);
5
+ return match ? match[0] : null;
6
+ }
7
+ async function classify(text) {
8
+ const response = await fetch(CLASSIFY_URL, {
9
+ method: "POST",
10
+ headers: { "Content-Type": "application/json" },
11
+ body: JSON.stringify({ text }),
12
+ });
13
+ if (!response.ok) {
14
+ throw new Error(`Classify API error: ${response.status}`);
15
+ }
16
+ return await response.json();
17
+ }
18
+ export async function classifyIntent(text, fallbackRecipient) {
19
+ let parsed;
20
+ try {
21
+ parsed = await classify(text);
22
+ }
23
+ catch {
24
+ // If API fails, use keyword fallback
25
+ parsed = keywordFallback(text);
26
+ }
27
+ const typeName = String(parsed.tokenType || "Attestation");
28
+ const typeIndex = TOKEN_TYPE_NAMES.indexOf(typeName);
29
+ const tokenType = typeIndex >= 0 ? typeIndex : TokenType.Attestation;
30
+ const soulbound = parsed.soulbound ?? (tokenType === TokenType.Attestation || tokenType === TokenType.Credential);
31
+ const name = String(parsed.name || "mint.day token");
32
+ const recipient = extractAddress(text) || fallbackRecipient;
33
+ const extractedFields = parsed.extractedFields || {};
34
+ const missingFields = parsed.missingFields || [];
35
+ // For Identity tokens, auto-generate did if missing
36
+ if (tokenType === TokenType.Identity && !extractedFields.did) {
37
+ extractedFields.did = `did:pkh:eip155:${parseInt(process.env.CHAIN_ID || "8453")}:${recipient}`;
38
+ }
39
+ // Default empty arrays for Identity ERC-8004 fields
40
+ if (tokenType === TokenType.Identity) {
41
+ if (!extractedFields.capabilities)
42
+ extractedFields.capabilities = [];
43
+ if (!extractedFields.endpoints)
44
+ extractedFields.endpoints = [];
45
+ }
46
+ return {
47
+ tokenType,
48
+ soulbound,
49
+ recipient,
50
+ missingFields: tokenType === TokenType.Identity ? missingFields.filter((f) => f !== "did" && !extractedFields[f]?.length) : [],
51
+ metadata: {
52
+ name,
53
+ description: text,
54
+ tokenType: TOKEN_TYPE_NAMES[tokenType],
55
+ soulbound,
56
+ creator: fallbackRecipient,
57
+ recipient,
58
+ timestamp: new Date().toISOString(),
59
+ chainId: parseInt(process.env.CHAIN_ID || "8453"),
60
+ mintday_version: "1",
61
+ ...extractedFields,
62
+ },
63
+ };
64
+ }
65
+ function keywordFallback(text) {
66
+ const lower = text.toLowerCase();
67
+ if (/identity|who i am|register|agent card/.test(lower)) {
68
+ return { tokenType: "Identity", soulbound: false, name: "mint.day token", extractedFields: {}, missingFields: [] };
69
+ }
70
+ if (/credential|vetted|reputation|certified|qualified/.test(lower)) {
71
+ return { tokenType: "Credential", soulbound: true, name: "mint.day token", extractedFields: {}, missingFields: [] };
72
+ }
73
+ if (/receipt|payment|settled|paid|invoice/.test(lower)) {
74
+ return { tokenType: "Receipt", soulbound: false, name: "mint.day token", extractedFields: {}, missingFields: [] };
75
+ }
76
+ if (/pass|access|api|grant|permission|key/.test(lower)) {
77
+ return { tokenType: "Pass", soulbound: false, name: "mint.day token", extractedFields: {}, missingFields: [] };
78
+ }
79
+ // Default: Attestation (proof of action)
80
+ return { tokenType: "Attestation", soulbound: true, name: "mint.day token", extractedFields: {}, missingFields: [] };
81
+ }
@@ -0,0 +1,3 @@
1
+ export declare function looksLikeEns(input: string): boolean;
2
+ export declare function resolveEns(nameOrAddress: string): Promise<string>;
3
+ export declare function reverseResolve(address: string): Promise<string | null>;
@@ -0,0 +1,32 @@
1
+ import { ethers } from "ethers";
2
+ // ENS lives on Ethereum mainnet, need an L1 provider for resolution
3
+ const ENS_RPC = process.env.ENS_RPC_URL || "https://eth.llamarpc.com";
4
+ let ensProvider = null;
5
+ function getEnsProvider() {
6
+ if (!ensProvider) {
7
+ ensProvider = new ethers.JsonRpcProvider(ENS_RPC);
8
+ }
9
+ return ensProvider;
10
+ }
11
+ export function looksLikeEns(input) {
12
+ return /^[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$/.test(input) && !input.startsWith("0x");
13
+ }
14
+ export async function resolveEns(nameOrAddress) {
15
+ if (!looksLikeEns(nameOrAddress))
16
+ return nameOrAddress;
17
+ const provider = getEnsProvider();
18
+ const resolved = await provider.resolveName(nameOrAddress);
19
+ if (!resolved) {
20
+ throw new Error(`Could not resolve ENS name: ${nameOrAddress}`);
21
+ }
22
+ return resolved;
23
+ }
24
+ export async function reverseResolve(address) {
25
+ try {
26
+ const provider = getEnsProvider();
27
+ return await provider.lookupAddress(address);
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Resolve an image value to a URL suitable for on-chain metadata.
3
+ *
4
+ * - URLs pass through unchanged.
5
+ * - Local file paths and data URIs are uploaded to 0x0.st (no API key needed).
6
+ * - Falls back to base64 data URI if upload fails.
7
+ */
8
+ export declare function resolveImage(image: string): Promise<{
9
+ url: string;
10
+ uploaded: boolean;
11
+ }>;
@@ -0,0 +1,70 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { basename } from "path";
3
+ function isLocalPath(value) {
4
+ return (value.startsWith("/") ||
5
+ value.startsWith("~/") ||
6
+ value.startsWith("./") ||
7
+ value.startsWith("../"));
8
+ }
9
+ function isDataUri(value) {
10
+ return value.startsWith("data:");
11
+ }
12
+ function isUrl(value) {
13
+ return value.startsWith("http://") || value.startsWith("https://");
14
+ }
15
+ function resolvePath(path) {
16
+ return path.startsWith("~/")
17
+ ? path.replace("~", process.env.HOME || "")
18
+ : path;
19
+ }
20
+ /**
21
+ * Resolve an image value to a URL suitable for on-chain metadata.
22
+ *
23
+ * - URLs pass through unchanged.
24
+ * - Local file paths and data URIs are uploaded to 0x0.st (no API key needed).
25
+ * - Falls back to base64 data URI if upload fails.
26
+ */
27
+ export async function resolveImage(image) {
28
+ if (isUrl(image)) {
29
+ return { url: image, uploaded: false };
30
+ }
31
+ let fileBuffer;
32
+ let fileName = "image.png";
33
+ if (isLocalPath(image)) {
34
+ const resolved = resolvePath(image);
35
+ if (!existsSync(resolved)) {
36
+ throw new Error(`Image file not found: ${resolved}`);
37
+ }
38
+ fileBuffer = readFileSync(resolved);
39
+ fileName = basename(resolved);
40
+ }
41
+ else if (isDataUri(image)) {
42
+ const match = image.match(/^data:([^;]+);base64,(.+)$/);
43
+ if (!match) {
44
+ throw new Error("Invalid data URI format");
45
+ }
46
+ fileBuffer = Buffer.from(match[2], "base64");
47
+ const ext = match[1].split("/")[1] || "png";
48
+ fileName = `image.${ext}`;
49
+ }
50
+ else {
51
+ return { url: image, uploaded: false };
52
+ }
53
+ const url = await uploadTo0x0(fileBuffer, fileName);
54
+ return { url, uploaded: true };
55
+ }
56
+ async function uploadTo0x0(buffer, fileName) {
57
+ const uint8 = new Uint8Array(buffer);
58
+ const blob = new Blob([uint8]);
59
+ const formData = new FormData();
60
+ formData.append("file", blob, fileName);
61
+ const res = await fetch("https://0x0.st", {
62
+ method: "POST",
63
+ headers: { "User-Agent": "mint.day/0.2.0" },
64
+ body: formData,
65
+ });
66
+ if (!res.ok) {
67
+ throw new Error(`0x0.st upload failed (${res.status})`);
68
+ }
69
+ return (await res.text()).trim();
70
+ }
@@ -0,0 +1,2 @@
1
+ import { TokenMetadata } from "../types.js";
2
+ export declare function encodeMetadata(metadata: TokenMetadata): string;
@@ -0,0 +1,119 @@
1
+ const TYPE_COLORS = {
2
+ Identity: { bg: "#0c1220", accent: "#60a5fa", text: "#93bbfc" },
3
+ Attestation: { bg: "#0a1510", accent: "#22c55e", text: "#6ee7a0" },
4
+ Credential: { bg: "#100c1e", accent: "#a78bfa", text: "#c4b5fd" },
5
+ Receipt: { bg: "#151008", accent: "#fb923c", text: "#fdba74" },
6
+ Pass: { bg: "#0a1518", accent: "#22d3ee", text: "#67e8f9" },
7
+ };
8
+ function generateDefaultImage(metadata) {
9
+ const colors = TYPE_COLORS[metadata.tokenType] || TYPE_COLORS.Attestation;
10
+ const name = metadata.name || "mint.day token";
11
+ const tokenType = metadata.tokenType || "Token";
12
+ const soulbound = metadata.soulbound;
13
+ // Truncate name for display
14
+ const displayName = name.length > 80 ? name.slice(0, 77) + "..." : name;
15
+ // Split into lines if long
16
+ const words = displayName.split(" ");
17
+ const lines = [];
18
+ let current = "";
19
+ for (const word of words) {
20
+ if ((current + " " + word).trim().length > 30) {
21
+ lines.push(current.trim());
22
+ current = word;
23
+ }
24
+ else {
25
+ current = (current + " " + word).trim();
26
+ }
27
+ }
28
+ if (current)
29
+ lines.push(current.trim());
30
+ const nameLines = lines.slice(0, 4).map((line, i) => `<text x="40" y="${185 + i * 28}" font-family="monospace" font-size="18" font-weight="600" fill="${colors.text}">${escapeXml(line)}</text>`).join("\n ");
31
+ // Subtle pattern based on token type
32
+ const patternId = `p-${tokenType.toLowerCase()}`;
33
+ let pattern = "";
34
+ if (tokenType === "Identity") {
35
+ pattern = `<pattern id="${patternId}" width="40" height="40" patternUnits="userSpaceOnUse">
36
+ <circle cx="20" cy="20" r="1" fill="${colors.accent}" opacity="0.08"/>
37
+ </pattern>`;
38
+ }
39
+ else if (tokenType === "Attestation") {
40
+ pattern = `<pattern id="${patternId}" width="32" height="32" patternUnits="userSpaceOnUse">
41
+ <line x1="0" y1="32" x2="32" y2="0" stroke="${colors.accent}" stroke-width="0.5" opacity="0.06"/>
42
+ </pattern>`;
43
+ }
44
+ else if (tokenType === "Credential") {
45
+ pattern = `<pattern id="${patternId}" width="24" height="24" patternUnits="userSpaceOnUse">
46
+ <rect x="11" y="11" width="2" height="2" fill="${colors.accent}" opacity="0.08"/>
47
+ </pattern>`;
48
+ }
49
+ else if (tokenType === "Receipt") {
50
+ pattern = `<pattern id="${patternId}" width="36" height="36" patternUnits="userSpaceOnUse">
51
+ <line x1="0" y1="18" x2="36" y2="18" stroke="${colors.accent}" stroke-width="0.5" opacity="0.06"/>
52
+ </pattern>`;
53
+ }
54
+ else {
55
+ pattern = `<pattern id="${patternId}" width="28" height="28" patternUnits="userSpaceOnUse">
56
+ <circle cx="14" cy="14" r="0.8" fill="${colors.accent}" opacity="0.08"/>
57
+ </pattern>`;
58
+ }
59
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="400" height="400">
60
+ <defs>
61
+ ${pattern}
62
+ </defs>
63
+ <rect width="400" height="400" fill="${colors.bg}"/>
64
+ <rect width="400" height="400" fill="url(#${patternId})"/>
65
+
66
+ <!-- Top accent line -->
67
+ <rect x="40" y="40" width="48" height="3" rx="1.5" fill="${colors.accent}" opacity="0.8"/>
68
+
69
+ <!-- Token type label -->
70
+ <text x="40" y="80" font-family="monospace" font-size="12" fill="${colors.accent}" opacity="0.6" text-transform="uppercase" letter-spacing="2">${escapeXml(tokenType.toUpperCase())}</text>
71
+
72
+ <!-- Soulbound badge -->
73
+ ${soulbound ? `<text x="40" y="100" font-family="monospace" font-size="10" fill="${colors.accent}" opacity="0.35">SOULBOUND</text>` : ""}
74
+
75
+ <!-- Token name -->
76
+ ${nameLines}
77
+
78
+ <!-- Bottom branding -->
79
+ <text x="40" y="350" font-family="monospace" font-size="11" fill="${colors.accent}" opacity="0.2">mint.day</text>
80
+
81
+ <!-- Bottom accent line -->
82
+ <rect x="40" y="362" width="320" height="1" rx="0.5" fill="${colors.accent}" opacity="0.1"/>
83
+ </svg>`;
84
+ return `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
85
+ }
86
+ function escapeXml(str) {
87
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
88
+ }
89
+ export function encodeMetadata(metadata) {
90
+ // Auto-generate image if none provided
91
+ if (!metadata.image) {
92
+ metadata.image = generateDefaultImage(metadata);
93
+ }
94
+ // Build OpenSea-compatible attributes array
95
+ const attributes = [
96
+ { trait_type: "Token Type", value: metadata.tokenType },
97
+ { trait_type: "Soulbound", value: metadata.soulbound ? "Yes" : "No" },
98
+ ];
99
+ if (metadata.creator && metadata.creator !== "0x0000000000000000000000000000000000000000") {
100
+ attributes.push({ trait_type: "Creator", value: metadata.creator });
101
+ }
102
+ if (metadata.recipient) {
103
+ attributes.push({ trait_type: "Recipient", value: metadata.recipient });
104
+ }
105
+ // Add any capabilities as traits
106
+ if (Array.isArray(metadata.capabilities)) {
107
+ for (const cap of metadata.capabilities) {
108
+ attributes.push({ trait_type: "Capability", value: cap });
109
+ }
110
+ }
111
+ const enriched = {
112
+ ...metadata,
113
+ attributes,
114
+ external_url: "https://agent-mint-nine.vercel.app",
115
+ };
116
+ const json = JSON.stringify(enriched);
117
+ const base64 = Buffer.from(json).toString("base64");
118
+ return `data:application/json;base64,${base64}`;
119
+ }
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import { ethers } from "ethers";
3
+ import { CalldataService } from "../services/calldata.js";
4
+ export declare const mintCheckSchema: {
5
+ address: z.ZodOptional<z.ZodString>;
6
+ txHash: z.ZodOptional<z.ZodString>;
7
+ };
8
+ export declare function handleMintCheck(params: {
9
+ address?: string;
10
+ txHash?: string;
11
+ }, calldataService: CalldataService, provider: ethers.JsonRpcProvider, contractAddress: string, chainId: number): Promise<{
12
+ content: {
13
+ type: "text";
14
+ text: string;
15
+ }[];
16
+ }>;