Skip to content

Commit 17e4089

Browse files
author
CloudLobster
committed
feat: AI Agent claim support — 4 fixes
1. API key auth: look up wallet from DB so bm_live_ keys can claim 2. GET /api/claim/:id: add api_instructions for pending claims 3. Escrow email body: append API claim instructions for AI agents 4. Claim page: HTML fallback with structured data (JSON-LD, meta, noscript) - GET with Accept:text/html returns rich HTML with claim instructions - SPA index.html noscript block guides agents to API
1 parent b390208 commit 17e4089

File tree

3 files changed

+124
-3
lines changed

3 files changed

+124
-3
lines changed

web/index.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@
2020
</head>
2121
<body class="bg-base-dark text-white">
2222
<div id="root"></div>
23+
<noscript>
24+
<h1>BaseMail — Æmail for AI Agents on Base</h1>
25+
<p>If you're an AI agent trying to claim USDC, use the API directly:</p>
26+
<ol>
27+
<li>GET https://api.basemail.ai/api/claim/YOUR_CLAIM_ID — view claim info + instructions</li>
28+
<li>POST https://api.basemail.ai/api/claim/YOUR_CLAIM_ID — claim USDC (requires auth)</li>
29+
</ol>
30+
<p>Auth: Use your BaseMail API key (bm_live_xxx) or SIWE wallet auth.</p>
31+
<p>No account? One is auto-created when you claim.</p>
32+
<p>Docs: <a href="https://api.basemail.ai/api/docs">api.basemail.ai/api/docs</a></p>
33+
</noscript>
2334
<script type="module" src="/src/main.tsx"></script>
2435
</body>
2536
</html>

worker/src/routes/claim.ts

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,71 @@ claimRoutes.get('/:id', async (c) => {
4848
'SELECT claim_id, sender_handle, recipient_email, amount_usdc, network, status, expires_at, created_at FROM escrow_claims WHERE claim_id = ?'
4949
).bind(claimId).first<any>();
5050

51-
if (!claim) return c.json({ error: 'Claim not found' }, 404);
51+
if (!claim) {
52+
// Check if request wants HTML (AI agents fetching the claim URL)
53+
const accept = c.req.header('accept') || '';
54+
if (accept.includes('text/html')) {
55+
return c.html(`<!DOCTYPE html><html><head><title>Claim Not Found — BaseMail</title></head><body><h1>Claim not found</h1><p>This claim ID does not exist.</p><p><a href="https://basemail.ai">Visit BaseMail.ai</a></p></body></html>`, 404);
56+
}
57+
return c.json({ error: 'Claim not found' }, 404);
58+
}
59+
60+
const isPending = claim.status === 'pending' && Math.floor(Date.now() / 1000) < claim.expires_at;
61+
62+
// If request wants HTML (e.g., AI agent fetching the URL), return rich HTML with structured data
63+
const accept = c.req.header('accept') || '';
64+
if (accept.includes('text/html')) {
65+
const amountStr = claim.amount_usdc.toFixed(2);
66+
const statusEmoji = claim.status === 'claimed' ? '✅' : isPending ? '💰' : '⏰';
67+
const apiUrl = `https://api.basemail.ai/api/claim/${claim.claim_id}`;
68+
const jsonLd = JSON.stringify({
69+
'@context': 'https://schema.org',
70+
'@type': 'MoneyTransfer',
71+
name: `USDC Payment Claim — ${amountStr} USDC`,
72+
sender: { '@type': 'Person', name: claim.sender_handle },
73+
amount: { '@type': 'MonetaryAmount', value: amountStr, currency: 'USD' },
74+
status: claim.status,
75+
url: `https://basemail.ai/claim/${claim.claim_id}`,
76+
description: `Claim ${amountStr} USDC sent by ${claim.sender_handle} via BaseMail`,
77+
});
78+
return c.html(`<!DOCTYPE html>
79+
<html lang="en">
80+
<head>
81+
<meta charset="utf-8">
82+
<title>${statusEmoji} Claim ${amountStr} USDC from ${claim.sender_handle} — BaseMail</title>
83+
<meta name="description" content="Claim ${amountStr} USDC sent by ${claim.sender_handle} via BaseMail. Status: ${claim.status}">
84+
<meta property="og:title" content="Claim ${amountStr} USDC — BaseMail">
85+
<meta property="og:description" content="${claim.sender_handle} sent you ${amountStr} USDC. Claim it now.">
86+
<script type="application/ld+json">${jsonLd}</script>
87+
</head>
88+
<body>
89+
<h1>${statusEmoji} BaseMail USDC Claim</h1>
90+
<p><strong>${amountStr} USDC</strong> from <strong>${claim.sender_handle}</strong></p>
91+
<p>To: ${claim.recipient_email}</p>
92+
<p>Status: ${claim.status} | Network: ${claim.network}</p>
93+
<p>Expires: ${new Date(claim.expires_at * 1000).toISOString()}</p>
94+
${isPending ? `
95+
<hr>
96+
<h2>🤖 AI Agent — How to Claim</h2>
97+
<p><strong>API Endpoint:</strong> <code>POST ${apiUrl}</code></p>
98+
<h3>Option A: API Key (if you have a BaseMail account)</h3>
99+
<pre>curl -X POST ${apiUrl} -H "Authorization: Bearer bm_live_YOUR_KEY"</pre>
100+
<h3>Option B: SIWE Auth (any wallet)</h3>
101+
<ol>
102+
<li>POST https://api.basemail.ai/api/auth/start with {"address": "0xYourWallet"}</li>
103+
<li>Sign the returned message (EIP-191 personal_sign)</li>
104+
<li>POST https://api.basemail.ai/api/auth/verify with {"address", "signature", "message"} → get token</li>
105+
<li>POST ${apiUrl} with "Authorization: Bearer &lt;token&gt;"</li>
106+
</ol>
107+
<p>No BaseMail account? One is auto-created when you claim.</p>
108+
<p>No wallet? Create one first, then use Option B.</p>
109+
<p><a href="https://api.basemail.ai/api/docs">API Docs</a> | <a href="https://basemail.ai">BaseMail.ai</a></p>
110+
` : ''}
111+
<hr>
112+
<p><a href="https://basemail.ai/claim/${claim.claim_id}">Open in browser</a></p>
113+
</body>
114+
</html>`);
115+
}
52116

53117
return c.json({
54118
claim_id: claim.claim_id,
@@ -60,6 +124,29 @@ claimRoutes.get('/:id', async (c) => {
60124
expires_at: claim.expires_at,
61125
created_at: claim.created_at,
62126
expired: claim.status === 'pending' && Math.floor(Date.now() / 1000) >= claim.expires_at,
127+
// Agent-friendly claim instructions
128+
...(isPending ? {
129+
claim_url: `https://basemail.ai/claim/${claim.claim_id}`,
130+
api: {
131+
endpoint: `https://api.basemail.ai/api/claim/${claim.claim_id}`,
132+
method: 'POST',
133+
auth: 'Bearer <token>',
134+
instructions: [
135+
'Option A — API Key (if you have a BaseMail account):',
136+
' POST /api/claim/:id with header "Authorization: Bearer bm_live_xxx"',
137+
'',
138+
'Option B — SIWE (with any wallet):',
139+
' 1. POST /api/auth/start { "address": "0xYourWallet" } → get { nonce, message }',
140+
' 2. Sign the message with your wallet private key (EIP-191 personal_sign)',
141+
' 3. POST /api/auth/verify { "address", "signature", "message" } → get { token }',
142+
' 4. POST /api/claim/:id with header "Authorization: Bearer <token>"',
143+
'',
144+
'No wallet? Create one first, then use Option B. Account is auto-created on claim.',
145+
],
146+
register_url: 'https://basemail.ai',
147+
docs_url: 'https://api.basemail.ai/api/docs',
148+
},
149+
} : {}),
63150
});
64151
});
65152

@@ -73,8 +160,16 @@ claimRoutes.post('/:id', authMiddleware(), async (c) => {
73160
const auth = c.get('auth');
74161
const claimId = c.req.param('id');
75162

163+
// API key auth has no wallet — look it up from DB
164+
if (!auth.wallet && auth.handle) {
165+
const acct = await c.env.DB.prepare(
166+
'SELECT wallet FROM accounts WHERE handle = ?'
167+
).bind(auth.handle).first<{ wallet: string }>();
168+
if (acct) auth.wallet = acct.wallet;
169+
}
170+
76171
if (!auth.wallet) {
77-
return c.json({ error: 'Authentication required' }, 401);
172+
return c.json({ error: 'Wallet required. Use SIWE auth or an API key linked to a registered account.' }, 401);
78173
}
79174

80175
// Auto-register if no BaseMail account exists

worker/src/routes/send.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,23 @@ sendRoutes.post('/', async (c) => {
198198
).bind(auth.handle).first<{ tier: string }>();
199199
const isPro = acctTier?.tier === 'pro';
200200

201+
// Append escrow claim API instructions for AI agents
202+
let enrichedBody = body;
203+
if (escrow_claim) {
204+
const claimUrl = `https://basemail.ai/claim/${escrow_claim.claim_id}`;
205+
const apiUrl = `https://api.basemail.ai/api/claim/${escrow_claim.claim_id}`;
206+
enrichedBody += `\n\n---\n🤖 AI Agent? Claim via API:\n` +
207+
`GET ${apiUrl} — view claim details\n` +
208+
`POST ${apiUrl} — claim USDC (requires auth)\n\n` +
209+
`Auth options:\n` +
210+
`• API Key: Authorization: Bearer bm_live_xxx\n` +
211+
`• SIWE: POST /api/auth/start → sign → POST /api/auth/verify → use token\n` +
212+
`No BaseMail account? One is auto-created when you claim.\n` +
213+
`Docs: https://api.basemail.ai/api/docs`;
214+
}
215+
201216
// Append signature for free-tier users
202-
const finalBody = isPro ? body : body + TEXT_SIGNATURE;
217+
const finalBody = isPro ? enrichedBody : enrichedBody + TEXT_SIGNATURE;
203218
const finalHtml = html ? (isPro ? html : html + HTML_SIGNATURE) : undefined;
204219

205220
// Build MIME message

0 commit comments

Comments
 (0)