mint-day 0.3.3 → 0.4.1

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, {
@@ -106,8 +111,8 @@ export async function handleMint(params, calldataService, defaultRecipient, user
106
111
  const privateKey = userKey || (isTestnet ? tryit.tryitKey : "");
107
112
  // Confirmation path: replay cached intent
108
113
  if (params.mintId) {
109
- const cached = getCachedIntent(params.mintId);
110
- if (!cached) {
114
+ const entry = getCachedEntry(params.mintId);
115
+ if (!entry) {
111
116
  return {
112
117
  content: [{
113
118
  type: "text",
@@ -115,7 +120,38 @@ export async function handleMint(params, calldataService, defaultRecipient, user
115
120
  }],
116
121
  };
117
122
  }
123
+ const cached = entry.intent;
124
+ const cachedPlatform = entry.platform;
118
125
  intentCache.delete(params.mintId);
126
+ // Rare Protocol path: mint via SuperRare contracts on Base
127
+ if (cachedPlatform === "superrare") {
128
+ if (!privateKey) {
129
+ return {
130
+ content: [{
131
+ type: "text",
132
+ text: JSON.stringify({ error: "SuperRare minting requires a private key. Save one to ~/.mint-day/credentials." }, null, 2),
133
+ }],
134
+ };
135
+ }
136
+ try {
137
+ const rareResult = await mintOnRareProtocol(cached, privateKey, isTestnet);
138
+ return {
139
+ content: [{
140
+ type: "text",
141
+ text: JSON.stringify(rareResult, null, 2),
142
+ }],
143
+ };
144
+ }
145
+ catch (err) {
146
+ const message = err instanceof Error ? err.message : String(err);
147
+ return {
148
+ content: [{
149
+ type: "text",
150
+ text: JSON.stringify({ error: `SuperRare mint failed: ${message}` }, null, 2),
151
+ }],
152
+ };
153
+ }
154
+ }
119
155
  // Use testnet calldataService if in try-it mode
120
156
  let activeCalldataService = calldataService;
121
157
  if (isTestnet) {
@@ -326,11 +362,13 @@ export async function handleMint(params, calldataService, defaultRecipient, user
326
362
  }
327
363
  }
328
364
  // Cache intent and return preview
329
- const mintId = cacheMintId(intent);
365
+ const selectedPlatform = params.platform || "mintday";
366
+ const mintId = cacheMintId(intent, selectedPlatform);
330
367
  const preview = {
331
368
  status: "preview",
332
369
  mintId,
333
- message: `I'll mint a ${TOKEN_TYPE_NAMES[intent.tokenType]} token${intent.soulbound ? " (soulbound)" : ""} to ${intent.recipient}.`,
370
+ platform: selectedPlatform,
371
+ 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
372
  tokenType: TOKEN_TYPE_NAMES[intent.tokenType],
335
373
  soulbound: intent.soulbound,
336
374
  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.1",
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
+ }