I've been building a series of AI micro-tools under the name BaseAI. The latest: HostAI, a free listing optimizer for Airbnb, Booking.com and Vrbo. Here's exactly how it's built, including the parts that surprised me.
The stack (zero monthly cost)
- Cloudflare Workers — API, AI calls, rate limiting, Telegram cron
- Cloudflare KV — rate limiting (5 free req/day per IP) + Pro token storage
- Cloudflare Pages — static frontend
- Anthropic Claude — Haiku for quick optimizations, Sonnet for full audits
- Brevo — transactional email (300/day free tier)
- Stripe Payment Links — no checkout page needed
Total fixed monthly cost: $0. It runs on free tiers until traffic justifies upgrading.
Rate limiting without a database
The key design decision: no user accounts, no database, no friction. Three free optimizations per day per IP, verified via KV:
async function checkRateLimit(env, ip, type) {
const limits = { optimize: 3, 'mini-audit': 1 };
const today = new Date().toISOString().split('T')[0];
const key = `rate:${type}:${ip}:${today}`;
const count = parseInt((await env.HA_RATE.get(key)) || '0');
if (count >= limits[type]) return { allowed: false };
await env.HA_RATE.put(key, String(count + 1), { expirationTtl: 86400 });
return { allowed: true };
}
KV keys auto-expire after 24h. No cleanup needed. No cron job. No database.
The Pro activation flow (no webhook needed)
Stripe webhooks are overkill for this use case. The approach: Stripe redirects to the success URL with ?session_id=cs_live_xxx. The frontend POSTs that to /activate-pro, the worker validates the cs_live_ prefix and generates a permanent token:
if (!sessionId?.startsWith('cs_live_')) return json({ error: 'Invalid session' }, 400);
const existing = await env.HA_TOKENS.get('session:' + sessionId);
if (existing) return json({ token: existing, reused: true });
const token = 'ha_pro_' + crypto.randomUUID().replace(/-/g, '');
await env.HA_TOKENS.put('session:' + sessionId, token);
await env.HA_TOKENS.put('token:' + token, JSON.stringify({ email, activated: new Date().toISOString() }));
Idempotent — replaying the same session_id returns the same token. The user gets it by email via Brevo.
Async audit delivery via ctx.waitUntil
The full HostScan audit (10 sections, Sonnet model) takes 15-30 seconds. Too long for a synchronous response. Solution: respond immediately, run the audit in the background:
ctx.waitUntil((async () => {
const audit = await runFullAudit(env, body);
const html = buildAuditEmail(audit, body.title);
await sendEmail(env, body.email, `HostScan: ${body.title}`, html);
})());
return json({ success: true, message: 'Check your email in ~15 minutes.' });
ctx.waitUntil keeps the Worker alive after the response is sent. The user gets instant confirmation, audit arrives by email.
Telegram daily cron (passive audience building)
# wrangler.toml
[triggers]
crons = ["0 8 * * *"]
export default {
fetch: handleRequest,
async scheduled(event, env, ctx) {
ctx.waitUntil(postDailyTip(env));
}
};
Every morning at 08:00 UTC, one actionable Airbnb hosting tip goes out to the Telegram channel. No manual work. The channel grows while I sleep.
What surprised me
Platform-specific prompting matters more than I expected. Airbnb titles max out at 50 characters. Booking.com weights location keywords heavily. Vrbo leans toward family/group language. Giving Claude explicit platform context in the prompt increased output quality noticeably — generic prompts produce generic results.
The audit model selection is important. Using Haiku for 10-section audits produced mediocre output. Sonnet produces genuinely useful, specific recommendations. The $29 price point absorbs the extra cost easily.
Try it
HostAI — free optimizer, 3/day, no signup
HostScan — full 10-section audit, $29, delivered by email
Full codebase available — drop a comment if you want the repo link.
Top comments (0)