#!/usr/bin/env npx tsx /** * REST API Template: Express.js Middleware * Monetize any Express API with per-route billing. * * This template uses settlegridMiddleware() — the REST equivalent of sg.wrap(). * While MCP templates use sg.wrap() to bill function calls, REST templates use * settlegridMiddleware() to bill HTTP requests. Same billing pipeline, different * integration pattern. * * Works with all 10 SettleGrid protocols — protocol detection is automatic. * * Setup: * 1. npm install @settlegrid/mcp express * 2. Set SETTLEGRID_API_KEY in your env * 3. Register your tool at settlegrid.ai/dashboard/tools * 4. Run: npx tsx rest-express-api.ts * * Pricing: 3 cents for search, 10 cents for analysis * - Search hits a lightweight index — low compute cost * - Analysis runs Claude — ~$0.01 API cost per call * - 10 cents gives you ~10x margin on analysis * * Revenue: You keep 95-100% (100% on Free tier, 95% on paid tiers) */ import express, { Request, Response, NextFunction } from 'express' import { settlegridMiddleware } from '@settlegrid/mcp/rest' const app = express() app.use(express.json({ limit: '1mb' })) // ── SettleGrid Billing Setup ──────────────────────────────────────────────── // Create a billing middleware instance for each pricing tier. Mount it on // specific routes so different endpoints can charge different amounts. const searchBilling = settlegridMiddleware({ toolSlug: 'my-knowledge-api', // Replace with your tool slug pricing: { defaultCostCents: 3, methods: { search: { costCents: 3, displayName: 'Knowledge Search' }, }, }, }) const analyzeBilling = settlegridMiddleware({ toolSlug: 'my-knowledge-api', pricing: { defaultCostCents: 10, methods: { analyze: { costCents: 10, displayName: 'Deep Analysis' }, }, }, }) // ── Types ─────────────────────────────────────────────────────────────────── interface SearchResult { id: string title: string snippet: string score: number } interface AnalysisResult { summary: string keyTopics: string[] sentiment: 'positive' | 'neutral' | 'negative' confidence: number } // ── Implementation ────────────────────────────────────────────────────────── async function searchKnowledgeBase(query: string, limit: number): Promise { if (!query || query.trim().length === 0) { throw new Error('Query must be a non-empty string') } const safeLimit = Math.min(Math.max(limit, 1), 50) // Replace with your actual search backend (Elasticsearch, Typesense, pgvector, etc.) const response = await fetch( `${process.env.SEARCH_ENDPOINT!}/api/search`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.SEARCH_API_KEY!}`, }, body: JSON.stringify({ query, limit: safeLimit }), } ) if (!response.ok) { throw new Error(`Search backend returned ${response.status}`) } const data = await response.json() return (data.results ?? []).map((r: { id: string; title: string; snippet: string; score: number }) => ({ id: r.id, title: r.title, snippet: r.snippet, score: r.score, })) } async function analyzeContent(text: string): Promise { if (!text || text.trim().length === 0) { throw new Error('Text must be non-empty') } if (text.length > 50_000) { throw new Error('Text exceeds 50,000 character limit') } // Replace with your actual AI provider const response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.ANTHROPIC_API_KEY!, 'anthropic-version': '2023-06-01', }, body: JSON.stringify({ model: 'claude-sonnet-4-20250514', max_tokens: 2048, system: 'Analyze the provided text. Return a JSON object with: summary (string), keyTopics (string[]), sentiment ("positive"|"neutral"|"negative"), confidence (0-1 number). Return ONLY valid JSON.', messages: [{ role: 'user', content: text }], }), }) if (!response.ok) { throw new Error(`Claude API returned ${response.status}`) } const data = await response.json() const raw = data.content?.[0]?.text ?? '{}' try { const cleaned = raw.replace(/```json\n?|\n?```/g, '').trim() return JSON.parse(cleaned) as AnalysisResult } catch { return { summary: raw, keyTopics: [], sentiment: 'neutral', confidence: 0 } } } // ── Routes ────────────────────────────────────────────────────────────────── // POST /api/search — 3 cents per query app.post('/api/search', async (req: Request, res: Response, next: NextFunction) => { try { await searchBilling(req, 'search') } catch (err) { const message = err instanceof Error ? err.message : 'Billing error' res.status(402).json({ error: message }) return } try { const { query, limit = 10 } = req.body as { query: string; limit?: number } const results = await searchKnowledgeBase(query, limit) res.json({ results, count: results.length }) } catch (err) { const message = err instanceof Error ? err.message : 'Internal error' res.status(500).json({ error: message }) } }) // POST /api/analyze — 10 cents per analysis app.post('/api/analyze', async (req: Request, res: Response, next: NextFunction) => { try { await analyzeBilling(req, 'analyze') } catch (err) { const message = err instanceof Error ? err.message : 'Billing error' res.status(402).json({ error: message }) return } try { const { text } = req.body as { text: string } const analysis = await analyzeContent(text) res.json({ analysis }) } catch (err) { const message = err instanceof Error ? err.message : 'Internal error' res.status(500).json({ error: message }) } }) // ── Health check (no billing) ─────────────────────────────────────────────── app.get('/health', (_req: Request, res: Response) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }) }) // ── Start Server ──────────────────────────────────────────────────────────── const PORT = parseInt(process.env.PORT ?? '3000', 10) app.listen(PORT, () => { console.log(`Knowledge API running on :${PORT}`) console.log(' POST /api/search — 3 cents/query') console.log(' POST /api/analyze — 10 cents/call') })