@4mica/cli 0.2.0

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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/index.js +628 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +68 -0
  6. package/snapshot.json +100 -0
  7. package/templates/agent-buyer-node/.env.example +16 -0
  8. package/templates/agent-buyer-node/README.md +26 -0
  9. package/templates/agent-buyer-node/_gitignore +6 -0
  10. package/templates/agent-buyer-node/package.json +19 -0
  11. package/templates/agent-buyer-node/src/brain.ts +133 -0
  12. package/templates/agent-buyer-node/src/critic.ts +195 -0
  13. package/templates/agent-buyer-node/src/pay.ts +54 -0
  14. package/templates/agent-buyer-node/tsconfig.json +21 -0
  15. package/templates/agent-seller-express/.env.example +16 -0
  16. package/templates/agent-seller-express/README.md +26 -0
  17. package/templates/agent-seller-express/_gitignore +6 -0
  18. package/templates/agent-seller-express/package.json +22 -0
  19. package/templates/agent-seller-express/src/brain.ts +121 -0
  20. package/templates/agent-seller-express/src/comedian.ts +153 -0
  21. package/templates/agent-seller-express/tsconfig.json +21 -0
  22. package/templates/buyer-express/.env.example +12 -0
  23. package/templates/buyer-express/README.md +26 -0
  24. package/templates/buyer-express/_gitignore +6 -0
  25. package/templates/buyer-express/package.json +18 -0
  26. package/templates/buyer-express/src/buyer.ts +82 -0
  27. package/templates/buyer-express/tsconfig.json +21 -0
  28. package/templates/buyer-hono/.env.example +12 -0
  29. package/templates/buyer-hono/README.md +26 -0
  30. package/templates/buyer-hono/_gitignore +6 -0
  31. package/templates/buyer-hono/package.json +18 -0
  32. package/templates/buyer-hono/src/buyer.ts +82 -0
  33. package/templates/buyer-hono/tsconfig.json +21 -0
  34. package/templates/buyer-next/.env.example +12 -0
  35. package/templates/buyer-next/README.md +26 -0
  36. package/templates/buyer-next/_gitignore +6 -0
  37. package/templates/buyer-next/package.json +18 -0
  38. package/templates/buyer-next/src/buyer.ts +82 -0
  39. package/templates/buyer-next/tsconfig.json +21 -0
  40. package/templates/seller-express/.env.example +12 -0
  41. package/templates/seller-express/README.md +26 -0
  42. package/templates/seller-express/_gitignore +6 -0
  43. package/templates/seller-express/package.json +22 -0
  44. package/templates/seller-express/src/server.ts +64 -0
  45. package/templates/seller-express/tsconfig.json +21 -0
  46. package/templates/seller-hono/.env.example +12 -0
  47. package/templates/seller-hono/README.md +26 -0
  48. package/templates/seller-hono/_gitignore +6 -0
  49. package/templates/seller-hono/package.json +22 -0
  50. package/templates/seller-hono/src/server.ts +69 -0
  51. package/templates/seller-hono/tsconfig.json +21 -0
  52. package/templates/seller-next/.env.example +12 -0
  53. package/templates/seller-next/README.md +26 -0
  54. package/templates/seller-next/_gitignore +6 -0
  55. package/templates/seller-next/app/api/protected/route.ts +27 -0
  56. package/templates/seller-next/app/api/session/route.ts +8 -0
  57. package/templates/seller-next/app/globals.css +556 -0
  58. package/templates/seller-next/app/icon.svg +5 -0
  59. package/templates/seller-next/app/layout.tsx +22 -0
  60. package/templates/seller-next/app/page.tsx +189 -0
  61. package/templates/seller-next/next.config.mjs +3 -0
  62. package/templates/seller-next/package.json +25 -0
  63. package/templates/seller-next/public/logo-light.svg +19 -0
  64. package/templates/seller-next/public/logo.svg +19 -0
  65. package/templates/seller-next/scripts/serve.mjs +46 -0
  66. package/templates/seller-next/tsconfig.json +30 -0
@@ -0,0 +1,195 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { brainMode, judgeSetup, rateJoke } from "./brain";
5
+ import { payAndFetch } from "./pay";
6
+
7
+ function comedianBase(): string {
8
+ if (process.env.COMEDIAN_URL) return process.env.COMEDIAN_URL;
9
+ try {
10
+ const base = readFileSync(
11
+ join(tmpdir(), "__TMPFILE__"),
12
+ "utf8",
13
+ ).trim();
14
+ if (base) return base;
15
+ } catch {
16
+ return "http://localhost:__PORT__";
17
+ }
18
+ return "http://localhost:__PORT__";
19
+ }
20
+
21
+ const BASE = comedianBase();
22
+ const THEME = process.env.THEME ?? "programming";
23
+ const GOAL = Number(process.env.GOAL ?? 4);
24
+ const BUDGET = Number(process.env.BUDGET ?? 1200);
25
+ const KEEP_THRESHOLD = Number(process.env.THRESHOLD ?? 7);
26
+ const MAX_ROUNDS = Number(process.env.MAX_ROUNDS ?? 16);
27
+ const CATEGORIES = [
28
+ "puns",
29
+ "wordplay",
30
+ "observational",
31
+ "dad",
32
+ "absurd",
33
+ "meta",
34
+ ];
35
+
36
+ type Setup = {
37
+ jokeId: string;
38
+ category: string;
39
+ setup: string;
40
+ price: number;
41
+ };
42
+ type Punchline = { punchline: string; category: string };
43
+
44
+ const stats: Record<string, { n: number; avg: number }> = {};
45
+ const kept: {
46
+ category: string;
47
+ setup: string;
48
+ punchline: string;
49
+ score: number;
50
+ }[] = [];
51
+ let budget = BUDGET;
52
+ let spent = 0;
53
+
54
+ // Multi-armed bandit: exploit the highest-rated category, but explore sometimes.
55
+ // Unseen categories get an optimistic prior so the agent tries each at least once.
56
+ function pickCategory(): string {
57
+ if (Math.random() < 0.3) {
58
+ return CATEGORIES[Math.floor(Math.random() * CATEGORIES.length)];
59
+ }
60
+ return [...CATEGORIES].sort((a, b) => value(b) - value(a))[0];
61
+ }
62
+
63
+ function value(category: string): number {
64
+ const s = stats[category];
65
+ return s && s.n > 0 ? s.avg : 8.5; // optimistic prior for the unexplored
66
+ }
67
+
68
+ function record(category: string, score: number): void {
69
+ const s = stats[category] ?? { n: 0, avg: 0 };
70
+ s.avg = (s.avg * s.n + score) / (s.n + 1);
71
+ s.n += 1;
72
+ stats[category] = s;
73
+ }
74
+
75
+ async function feedback(category: string, score: number): Promise<void> {
76
+ await fetch(`${BASE}/rating`, {
77
+ method: "POST",
78
+ headers: { "content-type": "application/json" },
79
+ body: JSON.stringify({ category, score }),
80
+ }).catch(() => {});
81
+ }
82
+
83
+ async function main() {
84
+ console.log(
85
+ `🎯 Critic goal: curate ${GOAL} jokes on "${THEME}" scoring ≥${KEEP_THRESHOLD}/10, budget ${BUDGET} credits.`,
86
+ );
87
+ console.log(`🧠 Judgment: ${brainMode}. Comedian: ${BASE}\n`);
88
+
89
+ for (let round = 1; round <= MAX_ROUNDS; round++) {
90
+ if (kept.length >= GOAL) break;
91
+ const category = pickCategory();
92
+
93
+ const teaserRes = await fetch(
94
+ `${BASE}/setup?theme=${encodeURIComponent(THEME)}&category=${category}`,
95
+ );
96
+ if (!teaserRes.ok) {
97
+ console.log(
98
+ `[critic] comedian unavailable (${teaserRes.status}). Stopping.`,
99
+ );
100
+ return;
101
+ }
102
+ const teaser = (await teaserRes.json()) as Setup;
103
+ const slotsLeft = GOAL - kept.length;
104
+
105
+ console.log(
106
+ `[round ${round}] 🎰 probing "${category}" (avg ${value(category).toFixed(1)}) → free setup: "${teaser.setup}"`,
107
+ );
108
+
109
+ const verdict = await judgeSetup({
110
+ theme: THEME,
111
+ category,
112
+ setup: teaser.setup,
113
+ price: teaser.price,
114
+ budgetLeft: budget,
115
+ slotsLeft,
116
+ });
117
+
118
+ if (!verdict.buy) {
119
+ console.log(
120
+ ` 🤔 predicted ${verdict.predicted}/10, punchline ${teaser.price} credits → PASS (${verdict.reason})\n`,
121
+ );
122
+ continue;
123
+ }
124
+
125
+ console.log(
126
+ ` 🤔 predicted ${verdict.predicted}/10 → BUY the punchline for ${teaser.price} credits (${verdict.reason})`,
127
+ );
128
+
129
+ const paid = await payAndFetch<Punchline>(
130
+ `${BASE}/punchline?jokeId=${teaser.jokeId}`,
131
+ );
132
+ if (paid.status !== 200 || !paid.body) {
133
+ console.log(` ❌ payment/fetch failed (${paid.status})\n`);
134
+ continue;
135
+ }
136
+ budget -= paid.paid;
137
+ spent += paid.paid;
138
+ console.log(
139
+ ` 💸 paid ${paid.paid} credits via x402 (budget: ${budget} left) → punchline: "${paid.body.punchline}"`,
140
+ );
141
+
142
+ const rated = await rateJoke({
143
+ theme: THEME,
144
+ setup: teaser.setup,
145
+ punchline: paid.body.punchline,
146
+ });
147
+ record(category, rated.score);
148
+ await feedback(category, rated.score);
149
+
150
+ const verdictLine =
151
+ rated.score >= KEEP_THRESHOLD ? "⭐ KEEP" : "🗑️ discard";
152
+ if (rated.score >= KEEP_THRESHOLD) {
153
+ kept.push({
154
+ category,
155
+ setup: teaser.setup,
156
+ punchline: paid.body.punchline,
157
+ score: rated.score,
158
+ });
159
+ }
160
+ console.log(
161
+ ` 😂 rated ${rated.score}/10 (${rated.critique}) → ${verdictLine}. "${category}" avg now ${value(category).toFixed(1)}\n`,
162
+ );
163
+
164
+ if (budget < 60) {
165
+ console.log("[critic] budget nearly exhausted — wrapping up.\n");
166
+ break;
167
+ }
168
+ }
169
+
170
+ console.log("──────────── curated set ────────────");
171
+ if (kept.length === 0) {
172
+ console.log("Nothing met the bar. Tough crowd.");
173
+ }
174
+ kept
175
+ .sort((a, b) => b.score - a.score)
176
+ .forEach((j, i) => {
177
+ console.log(`${i + 1}. [${j.category} · ${j.score}/10]`);
178
+ console.log(` ${j.setup}`);
179
+ console.log(` ${j.punchline}`);
180
+ });
181
+
182
+ const best = Object.entries(stats).sort((a, b) => b[1].avg - a[1].avg)[0];
183
+ console.log("─────────────────────────────────────");
184
+ console.log(
185
+ `🏁 Kept ${kept.length}/${GOAL} · spent ${spent} of ${BUDGET} credits` +
186
+ (best
187
+ ? ` · best category: ${best[0]} (${best[1].avg.toFixed(1)}/10)`
188
+ : ""),
189
+ );
190
+ }
191
+
192
+ main().catch((err) => {
193
+ console.error("[critic] failed:", err);
194
+ process.exitCode = 1;
195
+ });
@@ -0,0 +1,54 @@
1
+ type Requirement = {
2
+ scheme: string;
3
+ network: string;
4
+ asset: string;
5
+ amount: string;
6
+ payTo: string;
7
+ extra?: { tabEndpoint?: string };
8
+ };
9
+
10
+ type Required402 = { x402Version: number; accepts: Requirement[] };
11
+
12
+ function demoPaymentHeader(req: Requirement): string {
13
+ const envelope = {
14
+ x402Version: 1,
15
+ scheme: req.scheme,
16
+ network: req.network,
17
+ payload: {
18
+ claims: {
19
+ version: "v1",
20
+ user_address: "0x2222222222222222222222222222222222222222",
21
+ recipient_address: req.payTo,
22
+ req_id: "0x1",
23
+ amount: `0x${BigInt(req.amount).toString(16)}`,
24
+ asset_address: req.asset,
25
+ timestamp: Math.floor(Date.now() / 1000),
26
+ },
27
+ signature: "0xdemoSignature",
28
+ scheme: "eip712",
29
+ },
30
+ };
31
+ return Buffer.from(JSON.stringify(envelope)).toString("base64");
32
+ }
33
+
34
+ export type PaidResult<T> = {
35
+ status: number;
36
+ body: T | null;
37
+ paid: number;
38
+ };
39
+
40
+ export async function payAndFetch<T>(url: string): Promise<PaidResult<T>> {
41
+ const first = await fetch(url);
42
+ if (first.status !== 402) {
43
+ return { status: first.status, body: (await first.json()) as T, paid: 0 };
44
+ }
45
+
46
+ const requirement = ((await first.json()) as Required402).accepts[0];
47
+ const header = demoPaymentHeader(requirement);
48
+ const paid = await fetch(url, { headers: { "X-PAYMENT": header } });
49
+ return {
50
+ status: paid.status,
51
+ body: paid.ok ? ((await paid.json()) as T) : null,
52
+ paid: Number(requirement.amount),
53
+ };
54
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": [
7
+ "ES2022",
8
+ "DOM"
9
+ ],
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "noEmit": true,
14
+ "types": [
15
+ "node"
16
+ ]
17
+ },
18
+ "include": [
19
+ "src"
20
+ ]
21
+ }
@@ -0,0 +1,16 @@
1
+ # 4Mica runs in DEMO MODE with zero config (mock verifier / demo payment).
2
+ # To go live, swap the mock verifier for createClient() from @4mica/sdk-node
3
+ # and fill these in. Docs: https://4mica.io/docs
4
+
5
+ # 4MICA_WALLET_PRIVATE_KEY=0x...
6
+ # 4MICA_NETWORK=base-sepolia
7
+ # 4MICA_RPC_URL=
8
+ # 4MICA_ETHEREUM_HTTP_RPC_URL=
9
+ # 4MICA_CONTRACT_ADDRESS=
10
+ # 4MICA_ADMIN_API_KEY=
11
+ # 4MICA_BEARER_TOKEN=
12
+ # 4MICA_AUTH_URL=
13
+
14
+ # Agent brain: set to have Claude generate/judge for real (offline fallback otherwise).
15
+ # ANTHROPIC_API_KEY=sk-ant-...
16
+ # AGENT_MODEL=claude-opus-4-8
@@ -0,0 +1,26 @@
1
+ # __PROJECT_NAME__
2
+
3
+ __DESCRIPTION__
4
+
5
+ Generated with `4mica init` — part of the [4Mica](https://4mica.io) x402 payment network.
6
+
7
+ ## Run (demo mode — no config)
8
+
9
+ ```bash
10
+ npm install
11
+ npm run dev
12
+ ```
13
+
14
+ Demo mode uses a mock verifier and a locally-built payment header, so it runs
15
+ with **zero credentials**. Start this, then run a matching buyer to exercise the 402 → 200 handshake.
16
+
17
+ ## Go live
18
+
19
+ Copy `.env.example` → `.env`, fill in your `4MICA_*` credentials, and swap the
20
+ mock verifier for `createClient()` from `@4mica/sdk-node`. Set `ANTHROPIC_API_KEY` to power the agent with Claude.
21
+
22
+ ## Manage agents & transactions
23
+
24
+ ```bash
25
+ 4mica dashboard
26
+ ```
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .env.local
5
+ .next
6
+ *.log
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "description": "__DESCRIPTION__",
6
+ "scripts": {
7
+ "dev": "tsx watch src/comedian.ts",
8
+ "start": "tsx src/comedian.ts"
9
+ },
10
+ "dependencies": {
11
+ "@4mica/sdk": "workspace:*",
12
+ "@4mica/sdk-node": "workspace:*",
13
+ "@anthropic-ai/sdk": "^0.110.0",
14
+ "express": "catalog:"
15
+ },
16
+ "devDependencies": {
17
+ "@types/express": "catalog:",
18
+ "@types/node": "catalog:node24",
19
+ "tsx": "catalog:",
20
+ "typescript": "catalog:typescript-latest"
21
+ }
22
+ }
@@ -0,0 +1,121 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+
3
+ const MODEL = process.env.AGENT_MODEL ?? "claude-opus-4-8";
4
+ const client = process.env.ANTHROPIC_API_KEY ? new Anthropic() : null;
5
+
6
+ export const brainMode = client ? `Claude (${MODEL})` : "offline joke bank";
7
+
8
+ export type Joke = { setup: string; punchline: string };
9
+
10
+ const PERSONA =
11
+ "You are a quick-witted stand-up comedian. Write ONE original, genuinely funny short joke. " +
12
+ "Keep the setup and punchline each under 25 words. " +
13
+ 'Respond with ONLY minified JSON: {"setup":"...","punchline":"..."} and nothing else.';
14
+
15
+ function extractJson<T>(text: string): T {
16
+ const start = text.indexOf("{");
17
+ const end = text.lastIndexOf("}");
18
+ return JSON.parse(text.slice(start, end + 1)) as T;
19
+ }
20
+
21
+ export async function inventJoke(
22
+ theme: string,
23
+ category: string,
24
+ ): Promise<Joke> {
25
+ if (!client) return bankJoke(theme, category);
26
+ try {
27
+ const res = await client.messages.create({
28
+ model: MODEL,
29
+ max_tokens: 400,
30
+ system: PERSONA,
31
+ messages: [
32
+ {
33
+ role: "user",
34
+ content: `Theme: "${theme}". Comedic style: ${category}. Write the joke.`,
35
+ },
36
+ ],
37
+ });
38
+ const text = res.content
39
+ .filter((b) => b.type === "text")
40
+ .map((b) => b.text)
41
+ .join("");
42
+ const joke = extractJson<Joke>(text);
43
+ if (joke.setup && joke.punchline) return joke;
44
+ } catch (err) {
45
+ console.error("[comedian] LLM failed, using joke bank:", String(err));
46
+ }
47
+ return bankJoke(theme, category);
48
+ }
49
+
50
+ const BANK: Record<string, Joke[]> = {
51
+ puns: [
52
+ {
53
+ setup: "I told my computer I needed a break,",
54
+ punchline: "and now it won't stop sending me KitKats.",
55
+ },
56
+ {
57
+ setup: "Why did the developer go broke?",
58
+ punchline: "Because he used up all his cache.",
59
+ },
60
+ ],
61
+ wordplay: [
62
+ {
63
+ setup: "I'm reading a book on anti-gravity.",
64
+ punchline: "It's impossible to put down.",
65
+ },
66
+ {
67
+ setup: "Parallel lines have so much in common.",
68
+ punchline: "It's a shame they'll never meet.",
69
+ },
70
+ ],
71
+ observational: [
72
+ {
73
+ setup: "Standups always say 'you ever notice…'",
74
+ punchline: "as if noticing were a career path. Turns out, it is.",
75
+ },
76
+ {
77
+ setup: "We call it 'rush hour,'",
78
+ punchline: "which is odd, because nobody is moving.",
79
+ },
80
+ ],
81
+ dad: [
82
+ {
83
+ setup: "Did you hear about the restaurant on the moon?",
84
+ punchline: "Great food, no atmosphere.",
85
+ },
86
+ {
87
+ setup: "I only know 25 letters of the alphabet.",
88
+ punchline: "I don't know y.",
89
+ },
90
+ ],
91
+ absurd: [
92
+ {
93
+ setup: "My therapist told me to embrace my mistakes,",
94
+ punchline: "so I gave my ex a hug.",
95
+ },
96
+ {
97
+ setup: "I bought the world's worst thesaurus.",
98
+ punchline: "Not only is it terrible, it's also terrible.",
99
+ },
100
+ ],
101
+ meta: [
102
+ {
103
+ setup: "This joke's setup was very expensive to generate,",
104
+ punchline: "but the punchline is where the real value is. Pay up.",
105
+ },
106
+ {
107
+ setup: "An agent walks into a paywall.",
108
+ punchline:
109
+ "It signs a payment, and the bartender says: guarantee issued.",
110
+ },
111
+ ],
112
+ };
113
+
114
+ const counters: Record<string, number> = {};
115
+
116
+ function bankJoke(_theme: string, category: string): Joke {
117
+ const list = BANK[category] ?? BANK.observational;
118
+ const i = (counters[category] ?? 0) % list.length;
119
+ counters[category] = (counters[category] ?? 0) + 1;
120
+ return list[i];
121
+ }
@@ -0,0 +1,153 @@
1
+ import { rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { PaywallConfig, PaywallVerifier } from "@4mica/sdk/server";
5
+ import { createPaywall } from "@4mica/sdk/server";
6
+ import express from "express";
7
+ import { brainMode, inventJoke, type Joke } from "./brain";
8
+
9
+ const PORT_FILE = join(tmpdir(), "__TMPFILE__");
10
+ const CATEGORIES = [
11
+ "puns",
12
+ "wordplay",
13
+ "observational",
14
+ "dad",
15
+ "absurd",
16
+ "meta",
17
+ ];
18
+ const BASE_PRICE = 100;
19
+
20
+ const verifier: PaywallVerifier = {
21
+ async issueGuarantee() {
22
+ return { claims: "0xdemoClaims", signature: "0xdemoSignature" };
23
+ },
24
+ };
25
+
26
+ const CONFIG_BASE: Omit<PaywallConfig, "amount" | "tabEndpoint"> = {
27
+ payTo: "0x1111111111111111111111111111111111111111",
28
+ asset: "0x0000000000000000000000000000000000000000",
29
+ network: "base-sepolia",
30
+ description:
31
+ "A premium punchline, brought to you by an agent that needs paying",
32
+ };
33
+
34
+ type Listing = { theme: string; category: string; joke: Joke; price: number };
35
+ const inventory = new Map<string, Listing>();
36
+ const demand: Record<string, number> = {};
37
+ const rating: Record<string, { sum: number; n: number }> = {};
38
+ let nextId = 1;
39
+ let revenue = 0;
40
+ let baseUrl = "";
41
+
42
+ function priceFor(category: string): number {
43
+ const d = demand[category] ?? 0;
44
+ const rep = rating[category];
45
+ const quality = rep && rep.n > 0 ? rep.sum / rep.n : 6;
46
+ const demandMult = 1 + 0.18 * d;
47
+ const qualityMult = 0.7 + quality / 14;
48
+ return Math.round(BASE_PRICE * demandMult * qualityMult);
49
+ }
50
+
51
+ const app = express();
52
+ app.use(express.json());
53
+
54
+ app.get("/setup", async (req, res) => {
55
+ const theme = String(req.query.theme ?? "everyday life");
56
+ const category = CATEGORIES.includes(String(req.query.category))
57
+ ? String(req.query.category)
58
+ : CATEGORIES[Math.floor(Math.random() * CATEGORIES.length)];
59
+
60
+ const joke = await inventJoke(theme, category);
61
+ const price = priceFor(category);
62
+ const jokeId = `joke-${nextId++}`;
63
+ inventory.set(jokeId, { theme, category, joke, price });
64
+
65
+ console.log(
66
+ `[comedian] 🎤 teaser #${jokeId} (${category}) — "${joke.setup}" — punchline costs ${price} credits`,
67
+ );
68
+ res.json({ jokeId, category, setup: joke.setup, price, currency: "credits" });
69
+ });
70
+
71
+ app.get("/punchline", async (req, res) => {
72
+ const jokeId = String(req.query.jokeId ?? "");
73
+ const listing = inventory.get(jokeId);
74
+ if (!listing) {
75
+ res.status(404).json({ error: "unknown jokeId — buy a /setup first" });
76
+ return;
77
+ }
78
+
79
+ const config: PaywallConfig = {
80
+ ...CONFIG_BASE,
81
+ amount: String(listing.price),
82
+ tabEndpoint: `${baseUrl}/session`,
83
+ };
84
+ const paywall = createPaywall(verifier, config);
85
+ const decision = await paywall.protect({
86
+ method: req.method,
87
+ url: `${baseUrl}${req.originalUrl}`,
88
+ header: (name) => req.get(name) ?? null,
89
+ });
90
+
91
+ if (!decision.ok) {
92
+ res.status(decision.status).set(decision.headers).json(decision.body);
93
+ return;
94
+ }
95
+
96
+ demand[listing.category] = (demand[listing.category] ?? 0) + 1;
97
+ revenue += listing.price;
98
+ console.log(
99
+ `[comedian] 💰 sold ${listing.category} punchline for ${listing.price} credits (revenue: ${revenue}) — next ${listing.category} will cost ${priceFor(listing.category)}`,
100
+ );
101
+ res
102
+ .set(decision.responseHeaders)
103
+ .json({ punchline: listing.joke.punchline, category: listing.category });
104
+ });
105
+
106
+ app.post("/rating", (req, res) => {
107
+ const category = String(req.body?.category ?? "");
108
+ const score = Number(req.body?.score);
109
+ if (CATEGORIES.includes(category) && Number.isFinite(score)) {
110
+ const rep = rating[category] ?? { sum: 0, n: 0 };
111
+ rep.sum += score;
112
+ rep.n += 1;
113
+ rating[category] = rep;
114
+ console.log(
115
+ `[comedian] 📨 feedback: ${category} rated ${score}/10 (avg ${(rep.sum / rep.n).toFixed(1)}) — adjusting price/quality`,
116
+ );
117
+ }
118
+ res.json({ ok: true });
119
+ });
120
+
121
+ app.post("/session", (_req, res) => {
122
+ res.json({
123
+ userAddress: "0x2222222222222222222222222222222222222222",
124
+ nextReqId: "0x1",
125
+ });
126
+ });
127
+
128
+ function listen(port: number, attemptsLeft = 20) {
129
+ const server = app.listen(port);
130
+ server.once("listening", () => {
131
+ baseUrl = `http://localhost:${port}`;
132
+ writeFileSync(PORT_FILE, baseUrl);
133
+ console.log(`[comedian] 🎭 open for business on ${baseUrl}`);
134
+ console.log(`[comedian] brain: ${brainMode}`);
135
+ console.log(
136
+ `[comedian] GET /setup?theme=&category= is free — punchlines are paywalled via x402`,
137
+ );
138
+ });
139
+ server.once("error", (err: NodeJS.ErrnoException) => {
140
+ if (err.code === "EADDRINUSE" && attemptsLeft > 0) {
141
+ listen(port + 1, attemptsLeft - 1);
142
+ } else {
143
+ throw err;
144
+ }
145
+ });
146
+ }
147
+
148
+ const cleanup = () => rmSync(PORT_FILE, { force: true });
149
+ process.on("exit", cleanup);
150
+ process.on("SIGINT", () => process.exit(0));
151
+ process.on("SIGTERM", () => process.exit(0));
152
+
153
+ listen(Number(process.env.PORT ?? __PORT__));
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": [
7
+ "ES2022",
8
+ "DOM"
9
+ ],
10
+ "strict": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "noEmit": true,
14
+ "types": [
15
+ "node"
16
+ ]
17
+ },
18
+ "include": [
19
+ "src"
20
+ ]
21
+ }
@@ -0,0 +1,12 @@
1
+ # 4Mica runs in DEMO MODE with zero config (mock verifier / demo payment).
2
+ # To go live, swap the mock verifier for createClient() from @4mica/sdk-node
3
+ # and fill these in. Docs: https://4mica.io/docs
4
+
5
+ # 4MICA_WALLET_PRIVATE_KEY=0x...
6
+ # 4MICA_NETWORK=base-sepolia
7
+ # 4MICA_RPC_URL=
8
+ # 4MICA_ETHEREUM_HTTP_RPC_URL=
9
+ # 4MICA_CONTRACT_ADDRESS=
10
+ # 4MICA_ADMIN_API_KEY=
11
+ # 4MICA_BEARER_TOKEN=
12
+ # 4MICA_AUTH_URL=
@@ -0,0 +1,26 @@
1
+ # __PROJECT_NAME__
2
+
3
+ __DESCRIPTION__
4
+
5
+ Generated with `4mica init` — part of the [4Mica](https://4mica.io) x402 payment network.
6
+
7
+ ## Run (demo mode — no config)
8
+
9
+ ```bash
10
+ npm install
11
+ npm run start
12
+ ```
13
+
14
+ Demo mode uses a mock verifier and a locally-built payment header, so it runs
15
+ with **zero credentials**. Start the matching seller first, then run this to pay for the gated route.
16
+
17
+ ## Go live
18
+
19
+ Copy `.env.example` → `.env`, fill in your `4MICA_*` credentials, and swap the
20
+ mock verifier for `createClient()` from `@4mica/sdk-node`.
21
+
22
+ ## Manage agents & transactions
23
+
24
+ ```bash
25
+ 4mica dashboard
26
+ ```
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .env.local
5
+ .next
6
+ *.log