@ar-agents/mercadopago 0.17.1 → 0.18.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.
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Recipe 19 — Forensic-grade compliance dashboard powered by the
3
+ * `/api/play/audit/{sessionId}` endpoint.
4
+ *
5
+ * Every operator running an Argentine sociedad-IA accumulates an
6
+ * append-only HMAC-signed audit log under a sessionId. RFC-001 § 9.2
7
+ * makes that log legally probative — but "legally probative" only
8
+ * matters if a regulator can actually inspect it. This recipe is the
9
+ * compliance-side companion: a Node.js process that ingests audit
10
+ * entries on a schedule, checks for tampering, and routes alerts to
11
+ * the operator's SOC + the contador's monthly summary.
12
+ *
13
+ * # Pattern
14
+ *
15
+ * 1. Pull the latest audit entries via `fetchAudit(sessionId, { verify: true })`
16
+ * from `@ar-agents/incorporate` — same primitives the incorporation flow uses.
17
+ * 2. Reconcile the verified count + tampered count + entry count against
18
+ * expected ranges. Tampering immediately escalates.
19
+ * 3. Bucket entries by tool, governance class, and durationMs to surface
20
+ * operational anomalies (e.g., a `crear_factura` tool started running
21
+ * in 12s instead of <1s — that's an AFIP slowdown worth noting).
22
+ * 4. Stream a daily digest to the contador (Slack, email, WhatsApp)
23
+ * summarizing volume + categories + any anomalies.
24
+ *
25
+ * # When to use
26
+ *
27
+ * - Multi-tenant marketplace operating many sociedades-IA, one audit log
28
+ * per tenant. Daily compliance roll-up scales linearly.
29
+ * - Regulated SaaS where the audit log is contractually required to be
30
+ * monitored, not just retained.
31
+ * - Periodic third-party audit cycles where the auditor wants a
32
+ * reproducible forensic report (the JSON returned by `?verify=1` is
33
+ * already that report).
34
+ *
35
+ * # Edge Runtime
36
+ *
37
+ * Yes. The client is fetch-based, zero deps. Schedule via Vercel Cron
38
+ * (`vercel.json → crons`) or Cloudflare Workers Cron Triggers — either
39
+ * works.
40
+ *
41
+ * # Production-only assertions
42
+ *
43
+ * The audit log is HMAC-signed with `AUDIT_HMAC_SECRET` server-side.
44
+ * The verifier here delegates to `?verify=1` (so the secret never
45
+ * leaves the server) but the consumer can independently re-verify by
46
+ * re-implementing the canonical-JSON + HMAC check. The agent endpoint
47
+ * uses constant-time comparison; the read endpoint exposes per-entry
48
+ * hmac so external libraries can recompute.
49
+ */
50
+
51
+ import { fetchAudit } from "@ar-agents/incorporate";
52
+
53
+ // ─────────────────────────────────────────────────────────────────────────────
54
+ // Types
55
+ // ─────────────────────────────────────────────────────────────────────────────
56
+
57
+ interface AuditEntry {
58
+ id: string;
59
+ sessionId: string;
60
+ ts: string; // ISO 8601
61
+ tool: string;
62
+ governance:
63
+ | "algorithm-only"
64
+ | "audit-logged"
65
+ | "mocked-upstream"
66
+ | "requires-confirmation";
67
+ input: unknown;
68
+ output?: unknown;
69
+ errored?: boolean;
70
+ durationMs?: number;
71
+ hmac: string | null;
72
+ }
73
+
74
+ interface AuditEnvelope {
75
+ sessionId: string;
76
+ backend: "vercel-kv" | "in-memory";
77
+ count: number;
78
+ entries: AuditEntry[];
79
+ verification?: {
80
+ total: number;
81
+ verified: number;
82
+ tampered: number;
83
+ hmacWired: boolean;
84
+ };
85
+ }
86
+
87
+ interface DailyDigest {
88
+ sessionId: string;
89
+ generatedAt: string;
90
+ rangeStart: string;
91
+ rangeEnd: string;
92
+ totals: { all: number; errored: number; byGovernance: Record<string, number> };
93
+ byTool: Record<string, { count: number; avgDurationMs: number; errors: number }>;
94
+ anomalies: string[];
95
+ /** Highest-priority issue. If null, the day is clean. */
96
+ alert: { severity: "tampered" | "performance" | "errors"; message: string } | null;
97
+ }
98
+
99
+ // ─────────────────────────────────────────────────────────────────────────────
100
+ // Core: pull + verify + bucket
101
+ // ─────────────────────────────────────────────────────────────────────────────
102
+
103
+ const PERFORMANCE_THRESHOLDS_MS: Record<string, number> = {
104
+ // Per-tool latency expectations. If an entry's durationMs exceeds 4× this,
105
+ // the operator gets pinged. Numbers are heuristic — replace with your
106
+ // observed p95 from /api/play/audit/* over the last 30 days.
107
+ validate_cuit: 50,
108
+ validate_cbu: 50,
109
+ validate_solicitar_cae: 50,
110
+ validate_igj_inscription: 100,
111
+ lookup_cuit_afip: 1500,
112
+ lookup_credit_situation: 1200,
113
+ get_usd_oficial: 800,
114
+ bo_today: 2000,
115
+ igj_get_entity: 1200,
116
+ list_domicilio_inbox: 1500,
117
+ crear_factura: 3000,
118
+ send_whatsapp_text: 800,
119
+ mp_create_subscription: 1500,
120
+ auto_incorporate: 200,
121
+ };
122
+
123
+ export async function buildDailyDigest(
124
+ sessionId: string,
125
+ options: { rangeStart?: Date; rangeEnd?: Date; baseUrl?: string } = {},
126
+ ): Promise<DailyDigest> {
127
+ const rangeEnd = options.rangeEnd ?? new Date();
128
+ const rangeStart = options.rangeStart ?? new Date(rangeEnd.getTime() - 86_400_000);
129
+
130
+ const raw = (await fetchAudit(sessionId, {
131
+ verify: true,
132
+ baseUrl: options.baseUrl,
133
+ })) as AuditEnvelope;
134
+
135
+ // Bucket entries to the requested range (24h default).
136
+ const inRange = raw.entries.filter((e) => {
137
+ const t = Date.parse(e.ts);
138
+ return t >= rangeStart.getTime() && t < rangeEnd.getTime();
139
+ });
140
+
141
+ const totals = {
142
+ all: inRange.length,
143
+ errored: inRange.filter((e) => e.errored).length,
144
+ byGovernance: groupCount(inRange, (e) => e.governance),
145
+ };
146
+
147
+ const byTool: Record<
148
+ string,
149
+ { count: number; avgDurationMs: number; errors: number }
150
+ > = {};
151
+ for (const e of inRange) {
152
+ const slot = (byTool[e.tool] ??= { count: 0, avgDurationMs: 0, errors: 0 });
153
+ slot.count++;
154
+ if (e.errored) slot.errors++;
155
+ if (typeof e.durationMs === "number") {
156
+ // running mean
157
+ slot.avgDurationMs =
158
+ (slot.avgDurationMs * (slot.count - 1) + e.durationMs) / slot.count;
159
+ }
160
+ }
161
+
162
+ const anomalies: string[] = [];
163
+
164
+ // 1. Tampering — highest-severity escalation.
165
+ const tampered = raw.verification?.tampered ?? 0;
166
+ const hmacWired = raw.verification?.hmacWired ?? false;
167
+ if (!hmacWired) {
168
+ anomalies.push(
169
+ "AUDIT_HMAC_SECRET no está cableado en el deploy — el log no está firmado.",
170
+ );
171
+ }
172
+ if (tampered > 0) {
173
+ anomalies.push(
174
+ `${tampered} entrada${tampered === 1 ? "" : "s"} con tampering detectado en la sesión completa.`,
175
+ );
176
+ }
177
+
178
+ // 2. Performance — flag tools whose avg latency is 4× threshold.
179
+ for (const [tool, slot] of Object.entries(byTool)) {
180
+ const threshold = PERFORMANCE_THRESHOLDS_MS[tool];
181
+ if (threshold && slot.avgDurationMs > threshold * 4 && slot.count > 1) {
182
+ anomalies.push(
183
+ `${tool}: avg ${Math.round(slot.avgDurationMs)}ms (esperado <${threshold * 4}ms) en ${slot.count} llamadas.`,
184
+ );
185
+ }
186
+ }
187
+
188
+ // 3. Error rate — flag tools above 5% error rate over 10+ calls.
189
+ for (const [tool, slot] of Object.entries(byTool)) {
190
+ if (slot.count >= 10 && slot.errors / slot.count > 0.05) {
191
+ anomalies.push(
192
+ `${tool}: ${slot.errors}/${slot.count} (${Math.round((slot.errors / slot.count) * 100)}%) errores.`,
193
+ );
194
+ }
195
+ }
196
+
197
+ // Pick the highest-severity alert.
198
+ let alert: DailyDigest["alert"] = null;
199
+ if (tampered > 0) {
200
+ alert = {
201
+ severity: "tampered",
202
+ message: `URGENTE: ${tampered} entrada(s) con tampering detectado en la sesión ${sessionId}. Investigar acceso al audit log.`,
203
+ };
204
+ } else if (totals.errored / Math.max(totals.all, 1) > 0.1 && totals.all > 10) {
205
+ alert = {
206
+ severity: "errors",
207
+ message: `Tasa de error del día (${totals.errored}/${totals.all}) supera el 10%.`,
208
+ };
209
+ } else if (anomalies.some((a) => a.includes("avg"))) {
210
+ alert = {
211
+ severity: "performance",
212
+ message: anomalies.find((a) => a.includes("avg")) ?? "Performance anomaly",
213
+ };
214
+ }
215
+
216
+ return {
217
+ sessionId,
218
+ generatedAt: new Date().toISOString(),
219
+ rangeStart: rangeStart.toISOString(),
220
+ rangeEnd: rangeEnd.toISOString(),
221
+ totals,
222
+ byTool,
223
+ anomalies,
224
+ alert,
225
+ };
226
+ }
227
+
228
+ function groupCount<T>(arr: T[], key: (v: T) => string): Record<string, number> {
229
+ const out: Record<string, number> = {};
230
+ for (const v of arr) {
231
+ const k = key(v);
232
+ out[k] = (out[k] ?? 0) + 1;
233
+ }
234
+ return out;
235
+ }
236
+
237
+ // ─────────────────────────────────────────────────────────────────────────────
238
+ // Sink: contador's monthly summary
239
+ // ─────────────────────────────────────────────────────────────────────────────
240
+
241
+ /**
242
+ * Render a digest as a readable Spanish-language summary the contador
243
+ * can paste into a monthly compliance report. Structurally similar to
244
+ * a balance summary — totals + observations.
245
+ */
246
+ export function renderForContador(digest: DailyDigest): string {
247
+ const date = digest.rangeEnd.slice(0, 10);
248
+ const lines: string[] = [
249
+ `RESUMEN AUDITORÍA · ${date}`,
250
+ `Sesión: ${digest.sessionId}`,
251
+ "",
252
+ "TOTALES",
253
+ ` Tool calls: ${digest.totals.all}`,
254
+ ` Errores: ${digest.totals.errored}`,
255
+ "",
256
+ "POR CLASE DE GOVERNANCE",
257
+ ];
258
+ for (const [k, v] of Object.entries(digest.totals.byGovernance)) {
259
+ lines.push(` ${k}: ${v}`);
260
+ }
261
+ lines.push("", "POR TOOL");
262
+ const sorted = Object.entries(digest.byTool).sort(
263
+ ([, a], [, b]) => b.count - a.count,
264
+ );
265
+ for (const [tool, slot] of sorted) {
266
+ lines.push(
267
+ ` ${tool}: ${slot.count} llamadas (avg ${Math.round(slot.avgDurationMs)}ms${slot.errors ? `, ${slot.errors} errores` : ""})`,
268
+ );
269
+ }
270
+ if (digest.anomalies.length > 0) {
271
+ lines.push("", "ANOMALÍAS");
272
+ for (const a of digest.anomalies) lines.push(` - ${a}`);
273
+ }
274
+ if (digest.alert) {
275
+ lines.push(
276
+ "",
277
+ `ALERTA [${digest.alert.severity.toUpperCase()}]`,
278
+ ` ${digest.alert.message}`,
279
+ );
280
+ }
281
+ return lines.join("\n");
282
+ }
283
+
284
+ // ─────────────────────────────────────────────────────────────────────────────
285
+ // Cron entrypoint (drop in /api/cron/compliance-digest in your Next.js app)
286
+ // ─────────────────────────────────────────────────────────────────────────────
287
+
288
+ async function main(sessionId: string) {
289
+ const digest = await buildDailyDigest(sessionId);
290
+
291
+ // 1. Always log the structured digest for the operator's metrics pipeline.
292
+ console.log(JSON.stringify(digest));
293
+
294
+ // 2. Render a contador-friendly summary for the monthly compliance report.
295
+ console.log(renderForContador(digest));
296
+
297
+ // 3. Escalate on tampering or high error rate. In production, replace
298
+ // these console.warn calls with WhatsApp template / email / PagerDuty.
299
+ if (digest.alert) {
300
+ console.warn(`ESCALATE [${digest.alert.severity}]: ${digest.alert.message}`);
301
+ // await sendWhatsAppTemplate({ to: process.env.SOC_WHATSAPP, template: "audit_alert", ... });
302
+ // await sendEmail({ to: process.env.CONTADOR_EMAIL, subject: "...", body: ... });
303
+ }
304
+
305
+ return digest;
306
+ }
307
+
308
+ if (typeof require !== "undefined" && require.main === module) {
309
+ const sid = process.argv[2];
310
+ if (!sid) {
311
+ console.error("usage: pnpm tsx 19-forensic-compliance-dashboard.ts <sessionId>");
312
+ process.exit(1);
313
+ }
314
+ main(sid).catch((err) => {
315
+ console.error(err);
316
+ process.exit(1);
317
+ });
318
+ }
319
+
320
+ export { main };
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Recipe 20 — Multi-tenant marketplace spawning vendor sociedades-IA.
3
+ *
4
+ * # Pattern
5
+ *
6
+ * A platform (think: Tienda Nube, Mercado Shops, an SaaS marketplace)
7
+ * onboards vendors who each need a thin AR-side legal entity to operate
8
+ * within Argentina's jurisdiction. Pre-sociedad-IA, this was a manual
9
+ * escribono job per vendor. With `@ar-agents/incorporate`, the platform
10
+ * can spawn a fresh sociedad-IA spec on-demand at vendor sign-up — one
11
+ * API call, the vendor's deploy URL is materialized, the platform
12
+ * tracks the audit log per tenant.
13
+ *
14
+ * # When to use
15
+ *
16
+ * - Vertical SaaS where each vendor needs their own AR fiscal identity
17
+ * (factura emission under their CUIT, not the platform's).
18
+ * - Marketplaces that scale to hundreds of vendors and can't manually
19
+ * coordinate escribano + contador per spawn.
20
+ * - Cross-jurisdictional plays: a USA platform onboarding AR sellers who
21
+ * need a properly-incorporated sociedad to receive Mercado Pago.
22
+ *
23
+ * # Architecture
24
+ *
25
+ * Platform /api/auto-incorporate
26
+ * ┌──────────┐ POST ┌────────────────┐
27
+ * │ vendor │ ─────────────▶ │ ar-agents │
28
+ * │ signup │ with vendor │ generates spec │
29
+ * │ flow │ details + a │ + signs audit │
30
+ * └──────────┘ per-vendor │ entry under │
31
+ * │ sessionId │ vendor's id │
32
+ * ▼ └────────────────┘
33
+ * ┌──────────┐ │
34
+ * │ Vercel │ one-click deploy URL ◀───┘
35
+ * │ deploy │
36
+ * └──────────┘
37
+ * │
38
+ * ▼
39
+ * ┌──────────┐ GET /api/play/audit/{tenantId}?verify=1
40
+ * │ ops │ ◀──────────────────────────────────────┐
41
+ * │ dash │ │
42
+ * └──────────┘ │
43
+ * │ │
44
+ * ▼ (recipe 19 cron) │
45
+ * compliance digest per tenant ──────────────────────┘
46
+ *
47
+ * # Idempotency
48
+ *
49
+ * Calling /api/auto-incorporate with the same input + sessionId twice
50
+ * is idempotent on the spec output but writes two audit entries (one per
51
+ * call). For exact-once semantics, dedupe on the platform side keyed by
52
+ * (vendor_id, sociedad_denominacion). The audit log will show both
53
+ * attempts which is the more honest signal anyway.
54
+ *
55
+ * # Edge Runtime
56
+ *
57
+ * Yes — the @ar-agents/incorporate client is fetch-only.
58
+ */
59
+
60
+ import {
61
+ incorporate,
62
+ fetchAudit,
63
+ type IncorporateInput,
64
+ type IncorporateSuccess,
65
+ } from "@ar-agents/incorporate";
66
+
67
+ // ─────────────────────────────────────────────────────────────────────────────
68
+ // Platform's vendor model
69
+ // ─────────────────────────────────────────────────────────────────────────────
70
+
71
+ interface Vendor {
72
+ id: string; // platform's internal vendor id
73
+ legalName: string;
74
+ representanteName: string;
75
+ representanteCuit: string;
76
+ contactEmail: string;
77
+ /** What the vendor sells. Used as the seed for objeto social. */
78
+ category: "software" | "services" | "products" | "ecommerce";
79
+ expectedMonthlyRevenueArs: number;
80
+ }
81
+
82
+ // ─────────────────────────────────────────────────────────────────────────────
83
+ // Spawn a sociedad-IA for a vendor at sign-up
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+
86
+ const OBJETO_BY_CATEGORY: Record<Vendor["category"], string> = {
87
+ software:
88
+ "Desarrollo y comercialización de software propio para empresas y consumidores en Argentina, incluyendo pero no limitándose a aplicaciones web, móviles y servicios cloud.",
89
+ services:
90
+ "Prestación de servicios profesionales digitales (consultoría, desarrollo, marketing, atención al cliente) a empresas y consumidores en territorio argentino.",
91
+ products:
92
+ "Comercialización mayorista y minorista de productos físicos y digitales propios y de terceros, incluyendo importación, almacenamiento, marketing y logística en Argentina.",
93
+ ecommerce:
94
+ "Operación de tiendas online y marketplaces para la venta de productos y servicios al consumidor final argentino, incluyendo procesamiento de pagos y logística.",
95
+ };
96
+
97
+ /**
98
+ * The platform's tenantId for this vendor's sociedad-IA. Becomes the
99
+ * sessionId for the entire forensic timeline (incorporation + ongoing
100
+ * tool calls). Pick something stable + opaque — UUID v4 or a hashed
101
+ * concatenation of vendor_id + a per-platform secret.
102
+ */
103
+ function tenantSessionIdFor(vendor: Vendor): string {
104
+ // For the cookbook, derive deterministically. In production, prefer
105
+ // crypto.randomUUID() per tenant to avoid leaking enumerable ids.
106
+ return `tenant-${vendor.id.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 56)}`;
107
+ }
108
+
109
+ export async function spawnVendorSociedad(vendor: Vendor): Promise<IncorporateSuccess> {
110
+ const sessionId = tenantSessionIdFor(vendor);
111
+
112
+ // Pieza selection driven by vendor profile.
113
+ const piezas: IncorporateInput["piezas"] = [
114
+ "identity",
115
+ "gde-tad",
116
+ "mercadopago",
117
+ "banking",
118
+ "facturacion",
119
+ "boletin-oficial",
120
+ "igj",
121
+ ];
122
+ if (vendor.category === "ecommerce" || vendor.category === "products") {
123
+ piezas.push("shipping");
124
+ }
125
+ if (vendor.expectedMonthlyRevenueArs > 1_000_000) {
126
+ // Large-revenue tenants need WhatsApp customer-comms + ACP for LLM-buyer
127
+ // checkout. Smaller tenants can stay lean.
128
+ piezas.push("whatsapp", "agentic-commerce-bridge", "ap2");
129
+ }
130
+
131
+ // Capital social: SAS minimum is 100k. Bump for large-revenue vendors so
132
+ // BCRA and downstream banks don't flag the disparity later.
133
+ const capitalSocial =
134
+ vendor.expectedMonthlyRevenueArs > 1_000_000 ? 500_000 : 200_000;
135
+
136
+ const input: IncorporateInput = {
137
+ denominacion: `${vendor.legalName} SAS`,
138
+ tipo: "SAS", // SOCIEDAD-IA when the regime ships
139
+ capitalSocial,
140
+ objeto: OBJETO_BY_CATEGORY[vendor.category],
141
+ representante: {
142
+ nombre: vendor.representanteName,
143
+ cuit: vendor.representanteCuit,
144
+ },
145
+ emailContacto: vendor.contactEmail,
146
+ piezas,
147
+ sessionId,
148
+ };
149
+
150
+ const result = await incorporate(input);
151
+
152
+ if (!result.ok) {
153
+ // Pre-flight failure — surface the findings + reject the signup.
154
+ const errors = result.validation.findings
155
+ .filter((f) => f.severity === "error")
156
+ .map((f) => `${f.field}: ${f.message}`);
157
+ throw new Error(
158
+ `Vendor signup rejected at IGJ pre-flight: ${errors.join("; ")}`,
159
+ );
160
+ }
161
+ return result;
162
+ }
163
+
164
+ // ─────────────────────────────────────────────────────────────────────────────
165
+ // Platform's spawn flow: handle a fresh signup
166
+ // ─────────────────────────────────────────────────────────────────────────────
167
+
168
+ interface SignupRecord {
169
+ vendorId: string;
170
+ tenantSessionId: string;
171
+ slug: string;
172
+ deployUrl: string;
173
+ auditDashboardUrl: string;
174
+ auditVerifyUrl: string;
175
+ badgeSvgUrl: string;
176
+ createdAt: string;
177
+ }
178
+
179
+ /**
180
+ * Persist this in the platform's vendor table (Postgres / KV / whatever).
181
+ * The badge SVG URL is what you embed in the vendor's profile page so any
182
+ * visitor sees their "verified · N/M" forensic-clean status.
183
+ */
184
+ function recordFor(
185
+ vendor: Vendor,
186
+ result: IncorporateSuccess,
187
+ ): SignupRecord {
188
+ return {
189
+ vendorId: vendor.id,
190
+ tenantSessionId: result.audit.sessionId,
191
+ slug: result.sociedad.slug,
192
+ deployUrl: result.deploy.oneClickUrl,
193
+ auditDashboardUrl: result.audit.dashboardUrl,
194
+ auditVerifyUrl: result.audit.verifyUrl,
195
+ badgeSvgUrl: `https://ar-agents.ar/api/badge/${result.audit.sessionId}`,
196
+ createdAt: new Date().toISOString(),
197
+ };
198
+ }
199
+
200
+ // ─────────────────────────────────────────────────────────────────────────────
201
+ // Periodic compliance digest per tenant (recipe 19 in a fan-out loop)
202
+ // ─────────────────────────────────────────────────────────────────────────────
203
+
204
+ interface TenantHealth {
205
+ vendorId: string;
206
+ totalEvents: number;
207
+ tampered: number;
208
+ hmacWired: boolean;
209
+ alert: string | null;
210
+ }
211
+
212
+ export async function platformComplianceSweep(
213
+ records: SignupRecord[],
214
+ ): Promise<TenantHealth[]> {
215
+ const results: TenantHealth[] = [];
216
+ for (const r of records) {
217
+ const audit = (await fetchAudit(r.tenantSessionId, { verify: true })) as {
218
+ count: number;
219
+ verification?: { tampered: number; verified: number; total: number; hmacWired: boolean };
220
+ };
221
+ const v = audit.verification ?? { tampered: 0, verified: 0, total: 0, hmacWired: false };
222
+ let alert: string | null = null;
223
+ if (!v.hmacWired) alert = "HMAC not wired in deploy — log unverified.";
224
+ else if (v.tampered > 0)
225
+ alert = `Tampering detected on tenant ${r.vendorId}: ${v.tampered} entries.`;
226
+ results.push({
227
+ vendorId: r.vendorId,
228
+ totalEvents: audit.count,
229
+ tampered: v.tampered,
230
+ hmacWired: v.hmacWired,
231
+ alert,
232
+ });
233
+ }
234
+ return results;
235
+ }
236
+
237
+ // ─────────────────────────────────────────────────────────────────────────────
238
+ // Example: signup + initial audit verify
239
+ // ─────────────────────────────────────────────────────────────────────────────
240
+
241
+ async function main() {
242
+ // 1. New vendor signs up on the platform.
243
+ const vendor: Vendor = {
244
+ id: "v_42",
245
+ legalName: "Berreta Software",
246
+ representanteName: "Pérez, Juan",
247
+ representanteCuit: "20-12345678-9",
248
+ contactEmail: "ops@berreta.example",
249
+ category: "software",
250
+ expectedMonthlyRevenueArs: 800_000,
251
+ };
252
+
253
+ // 2. Spawn the sociedad-IA spec.
254
+ const result = await spawnVendorSociedad(vendor);
255
+ const record = recordFor(vendor, result);
256
+
257
+ console.log("Vendor onboarded:", record);
258
+
259
+ // 3. Verify the signup audit entry was actually written + signed.
260
+ const sweep = await platformComplianceSweep([record]);
261
+ console.log("Initial compliance sweep:", sweep);
262
+
263
+ // 4. Embed the badge in the vendor's profile page in the platform UI.
264
+ console.log("Badge URL for vendor profile:", record.badgeSvgUrl);
265
+ }
266
+
267
+ if (typeof require !== "undefined" && require.main === module) {
268
+ main().catch((err) => {
269
+ console.error("Recipe 20 failed:", err);
270
+ process.exit(1);
271
+ });
272
+ }
273
+
274
+ export { main };