mint-day 0.3.3 → 0.4.2

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.
@@ -0,0 +1,69 @@
1
+ name: Sentry Auto-Triage
2
+
3
+ on:
4
+ repository_dispatch:
5
+ types: [sentry_issue]
6
+
7
+ permissions:
8
+ contents: write
9
+ pull-requests: write
10
+ issues: write
11
+ id-token: write
12
+
13
+ jobs:
14
+ triage:
15
+ runs-on: ubuntu-latest
16
+ timeout-minutes: 10
17
+
18
+ env:
19
+ SENTRY_TITLE: ${{ github.event.client_payload.title }}
20
+ SENTRY_CULPRIT: ${{ github.event.client_payload.culprit }}
21
+ SENTRY_LEVEL: ${{ github.event.client_payload.level }}
22
+ SENTRY_FIRST_SEEN: ${{ github.event.client_payload.first_seen }}
23
+ SENTRY_COUNT: ${{ github.event.client_payload.count }}
24
+ SENTRY_URL: ${{ github.event.client_payload.url }}
25
+ SENTRY_SHORT_ID: ${{ github.event.client_payload.short_id }}
26
+ SENTRY_TAGS: ${{ toJSON(github.event.client_payload.tags) }}
27
+ SENTRY_METADATA: ${{ toJSON(github.event.client_payload.metadata) }}
28
+
29
+ steps:
30
+ - name: Checkout
31
+ uses: actions/checkout@v4
32
+
33
+ - name: Triage with Claude Code
34
+ uses: anthropics/claude-code-action@v1
35
+ with:
36
+ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
37
+ prompt: |
38
+ A Sentry error was detected in the mint.day codebase. Triage it.
39
+
40
+ ## Error Details
41
+ - **Title**: ${SENTRY_TITLE}
42
+ - **Culprit**: ${SENTRY_CULPRIT}
43
+ - **Level**: ${SENTRY_LEVEL}
44
+ - **First seen**: ${SENTRY_FIRST_SEEN}
45
+ - **Count**: ${SENTRY_COUNT}
46
+ - **Sentry URL**: ${SENTRY_URL}
47
+ - **Tags**: ${SENTRY_TAGS}
48
+ - **Metadata**: ${SENTRY_METADATA}
49
+
50
+ ## Your job
51
+
52
+ 1. Read the relevant source files in `api/` and `src/` to understand the error.
53
+ 2. Determine: is this a real bug, or noise (transient network error, user input, rate limit)?
54
+
55
+ **If real bug:**
56
+ - Create a new branch `fix/sentry-${SENTRY_SHORT_ID}`
57
+ - Write the fix
58
+ - Commit and push
59
+ - Open a PR with the Sentry link in the description
60
+
61
+ **If noise:**
62
+ - Create an issue titled "Noise" with label "noise" and explain why it's not actionable
63
+ - Reference the Sentry short ID in the issue body
64
+
65
+ ## Context
66
+ - This is a Vercel serverless project (api/ directory has the functions)
67
+ - The MCP server is in src/
68
+ - Contract ABIs and addresses are in the source files
69
+ - Base mainnet RPC can be flaky — transient RPC errors are noise
package/api/classify.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { VercelRequest, VercelResponse } from "@vercel/node";
2
+ import { captureError } from "./lib/sentry.js";
2
3
 
3
4
  const GROQ_API_KEY = process.env.GROQ_API_KEY;
4
5
  const GROQ_MODEL = "llama-3.3-70b-versatile";
@@ -92,6 +93,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
92
93
  const parsed = JSON.parse(content);
93
94
  return res.status(200).json(parsed);
94
95
  } catch (err) {
96
+ captureError(err, { route: "classify" });
95
97
  return res.status(500).json({ error: "Classification failed", detail: String(err) });
96
98
  }
97
99
  }
package/api/feed.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { VercelRequest, VercelResponse } from "@vercel/node";
2
2
  import { ethers } from "ethers";
3
+ import { captureError } from "./lib/sentry.js";
3
4
 
4
5
  const RPC_URL = process.env.BASE_RPC_URL || "https://base-mainnet.public.blastapi.io";
5
6
  const CONTRACT = process.env.MINT_FACTORY_ADDRESS || "0x12a1c11a0b2860f64e7d8df20989f97d40de7f2c";
@@ -66,6 +67,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
66
67
  tokens: tokens.reverse(),
67
68
  });
68
69
  } catch (err: any) {
70
+ captureError(err, { route: "feed" });
69
71
  res.status(500).json({ error: err.message || "Failed to fetch feed" });
70
72
  }
71
73
  }
@@ -0,0 +1,28 @@
1
+ // Lightweight Sentry error reporting for Vercel serverless functions
2
+ // No framework SDK needed — just the Node SDK
3
+
4
+ import * as Sentry from "@sentry/node";
5
+
6
+ const SENTRY_DSN = process.env.SENTRY_DSN;
7
+
8
+ if (SENTRY_DSN) {
9
+ Sentry.init({
10
+ dsn: SENTRY_DSN,
11
+ environment: process.env.VERCEL_ENV || "development",
12
+ release: process.env.VERCEL_GIT_COMMIT_SHA,
13
+ tracesSampleRate: 1.0,
14
+ });
15
+ }
16
+
17
+ export { Sentry };
18
+
19
+ export function captureError(err: unknown, context?: Record<string, string>) {
20
+ if (!SENTRY_DSN) {
21
+ console.error("[sentry-disabled]", err);
22
+ return;
23
+ }
24
+ if (context) {
25
+ Sentry.setContext("mint.day", context);
26
+ }
27
+ Sentry.captureException(err);
28
+ }
@@ -0,0 +1,91 @@
1
+ import type { VercelRequest, VercelResponse } from "@vercel/node";
2
+ import { createHmac } from "crypto";
3
+
4
+ // Sentry sends webhooks when new issues are created.
5
+ // This endpoint validates the signature and dispatches a
6
+ // repository_dispatch event to GitHub so the Claude Code Action
7
+ // can triage + fix.
8
+
9
+ const SENTRY_CLIENT_SECRET = process.env.SENTRY_CLIENT_SECRET;
10
+ const GITHUB_TOKEN = process.env.GH_DISPATCH_TOKEN;
11
+ const GITHUB_REPO = "jordanlyall/mintday";
12
+
13
+ function verifySentrySignature(
14
+ body: string,
15
+ signature: string | undefined,
16
+ secret: string
17
+ ): boolean {
18
+ if (!signature) return false;
19
+ const hmac = createHmac("sha256", secret);
20
+ hmac.update(body, "utf8");
21
+ const digest = hmac.digest("hex");
22
+ return signature === digest;
23
+ }
24
+
25
+ export default async function handler(req: VercelRequest, res: VercelResponse) {
26
+ if (req.method !== "POST") {
27
+ return res.status(405).json({ error: "POST only" });
28
+ }
29
+
30
+ if (!SENTRY_CLIENT_SECRET || !GITHUB_TOKEN) {
31
+ return res.status(500).json({ error: "Missing env vars" });
32
+ }
33
+
34
+ // Verify Sentry webhook signature
35
+ const rawBody = JSON.stringify(req.body);
36
+ const signature = req.headers["sentry-hook-signature"] as string | undefined;
37
+
38
+ if (!verifySentrySignature(rawBody, signature, SENTRY_CLIENT_SECRET)) {
39
+ return res.status(401).json({ error: "Invalid signature" });
40
+ }
41
+
42
+ const resource = req.headers["sentry-hook-resource"] as string;
43
+
44
+ // Only act on new issues (not updates/resolves)
45
+ if (resource !== "issue" || req.body?.action !== "created") {
46
+ return res.status(200).json({ status: "ignored", resource, action: req.body?.action });
47
+ }
48
+
49
+ const issue = req.body.data?.issue;
50
+ if (!issue) {
51
+ return res.status(200).json({ status: "no issue data" });
52
+ }
53
+
54
+ // Extract useful context for the agent
55
+ const payload = {
56
+ title: issue.title,
57
+ culprit: issue.culprit,
58
+ level: issue.level,
59
+ first_seen: issue.firstSeen,
60
+ count: issue.count,
61
+ url: issue.permalink,
62
+ short_id: issue.shortId,
63
+ metadata: issue.metadata,
64
+ tags: issue.tags?.map((t: { key: string; value: string }) => `${t.key}:${t.value}`),
65
+ };
66
+
67
+ // Dispatch to GitHub Actions
68
+ const ghRes = await fetch(
69
+ `https://api.github.com/repos/${GITHUB_REPO}/dispatches`,
70
+ {
71
+ method: "POST",
72
+ headers: {
73
+ Authorization: `Bearer ${GITHUB_TOKEN}`,
74
+ Accept: "application/vnd.github+json",
75
+ "X-GitHub-Api-Version": "2022-11-28",
76
+ },
77
+ body: JSON.stringify({
78
+ event_type: "sentry_issue",
79
+ client_payload: payload,
80
+ }),
81
+ }
82
+ );
83
+
84
+ if (!ghRes.ok) {
85
+ const err = await ghRes.text();
86
+ console.error("GitHub dispatch failed:", err);
87
+ return res.status(502).json({ error: "GitHub dispatch failed" });
88
+ }
89
+
90
+ return res.status(200).json({ status: "dispatched", issue: payload.short_id });
91
+ }
package/api/sponsor.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { VercelRequest, VercelResponse } from "@vercel/node";
2
2
  import { ethers } from "ethers";
3
+ import { captureError } from "./lib/sentry.js";
3
4
 
4
5
  const PRIVATE_KEY = process.env.SPONSORED_KEY;
5
6
  const RPC_URL = process.env.BASE_RPC_URL || "https://mainnet.base.org";
@@ -100,6 +101,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
100
101
  explorer: `https://basescan.org/tx/${receipt!.hash}`,
101
102
  });
102
103
  } catch (err) {
104
+ captureError(err, { route: "sponsor", recipient: recipient || "unknown" });
103
105
  const message = err instanceof Error ? err.message : String(err);
104
106
  return res.status(500).json({ error: `Transaction failed: ${message}` });
105
107
  }
package/api/verify.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { VercelRequest, VercelResponse } from "@vercel/node";
2
2
  import { ethers } from "ethers";
3
+ import { captureError } from "./lib/sentry.js";
3
4
 
4
5
  const RPC_URL = process.env.BASE_RPC_URL || "https://base-mainnet.public.blastapi.io";
5
6
  const CONTRACT = process.env.MINT_FACTORY_ADDRESS || "0x12a1c11a0b2860f64e7d8df20989f97d40de7f2c";
@@ -88,6 +89,7 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
88
89
  tokens,
89
90
  });
90
91
  } catch (err: any) {
92
+ captureError(err, { route: "verify", address });
91
93
  res.status(500).json({ error: err.message || "Failed to verify" });
92
94
  }
93
95
  }
package/dist/index.js CHANGED
@@ -12,11 +12,11 @@ import { mintCheckSchema, handleMintCheck } from "./tools/mint-check.js";
12
12
  import { mintResolveSchema, handleMintResolve } from "./tools/mint-resolve.js";
13
13
  dotenv.config();
14
14
  // Testnet try-it wallet (Base Sepolia, pre-funded)
15
- const TRYIT_KEY = "REDACTED_RETIRED_KEY";
15
+ const TRYIT_KEY = "";
16
16
  const TRYIT_CONTRACT = "0xa52450397f312c256Bd68B202C0CF90387Ea0E67";
17
17
  const TRYIT_RPC = "https://sepolia.base.org";
18
18
  const TRYIT_CHAIN = 84532;
19
- const MAINNET_CONTRACT = "0xbf12d372444dcf69df9316d961439f6b5919e8d0";
19
+ const MAINNET_CONTRACT = "0x12a1c11a0b2860f64e7d8df20989f97d40de7f2c";
20
20
  const MAINNET_RPC = "https://mainnet.base.org";
21
21
  const MAINNET_CHAIN = 8453;
22
22
  // Load private key: env var > ~/.mint-day/credentials file
@@ -52,9 +52,9 @@ const defaultRecipient = process.env.DEFAULT_RECIPIENT || signerAddress || "";
52
52
  const calldataService = new CalldataService(rpcUrl, contractAddress, chainId);
53
53
  const server = new McpServer({
54
54
  name: "mint-day",
55
- version: "0.3.3",
55
+ version: "0.4.0",
56
56
  });
57
- 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, {
57
+ 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). Supports two protocols: 'mintday' (default, gas-sponsored) and 'superrare' (Rare Protocol, requires PRIVATE_KEY). Set platform: 'superrare' to mint on Rare Protocol contracts. The token is immutable and publicly verifiable. Provide an image URL, local file path, or base64 for visual tokens. Returns a preview with mintId first; call again with mintId to confirm.", mintSchema, async (params) => handleMint(params, calldataService, defaultRecipient, userKey, {
58
58
  tryitKey: TRYIT_KEY,
59
59
  tryitContract: TRYIT_CONTRACT,
60
60
  tryitRpc: TRYIT_RPC,
@@ -0,0 +1,19 @@
1
+ import { MintIntent } from "../types.js";
2
+ export interface RareMintResult {
3
+ status: string;
4
+ txHash: string;
5
+ tokenId: string;
6
+ contract: string;
7
+ platform: string;
8
+ tokenType: string;
9
+ soulbound: boolean;
10
+ recipient: string;
11
+ chain: string;
12
+ explorer: string;
13
+ tokenUri: string;
14
+ }
15
+ export declare function mintOnRareProtocol(intent: MintIntent, privateKey: string, isTestnet?: boolean): Promise<RareMintResult>;
16
+ export declare function deployRareCollection(privateKey: string, name: string, symbol: string, isTestnet?: boolean): Promise<{
17
+ contract: string;
18
+ txHash: string;
19
+ }>;
@@ -0,0 +1,77 @@
1
+ import { createPublicClient, createWalletClient, http } from "viem";
2
+ import { privateKeyToAccount } from "viem/accounts";
3
+ import { base, baseSepolia } from "viem/chains";
4
+ import { createRareClient } from "@rareprotocol/rare-cli/client";
5
+ import { encodeMetadata } from "./metadata.js";
6
+ import { TOKEN_TYPE_NAMES } from "../types.js";
7
+ // Default mint.day collection on Rare Protocol (Base mainnet)
8
+ // Set via RARE_CONTRACT env var, or deploy one with scripts/deploy-rare-collection.ts
9
+ const DEFAULT_RARE_CONTRACT = process.env.RARE_CONTRACT || "";
10
+ export async function mintOnRareProtocol(intent, privateKey, isTestnet = false) {
11
+ const chain = isTestnet ? baseSepolia : base;
12
+ const contractAddress = DEFAULT_RARE_CONTRACT;
13
+ if (!contractAddress) {
14
+ throw new Error("RARE_CONTRACT not set. Deploy a collection first: node scripts/deploy-rare-collection.mjs");
15
+ }
16
+ const account = privateKeyToAccount(privateKey);
17
+ const publicClient = createPublicClient({
18
+ chain,
19
+ transport: http(),
20
+ });
21
+ const walletClient = createWalletClient({
22
+ account,
23
+ chain,
24
+ transport: http(),
25
+ });
26
+ // Cast clients to satisfy rare-cli's bundled viem types (runtime-compatible)
27
+ const rare = createRareClient({
28
+ publicClient: publicClient,
29
+ walletClient: walletClient,
30
+ account: account.address,
31
+ });
32
+ // Build metadata URI (same format as MintFactory path)
33
+ const tokenUri = encodeMetadata(intent.metadata);
34
+ // Mint via Rare Protocol
35
+ const result = await rare.mint.mintTo({
36
+ contract: contractAddress,
37
+ tokenUri,
38
+ to: intent.recipient,
39
+ });
40
+ const explorerBase = isTestnet ? "sepolia.basescan.org" : "basescan.org";
41
+ return {
42
+ status: "minted",
43
+ txHash: result.txHash,
44
+ tokenId: result.tokenId?.toString() || "unknown",
45
+ contract: contractAddress,
46
+ platform: "superrare",
47
+ tokenType: TOKEN_TYPE_NAMES[intent.tokenType],
48
+ soulbound: intent.soulbound,
49
+ recipient: intent.recipient,
50
+ chain: isTestnet ? "Base Sepolia (testnet)" : "Base",
51
+ explorer: `https://${explorerBase}/tx/${result.txHash}`,
52
+ tokenUri,
53
+ };
54
+ }
55
+ export async function deployRareCollection(privateKey, name, symbol, isTestnet = false) {
56
+ const chain = isTestnet ? baseSepolia : base;
57
+ const account = privateKeyToAccount(privateKey);
58
+ const publicClient = createPublicClient({
59
+ chain,
60
+ transport: http(),
61
+ });
62
+ const walletClient = createWalletClient({
63
+ account,
64
+ chain,
65
+ transport: http(),
66
+ });
67
+ const rare = createRareClient({
68
+ publicClient: publicClient,
69
+ walletClient: walletClient,
70
+ account: account.address,
71
+ });
72
+ const result = await rare.deploy.erc721({ name, symbol });
73
+ return {
74
+ contract: result.contract || "unknown",
75
+ txHash: result.txHash,
76
+ };
77
+ }
@@ -15,6 +15,10 @@ export declare const mintSchema: {
15
15
  animation_url: z.ZodOptional<z.ZodString>;
16
16
  mintId: z.ZodOptional<z.ZodString>;
17
17
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
18
+ platform: z.ZodOptional<z.ZodEnum<{
19
+ superrare: "superrare";
20
+ mintday: "mintday";
21
+ }>>;
18
22
  };
19
23
  interface TryitConfig {
20
24
  tryitKey: string;
@@ -31,6 +35,7 @@ export declare function handleMint(params: {
31
35
  animation_url?: string;
32
36
  mintId?: string;
33
37
  metadata?: Record<string, unknown>;
38
+ platform?: string;
34
39
  }, calldataService: CalldataService, defaultRecipient: string, userKey?: string, tryit?: TryitConfig): Promise<{
35
40
  content: {
36
41
  type: "text";
@@ -5,18 +5,19 @@ import { TokenType, TOKEN_TYPE_NAMES } from "../types.js";
5
5
  import { classifyIntent } from "../services/classifier.js";
6
6
  import { CalldataService } from "../services/calldata.js";
7
7
  import { resolveImage } from "../services/image-upload.js";
8
- // Intent cache: preview mintId -> frozen intent
8
+ import { mintOnRareProtocol } from "../services/rare-protocol.js";
9
+ // Intent cache: preview mintId -> frozen intent + platform
9
10
  const intentCache = new Map();
10
11
  const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
11
- function cacheMintId(intent) {
12
+ function cacheMintId(intent, platform = "mintday") {
12
13
  const hash = createHash("sha256")
13
14
  .update(JSON.stringify(intent))
14
15
  .digest("hex")
15
16
  .slice(0, 12);
16
- intentCache.set(hash, { intent, expiresAt: Date.now() + CACHE_TTL_MS });
17
+ intentCache.set(hash, { intent, platform, expiresAt: Date.now() + CACHE_TTL_MS });
17
18
  return hash;
18
19
  }
19
- function getCachedIntent(mintId) {
20
+ function getCachedEntry(mintId) {
20
21
  const entry = intentCache.get(mintId);
21
22
  if (!entry)
22
23
  return null;
@@ -24,7 +25,7 @@ function getCachedIntent(mintId) {
24
25
  intentCache.delete(mintId);
25
26
  return null;
26
27
  }
27
- return entry.intent;
28
+ return { intent: entry.intent, platform: entry.platform };
28
29
  }
29
30
  export const mintSchema = {
30
31
  description: z
@@ -59,8 +60,12 @@ export const mintSchema = {
59
60
  .record(z.string(), z.unknown())
60
61
  .optional()
61
62
  .describe("Additional key-value metadata. Supports strings, arrays, and objects (e.g. capabilities: ['mint'])."),
63
+ platform: z
64
+ .enum(["mintday", "superrare"])
65
+ .optional()
66
+ .describe("Which protocol to mint on. 'mintday' uses the MintFactory contract (default). 'superrare' mints via Rare Protocol on Base."),
62
67
  };
63
- const SPONSOR_URL = process.env.SPONSOR_URL || "https://agent-mint-nine.vercel.app/api/sponsor";
68
+ const SPONSOR_URL = process.env.SPONSOR_URL || "https://www.mint.day/api/sponsor";
64
69
  async function sponsoredMint(result, recipient) {
65
70
  try {
66
71
  const response = await fetch(SPONSOR_URL, {
@@ -92,22 +97,17 @@ async function sponsoredMint(result, recipient) {
92
97
  sponsored: true,
93
98
  };
94
99
  }
95
- catch {
96
- return null;
100
+ catch (err) {
101
+ const message = err instanceof Error ? err.message : String(err);
102
+ return { error: `Sponsor request failed: ${message}` };
97
103
  }
98
104
  }
99
105
  export async function handleMint(params, calldataService, defaultRecipient, userKey = "", tryit = { tryitKey: "", tryitContract: "", tryitRpc: "", tryitChain: 84532 }) {
100
- // Determine mode:
101
- // 1. User has private key: mainnet, sign + submit locally
102
- // 2. No key but recipient provided: mainnet, sponsored gas via server
103
- // 3. No key, no recipient: testnet try-it mode
104
- const hasRecipient = !!params.recipient;
105
- const isTestnet = !userKey && !hasRecipient;
106
- const privateKey = userKey || (isTestnet ? tryit.tryitKey : "");
107
106
  // Confirmation path: replay cached intent
107
+ // Check cache first so we can use cached recipient for mode detection
108
108
  if (params.mintId) {
109
- const cached = getCachedIntent(params.mintId);
110
- if (!cached) {
109
+ const entry = getCachedEntry(params.mintId);
110
+ if (!entry) {
111
111
  return {
112
112
  content: [{
113
113
  type: "text",
@@ -115,7 +115,42 @@ export async function handleMint(params, calldataService, defaultRecipient, user
115
115
  }],
116
116
  };
117
117
  }
118
+ const cached = entry.intent;
119
+ const cachedPlatform = entry.platform;
118
120
  intentCache.delete(params.mintId);
121
+ // Mode detection using cached recipient
122
+ const hasRecipient = !!cached.recipient;
123
+ const isTestnet = !userKey && !hasRecipient;
124
+ const privateKey = userKey || (isTestnet ? tryit.tryitKey : "");
125
+ // Rare Protocol path: mint via SuperRare contracts on Base
126
+ if (cachedPlatform === "superrare") {
127
+ if (!privateKey) {
128
+ return {
129
+ content: [{
130
+ type: "text",
131
+ text: JSON.stringify({ error: "SuperRare minting requires a private key. Save one to ~/.mint-day/credentials." }, null, 2),
132
+ }],
133
+ };
134
+ }
135
+ try {
136
+ const rareResult = await mintOnRareProtocol(cached, privateKey, isTestnet);
137
+ return {
138
+ content: [{
139
+ type: "text",
140
+ text: JSON.stringify(rareResult, null, 2),
141
+ }],
142
+ };
143
+ }
144
+ catch (err) {
145
+ const message = err instanceof Error ? err.message : String(err);
146
+ return {
147
+ content: [{
148
+ type: "text",
149
+ text: JSON.stringify({ error: `SuperRare mint failed: ${message}` }, null, 2),
150
+ }],
151
+ };
152
+ }
153
+ }
119
154
  // Use testnet calldataService if in try-it mode
120
155
  let activeCalldataService = calldataService;
121
156
  if (isTestnet) {
@@ -221,6 +256,8 @@ export async function handleMint(params, calldataService, defaultRecipient, user
221
256
  };
222
257
  }
223
258
  const recipient = params.recipient || defaultRecipient;
259
+ const hasRecipient = !!recipient;
260
+ const isTestnet = !userKey && !hasRecipient;
224
261
  if (!recipient) {
225
262
  return {
226
263
  content: [{
@@ -326,11 +363,13 @@ export async function handleMint(params, calldataService, defaultRecipient, user
326
363
  }
327
364
  }
328
365
  // Cache intent and return preview
329
- const mintId = cacheMintId(intent);
366
+ const selectedPlatform = params.platform || "mintday";
367
+ const mintId = cacheMintId(intent, selectedPlatform);
330
368
  const preview = {
331
369
  status: "preview",
332
370
  mintId,
333
- message: `I'll mint a ${TOKEN_TYPE_NAMES[intent.tokenType]} token${intent.soulbound ? " (soulbound)" : ""} to ${intent.recipient}.`,
371
+ platform: selectedPlatform,
372
+ message: `I'll mint a ${TOKEN_TYPE_NAMES[intent.tokenType]} token${intent.soulbound ? " (soulbound)" : ""} to ${intent.recipient} via ${selectedPlatform === "superrare" ? "Rare Protocol (SuperRare)" : "mint.day"}.`,
334
373
  tokenType: TOKEN_TYPE_NAMES[intent.tokenType],
335
374
  soulbound: intent.soulbound,
336
375
  recipient: intent.recipient,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mint-day",
3
- "version": "0.3.3",
3
+ "version": "0.4.2",
4
4
  "description": "Agent-native minting on Base. One tool call, any token type.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -24,8 +24,11 @@
24
24
  "type": "module",
25
25
  "dependencies": {
26
26
  "@modelcontextprotocol/sdk": "^1.27.1",
27
+ "@rareprotocol/rare-cli": "^0.3.0",
28
+ "@sentry/node": "^10.45.0",
27
29
  "dotenv": "^17.3.1",
28
30
  "ethers": "^6.16.0",
31
+ "viem": "^2.47.6",
29
32
  "zod": "^4.3.6"
30
33
  },
31
34
  "devDependencies": {
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Deploy a mint.day collection on Rare Protocol (SuperRare).
4
+ *
5
+ * Usage:
6
+ * node scripts/deploy-rare-collection.mjs [--testnet]
7
+ *
8
+ * Requires: PRIVATE_KEY env var or ~/.mint-day/credentials
9
+ * Output: prints the contract address to set as RARE_CONTRACT
10
+ */
11
+
12
+ import { createPublicClient, createWalletClient, http } from "viem";
13
+ import { privateKeyToAccount } from "viem/accounts";
14
+ import { base, baseSepolia } from "viem/chains";
15
+ import { createRareClient } from "@rareprotocol/rare-cli/client";
16
+ import { readFileSync, existsSync } from "fs";
17
+ import { join } from "path";
18
+ import { homedir } from "os";
19
+
20
+ const isTestnet = process.argv.includes("--testnet");
21
+ const chain = isTestnet ? baseSepolia : base;
22
+
23
+ // Load private key
24
+ let privateKey = process.env.PRIVATE_KEY || "";
25
+ if (!privateKey) {
26
+ const credPath = join(homedir(), ".mint-day", "credentials");
27
+ if (existsSync(credPath)) {
28
+ privateKey = readFileSync(credPath, "utf-8").trim();
29
+ }
30
+ }
31
+
32
+ if (!privateKey) {
33
+ console.error("No private key found. Set PRIVATE_KEY env var or save to ~/.mint-day/credentials");
34
+ process.exit(1);
35
+ }
36
+
37
+ const account = privateKeyToAccount(privateKey);
38
+ console.log(`Deploying mint.day collection on ${isTestnet ? "Base Sepolia" : "Base"}...`);
39
+ console.log(`Deployer: ${account.address}`);
40
+
41
+ const publicClient = createPublicClient({ chain, transport: http() });
42
+ const walletClient = createWalletClient({ account, chain, transport: http() });
43
+
44
+ const rare = createRareClient({
45
+ publicClient,
46
+ walletClient,
47
+ account: account.address,
48
+ });
49
+
50
+ try {
51
+ const result = await rare.deploy.erc721({
52
+ name: "mint.day",
53
+ symbol: "MINTDAY",
54
+ });
55
+
56
+ console.log(`\nCollection deployed!`);
57
+ console.log(`Contract: ${result.contract}`);
58
+ console.log(`TX: https://${isTestnet ? "sepolia." : ""}basescan.org/tx/${result.txHash}`);
59
+ console.log(`\nSet this in your environment:`);
60
+ console.log(` export RARE_CONTRACT=${result.contract}`);
61
+ } catch (err) {
62
+ console.error("Deploy failed:", err.message || err);
63
+ process.exit(1);
64
+ }