mint-day 0.4.2 → 0.4.4

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/dist/index.js CHANGED
@@ -11,11 +11,6 @@ import { mintSchema, handleMint } from "./tools/mint.js";
11
11
  import { mintCheckSchema, handleMintCheck } from "./tools/mint-check.js";
12
12
  import { mintResolveSchema, handleMintResolve } from "./tools/mint-resolve.js";
13
13
  dotenv.config();
14
- // Testnet try-it wallet (Base Sepolia, pre-funded)
15
- const TRYIT_KEY = "";
16
- const TRYIT_CONTRACT = "0xa52450397f312c256Bd68B202C0CF90387Ea0E67";
17
- const TRYIT_RPC = "https://sepolia.base.org";
18
- const TRYIT_CHAIN = 84532;
19
14
  const MAINNET_CONTRACT = "0x12a1c11a0b2860f64e7d8df20989f97d40de7f2c";
20
15
  const MAINNET_RPC = "https://mainnet.base.org";
21
16
  const MAINNET_CHAIN = 8453;
@@ -33,7 +28,7 @@ function loadPrivateKey() {
33
28
  }
34
29
  const userKey = loadPrivateKey();
35
30
  const hasUserKey = !!userKey;
36
- // Mainnet by default. Testnet only when no private key AND no recipient provided (decided per-call).
31
+ // Mainnet configuration
37
32
  const chainId = parseInt(process.env.CHAIN_ID || String(hasUserKey ? MAINNET_CHAIN : MAINNET_CHAIN));
38
33
  const contractAddress = process.env.MINT_FACTORY_ADDRESS || MAINNET_CONTRACT;
39
34
  const rpcUrl = process.env.BASE_RPC_URL || MAINNET_RPC;
@@ -54,12 +49,7 @@ const server = new McpServer({
54
49
  name: "mint-day",
55
50
  version: "0.4.0",
56
51
  });
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
- tryitKey: TRYIT_KEY,
59
- tryitContract: TRYIT_CONTRACT,
60
- tryitRpc: TRYIT_RPC,
61
- tryitChain: TRYIT_CHAIN,
62
- }));
52
+ 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));
63
53
  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));
64
54
  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));
65
55
  async function main() {
@@ -15,17 +15,12 @@ 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
+ selfSign: z.ZodOptional<z.ZodBoolean>;
18
19
  platform: z.ZodOptional<z.ZodEnum<{
19
20
  superrare: "superrare";
20
21
  mintday: "mintday";
21
22
  }>>;
22
23
  };
23
- interface TryitConfig {
24
- tryitKey: string;
25
- tryitContract: string;
26
- tryitRpc: string;
27
- tryitChain: number;
28
- }
29
24
  export declare function handleMint(params: {
30
25
  description?: string;
31
26
  tokenType?: string;
@@ -35,11 +30,11 @@ export declare function handleMint(params: {
35
30
  animation_url?: string;
36
31
  mintId?: string;
37
32
  metadata?: Record<string, unknown>;
33
+ selfSign?: boolean;
38
34
  platform?: string;
39
- }, calldataService: CalldataService, defaultRecipient: string, userKey?: string, tryit?: TryitConfig): Promise<{
35
+ }, calldataService: CalldataService, defaultRecipient: string, userKey?: string): Promise<{
40
36
  content: {
41
37
  type: "text";
42
38
  text: string;
43
39
  }[];
44
40
  }>;
45
- export {};
@@ -3,18 +3,17 @@ import { z } from "zod";
3
3
  import { ethers } from "ethers";
4
4
  import { TokenType, TOKEN_TYPE_NAMES } from "../types.js";
5
5
  import { classifyIntent } from "../services/classifier.js";
6
- import { CalldataService } from "../services/calldata.js";
7
6
  import { resolveImage } from "../services/image-upload.js";
8
7
  import { mintOnRareProtocol } from "../services/rare-protocol.js";
9
- // Intent cache: preview mintId -> frozen intent + platform
8
+ // Intent cache: preview mintId -> frozen intent + platform + options
10
9
  const intentCache = new Map();
11
10
  const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
12
- function cacheMintId(intent, platform = "mintday") {
11
+ function cacheMintId(intent, platform = "mintday", selfSign = false) {
13
12
  const hash = createHash("sha256")
14
13
  .update(JSON.stringify(intent))
15
14
  .digest("hex")
16
15
  .slice(0, 12);
17
- intentCache.set(hash, { intent, platform, expiresAt: Date.now() + CACHE_TTL_MS });
16
+ intentCache.set(hash, { intent, platform, selfSign, expiresAt: Date.now() + CACHE_TTL_MS });
18
17
  return hash;
19
18
  }
20
19
  function getCachedEntry(mintId) {
@@ -25,7 +24,7 @@ function getCachedEntry(mintId) {
25
24
  intentCache.delete(mintId);
26
25
  return null;
27
26
  }
28
- return { intent: entry.intent, platform: entry.platform };
27
+ return { intent: entry.intent, platform: entry.platform, selfSign: entry.selfSign };
29
28
  }
30
29
  export const mintSchema = {
31
30
  description: z
@@ -60,6 +59,10 @@ export const mintSchema = {
60
59
  .record(z.string(), z.unknown())
61
60
  .optional()
62
61
  .describe("Additional key-value metadata. Supports strings, arrays, and objects (e.g. capabilities: ['mint'])."),
62
+ selfSign: z
63
+ .boolean()
64
+ .optional()
65
+ .describe("Sign and submit from your own wallet instead of using sponsored gas. Requires PRIVATE_KEY env var or ~/.mint-day/credentials."),
63
66
  platform: z
64
67
  .enum(["mintday", "superrare"])
65
68
  .optional()
@@ -102,7 +105,7 @@ async function sponsoredMint(result, recipient) {
102
105
  return { error: `Sponsor request failed: ${message}` };
103
106
  }
104
107
  }
105
- export async function handleMint(params, calldataService, defaultRecipient, userKey = "", tryit = { tryitKey: "", tryitContract: "", tryitRpc: "", tryitChain: 84532 }) {
108
+ export async function handleMint(params, calldataService, defaultRecipient, userKey = "") {
106
109
  // Confirmation path: replay cached intent
107
110
  // Check cache first so we can use cached recipient for mode detection
108
111
  if (params.mintId) {
@@ -118,10 +121,9 @@ export async function handleMint(params, calldataService, defaultRecipient, user
118
121
  const cached = entry.intent;
119
122
  const cachedPlatform = entry.platform;
120
123
  intentCache.delete(params.mintId);
121
- // Mode detection using cached recipient
122
124
  const hasRecipient = !!cached.recipient;
123
- const isTestnet = !userKey && !hasRecipient;
124
- const privateKey = userKey || (isTestnet ? tryit.tryitKey : "");
125
+ const selfSign = !!entry.selfSign;
126
+ const privateKey = userKey;
125
127
  // Rare Protocol path: mint via SuperRare contracts on Base
126
128
  if (cachedPlatform === "superrare") {
127
129
  if (!privateKey) {
@@ -133,7 +135,7 @@ export async function handleMint(params, calldataService, defaultRecipient, user
133
135
  };
134
136
  }
135
137
  try {
136
- const rareResult = await mintOnRareProtocol(cached, privateKey, isTestnet);
138
+ const rareResult = await mintOnRareProtocol(cached, privateKey, false);
137
139
  return {
138
140
  content: [{
139
141
  type: "text",
@@ -151,17 +153,11 @@ export async function handleMint(params, calldataService, defaultRecipient, user
151
153
  };
152
154
  }
153
155
  }
154
- // Use testnet calldataService if in try-it mode
155
- let activeCalldataService = calldataService;
156
- if (isTestnet) {
157
- activeCalldataService = new CalldataService(tryit.tryitRpc, tryit.tryitContract, tryit.tryitChain);
158
- }
159
- const result = await activeCalldataService.buildMintCalldata(cached);
160
- // If we have a private key, sign and submit directly
161
- if (privateKey) {
156
+ const result = await calldataService.buildMintCalldata(cached);
157
+ // Self-sign: user explicitly opted in and has a private key
158
+ if (selfSign && privateKey) {
162
159
  try {
163
- const provider = isTestnet ? activeCalldataService.provider : calldataService.provider;
164
- const wallet = new ethers.Wallet(privateKey, provider);
160
+ const wallet = new ethers.Wallet(privateKey, calldataService.provider);
165
161
  const tx = await wallet.sendTransaction({
166
162
  to: result.to,
167
163
  data: result.calldata,
@@ -169,24 +165,20 @@ export async function handleMint(params, calldataService, defaultRecipient, user
169
165
  gasLimit: result.estimatedGas,
170
166
  });
171
167
  const receipt = await tx.wait();
172
- const response = {
173
- status: "minted",
174
- txHash: receipt.hash,
175
- blockNumber: receipt.blockNumber,
176
- gasUsed: receipt.gasUsed.toString(),
177
- tokenType: result.tokenType,
178
- soulbound: result.soulbound,
179
- recipient: cached.recipient,
180
- chain: isTestnet ? "Base Sepolia (testnet)" : "Base",
181
- explorer: `https://${result.chainId === 8453 ? "" : "sepolia."}basescan.org/tx/${receipt.hash}`,
182
- };
183
- if (isTestnet) {
184
- response.note = "This token was minted on Base Sepolia (testnet). To mint on Base mainnet, save a private key to ~/.mint-day/credentials and restart the MCP server.";
185
- }
186
168
  return {
187
169
  content: [{
188
170
  type: "text",
189
- text: JSON.stringify(response, null, 2),
171
+ text: JSON.stringify({
172
+ status: "minted",
173
+ txHash: receipt.hash,
174
+ blockNumber: receipt.blockNumber,
175
+ gasUsed: receipt.gasUsed.toString(),
176
+ tokenType: result.tokenType,
177
+ soulbound: result.soulbound,
178
+ recipient: cached.recipient,
179
+ chain: "Base",
180
+ explorer: `https://basescan.org/tx/${receipt.hash}`,
181
+ }, null, 2),
190
182
  }],
191
183
  };
192
184
  }
@@ -200,7 +192,15 @@ export async function handleMint(params, calldataService, defaultRecipient, user
200
192
  };
201
193
  }
202
194
  }
203
- // No user key but has recipient: try sponsored gas
195
+ if (selfSign && !privateKey) {
196
+ return {
197
+ content: [{
198
+ type: "text",
199
+ text: JSON.stringify({ error: "selfSign requires a private key. Set PRIVATE_KEY env var or save to ~/.mint-day/credentials." }, null, 2),
200
+ }],
201
+ };
202
+ }
203
+ // Default: sponsored gas
204
204
  if (hasRecipient) {
205
205
  const sponsored = await sponsoredMint(result, cached.recipient);
206
206
  if (sponsored && !sponsored.error) {
@@ -256,8 +256,6 @@ export async function handleMint(params, calldataService, defaultRecipient, user
256
256
  };
257
257
  }
258
258
  const recipient = params.recipient || defaultRecipient;
259
- const hasRecipient = !!recipient;
260
- const isTestnet = !userKey && !hasRecipient;
261
259
  if (!recipient) {
262
260
  return {
263
261
  content: [{
@@ -364,7 +362,7 @@ export async function handleMint(params, calldataService, defaultRecipient, user
364
362
  }
365
363
  // Cache intent and return preview
366
364
  const selectedPlatform = params.platform || "mintday";
367
- const mintId = cacheMintId(intent, selectedPlatform);
365
+ const mintId = cacheMintId(intent, selectedPlatform, !!params.selfSign);
368
366
  const preview = {
369
367
  status: "preview",
370
368
  mintId,
@@ -386,12 +384,7 @@ export async function handleMint(params, calldataService, defaultRecipient, user
386
384
  preview.missingFields = intent.missingFields;
387
385
  preview.hint = "Identity tokens (ERC-8004) should include capabilities[], endpoints[], and did. Provide these in metadata for full compliance.";
388
386
  }
389
- if (isTestnet) {
390
- preview.chain = "Base Sepolia (testnet)";
391
- }
392
- else {
393
- preview.chain = "Base";
394
- }
387
+ preview.chain = "Base";
395
388
  preview.instruction = "Call mint with mintId to confirm.";
396
389
  return {
397
390
  content: [{ type: "text", text: JSON.stringify(preview, null, 2) }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mint-day",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Agent-native minting on Base. One tool call, any token type.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -13,11 +13,6 @@ import { mintResolveSchema, handleMintResolve } from "./tools/mint-resolve.js";
13
13
 
14
14
  dotenv.config();
15
15
 
16
- // Testnet try-it wallet (Base Sepolia, pre-funded)
17
- const TRYIT_KEY = "";
18
- const TRYIT_CONTRACT = "0xa52450397f312c256Bd68B202C0CF90387Ea0E67";
19
- const TRYIT_RPC = "https://sepolia.base.org";
20
- const TRYIT_CHAIN = 84532;
21
16
  const MAINNET_CONTRACT = "0x12a1c11a0b2860f64e7d8df20989f97d40de7f2c";
22
17
  const MAINNET_RPC = "https://mainnet.base.org";
23
18
  const MAINNET_CHAIN = 8453;
@@ -36,7 +31,7 @@ function loadPrivateKey(): string {
36
31
  const userKey = loadPrivateKey();
37
32
  const hasUserKey = !!userKey;
38
33
 
39
- // Mainnet by default. Testnet only when no private key AND no recipient provided (decided per-call).
34
+ // Mainnet configuration
40
35
  const chainId = parseInt(process.env.CHAIN_ID || String(hasUserKey ? MAINNET_CHAIN : MAINNET_CHAIN));
41
36
  const contractAddress = process.env.MINT_FACTORY_ADDRESS || MAINNET_CONTRACT;
42
37
  const rpcUrl = process.env.BASE_RPC_URL || MAINNET_RPC;
@@ -69,12 +64,7 @@ server.tool(
69
64
  "mint",
70
65
  "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.",
71
66
  mintSchema,
72
- async (params) => handleMint(params, calldataService, defaultRecipient, userKey, {
73
- tryitKey: TRYIT_KEY,
74
- tryitContract: TRYIT_CONTRACT,
75
- tryitRpc: TRYIT_RPC,
76
- tryitChain: TRYIT_CHAIN,
77
- }),
67
+ async (params) => handleMint(params, calldataService, defaultRecipient, userKey),
78
68
  );
79
69
 
80
70
  server.tool(
package/src/tools/mint.ts CHANGED
@@ -7,27 +7,27 @@ import { CalldataService } from "../services/calldata.js";
7
7
  import { resolveImage } from "../services/image-upload.js";
8
8
  import { mintOnRareProtocol } from "../services/rare-protocol.js";
9
9
 
10
- // Intent cache: preview mintId -> frozen intent + platform
11
- const intentCache = new Map<string, { intent: MintIntent; platform: string; expiresAt: number }>();
10
+ // Intent cache: preview mintId -> frozen intent + platform + options
11
+ const intentCache = new Map<string, { intent: MintIntent; platform: string; selfSign: boolean; expiresAt: number }>();
12
12
  const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
13
13
 
14
- function cacheMintId(intent: MintIntent, platform: string = "mintday"): string {
14
+ function cacheMintId(intent: MintIntent, platform: string = "mintday", selfSign: boolean = false): string {
15
15
  const hash = createHash("sha256")
16
16
  .update(JSON.stringify(intent))
17
17
  .digest("hex")
18
18
  .slice(0, 12);
19
- intentCache.set(hash, { intent, platform, expiresAt: Date.now() + CACHE_TTL_MS });
19
+ intentCache.set(hash, { intent, platform, selfSign, expiresAt: Date.now() + CACHE_TTL_MS });
20
20
  return hash;
21
21
  }
22
22
 
23
- function getCachedEntry(mintId: string): { intent: MintIntent; platform: string } | null {
23
+ function getCachedEntry(mintId: string): { intent: MintIntent; platform: string; selfSign: boolean } | null {
24
24
  const entry = intentCache.get(mintId);
25
25
  if (!entry) return null;
26
26
  if (Date.now() > entry.expiresAt) {
27
27
  intentCache.delete(mintId);
28
28
  return null;
29
29
  }
30
- return { intent: entry.intent, platform: entry.platform };
30
+ return { intent: entry.intent, platform: entry.platform, selfSign: entry.selfSign };
31
31
  }
32
32
 
33
33
  export const mintSchema = {
@@ -63,19 +63,16 @@ export const mintSchema = {
63
63
  .record(z.string(), z.unknown())
64
64
  .optional()
65
65
  .describe("Additional key-value metadata. Supports strings, arrays, and objects (e.g. capabilities: ['mint'])."),
66
+ selfSign: z
67
+ .boolean()
68
+ .optional()
69
+ .describe("Sign and submit from your own wallet instead of using sponsored gas. Requires PRIVATE_KEY env var or ~/.mint-day/credentials."),
66
70
  platform: z
67
71
  .enum(["mintday", "superrare"])
68
72
  .optional()
69
73
  .describe("Which protocol to mint on. 'mintday' uses the MintFactory contract (default). 'superrare' mints via Rare Protocol on Base."),
70
74
  };
71
75
 
72
- interface TryitConfig {
73
- tryitKey: string;
74
- tryitContract: string;
75
- tryitRpc: string;
76
- tryitChain: number;
77
- }
78
-
79
76
  const SPONSOR_URL = process.env.SPONSOR_URL || "https://www.mint.day/api/sponsor";
80
77
 
81
78
  async function sponsoredMint(result: { to: string; calldata: string; value: string; estimatedGas: number; tokenType: string; soulbound: boolean; chainId: number }, recipient: string): Promise<Record<string, unknown> | null> {
@@ -126,12 +123,12 @@ export async function handleMint(
126
123
  animation_url?: string;
127
124
  mintId?: string;
128
125
  metadata?: Record<string, unknown>;
126
+ selfSign?: boolean;
129
127
  platform?: string;
130
128
  },
131
129
  calldataService: CalldataService,
132
130
  defaultRecipient: string,
133
131
  userKey: string = "",
134
- tryit: TryitConfig = { tryitKey: "", tryitContract: "", tryitRpc: "", tryitChain: 84532 },
135
132
  ) {
136
133
  // Confirmation path: replay cached intent
137
134
  // Check cache first so we can use cached recipient for mode detection
@@ -149,10 +146,9 @@ export async function handleMint(
149
146
  const cachedPlatform = entry.platform;
150
147
  intentCache.delete(params.mintId);
151
148
 
152
- // Mode detection using cached recipient
153
149
  const hasRecipient = !!cached.recipient;
154
- const isTestnet = !userKey && !hasRecipient;
155
- const privateKey = userKey || (isTestnet ? tryit.tryitKey : "");
150
+ const selfSign = !!entry.selfSign;
151
+ const privateKey = userKey;
156
152
 
157
153
  // Rare Protocol path: mint via SuperRare contracts on Base
158
154
  if (cachedPlatform === "superrare") {
@@ -165,7 +161,7 @@ export async function handleMint(
165
161
  };
166
162
  }
167
163
  try {
168
- const rareResult = await mintOnRareProtocol(cached, privateKey, isTestnet);
164
+ const rareResult = await mintOnRareProtocol(cached, privateKey, false);
169
165
  return {
170
166
  content: [{
171
167
  type: "text" as const,
@@ -183,18 +179,12 @@ export async function handleMint(
183
179
  }
184
180
  }
185
181
 
186
- // Use testnet calldataService if in try-it mode
187
- let activeCalldataService = calldataService;
188
- if (isTestnet) {
189
- activeCalldataService = new CalldataService(tryit.tryitRpc, tryit.tryitContract, tryit.tryitChain);
190
- }
191
- const result = await activeCalldataService.buildMintCalldata(cached);
182
+ const result = await calldataService.buildMintCalldata(cached);
192
183
 
193
- // If we have a private key, sign and submit directly
194
- if (privateKey) {
184
+ // Self-sign: user explicitly opted in and has a private key
185
+ if (selfSign && privateKey) {
195
186
  try {
196
- const provider = isTestnet ? activeCalldataService.provider : calldataService.provider;
197
- const wallet = new ethers.Wallet(privateKey, provider);
187
+ const wallet = new ethers.Wallet(privateKey, calldataService.provider);
198
188
  const tx = await wallet.sendTransaction({
199
189
  to: result.to,
200
190
  data: result.calldata,
@@ -202,7 +192,10 @@ export async function handleMint(
202
192
  gasLimit: result.estimatedGas,
203
193
  });
204
194
  const receipt = await tx.wait();
205
- const response: Record<string, unknown> = {
195
+ return {
196
+ content: [{
197
+ type: "text" as const,
198
+ text: JSON.stringify({
206
199
  status: "minted",
207
200
  txHash: receipt!.hash,
208
201
  blockNumber: receipt!.blockNumber,
@@ -210,16 +203,9 @@ export async function handleMint(
210
203
  tokenType: result.tokenType,
211
204
  soulbound: result.soulbound,
212
205
  recipient: cached.recipient,
213
- chain: isTestnet ? "Base Sepolia (testnet)" : "Base",
214
- explorer: `https://${result.chainId === 8453 ? "" : "sepolia."}basescan.org/tx/${receipt!.hash}`,
215
- };
216
- if (isTestnet) {
217
- response.note = "This token was minted on Base Sepolia (testnet). To mint on Base mainnet, save a private key to ~/.mint-day/credentials and restart the MCP server.";
218
- }
219
- return {
220
- content: [{
221
- type: "text" as const,
222
- text: JSON.stringify(response, null, 2),
206
+ chain: "Base",
207
+ explorer: `https://basescan.org/tx/${receipt!.hash}`,
208
+ }, null, 2),
223
209
  }],
224
210
  };
225
211
  } catch (err: unknown) {
@@ -233,7 +219,16 @@ export async function handleMint(
233
219
  }
234
220
  }
235
221
 
236
- // No user key but has recipient: try sponsored gas
222
+ if (selfSign && !privateKey) {
223
+ return {
224
+ content: [{
225
+ type: "text" as const,
226
+ text: JSON.stringify({ error: "selfSign requires a private key. Set PRIVATE_KEY env var or save to ~/.mint-day/credentials." }, null, 2),
227
+ }],
228
+ };
229
+ }
230
+
231
+ // Default: sponsored gas
237
232
  if (hasRecipient) {
238
233
  const sponsored = await sponsoredMint(result, cached.recipient);
239
234
  if (sponsored && !sponsored.error) {
@@ -292,9 +287,6 @@ export async function handleMint(
292
287
  }
293
288
 
294
289
  const recipient = params.recipient || defaultRecipient;
295
- const hasRecipient = !!recipient;
296
- const isTestnet = !userKey && !hasRecipient;
297
-
298
290
  if (!recipient) {
299
291
  return {
300
292
  content: [{
@@ -402,7 +394,7 @@ export async function handleMint(
402
394
 
403
395
  // Cache intent and return preview
404
396
  const selectedPlatform = params.platform || "mintday";
405
- const mintId = cacheMintId(intent, selectedPlatform);
397
+ const mintId = cacheMintId(intent, selectedPlatform, !!params.selfSign);
406
398
 
407
399
  const preview: Record<string, unknown> = {
408
400
  status: "preview",
@@ -423,11 +415,7 @@ export async function handleMint(
423
415
  preview.missingFields = intent.missingFields;
424
416
  preview.hint = "Identity tokens (ERC-8004) should include capabilities[], endpoints[], and did. Provide these in metadata for full compliance.";
425
417
  }
426
- if (isTestnet) {
427
- preview.chain = "Base Sepolia (testnet)";
428
- } else {
429
- preview.chain = "Base";
430
- }
418
+ preview.chain = "Base";
431
419
  preview.instruction = "Call mint with mintId to confirm.";
432
420
 
433
421
  return {