thevoidforge 21.0.10 → 21.0.12

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 (108) hide show
  1. package/dist/.claude/commands/ai.md +69 -0
  2. package/dist/.claude/commands/architect.md +121 -0
  3. package/dist/.claude/commands/assemble.md +201 -0
  4. package/dist/.claude/commands/assess.md +75 -0
  5. package/dist/.claude/commands/blueprint.md +135 -0
  6. package/dist/.claude/commands/build.md +116 -0
  7. package/dist/.claude/commands/campaign.md +201 -0
  8. package/dist/.claude/commands/cultivation.md +166 -0
  9. package/dist/.claude/commands/current.md +128 -0
  10. package/dist/.claude/commands/dangerroom.md +74 -0
  11. package/dist/.claude/commands/debrief.md +178 -0
  12. package/dist/.claude/commands/deploy.md +99 -0
  13. package/dist/.claude/commands/devops.md +143 -0
  14. package/dist/.claude/commands/gauntlet.md +140 -0
  15. package/dist/.claude/commands/git.md +104 -0
  16. package/dist/.claude/commands/grow.md +146 -0
  17. package/dist/.claude/commands/imagine.md +126 -0
  18. package/dist/.claude/commands/portfolio.md +50 -0
  19. package/dist/.claude/commands/prd.md +113 -0
  20. package/dist/.claude/commands/qa.md +107 -0
  21. package/dist/.claude/commands/review.md +151 -0
  22. package/dist/.claude/commands/security.md +100 -0
  23. package/dist/.claude/commands/test.md +96 -0
  24. package/dist/.claude/commands/thumper.md +116 -0
  25. package/dist/.claude/commands/treasury.md +100 -0
  26. package/dist/.claude/commands/ux.md +118 -0
  27. package/dist/.claude/commands/vault.md +189 -0
  28. package/dist/.claude/commands/void.md +108 -0
  29. package/dist/CHANGELOG.md +1918 -0
  30. package/dist/CLAUDE.md +250 -0
  31. package/dist/HOLOCRON.md +856 -0
  32. package/dist/VERSION.md +123 -0
  33. package/dist/docs/NAMING_REGISTRY.md +478 -0
  34. package/dist/docs/methods/AI_INTELLIGENCE.md +276 -0
  35. package/dist/docs/methods/ASSEMBLER.md +142 -0
  36. package/dist/docs/methods/BACKEND_ENGINEER.md +165 -0
  37. package/dist/docs/methods/BUILD_JOURNAL.md +185 -0
  38. package/dist/docs/methods/BUILD_PROTOCOL.md +426 -0
  39. package/dist/docs/methods/CAMPAIGN.md +568 -0
  40. package/dist/docs/methods/CONTEXT_MANAGEMENT.md +189 -0
  41. package/dist/docs/methods/DEEP_CURRENT.md +184 -0
  42. package/dist/docs/methods/DEVOPS_ENGINEER.md +295 -0
  43. package/dist/docs/methods/FIELD_MEDIC.md +261 -0
  44. package/dist/docs/methods/FORGE_ARTIST.md +108 -0
  45. package/dist/docs/methods/FORGE_KEEPER.md +268 -0
  46. package/dist/docs/methods/GAUNTLET.md +344 -0
  47. package/dist/docs/methods/GROWTH_STRATEGIST.md +466 -0
  48. package/dist/docs/methods/HEARTBEAT.md +168 -0
  49. package/dist/docs/methods/MCP_INTEGRATION.md +139 -0
  50. package/dist/docs/methods/MUSTER.md +148 -0
  51. package/dist/docs/methods/PRD_GENERATOR.md +186 -0
  52. package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +250 -0
  53. package/dist/docs/methods/QA_ENGINEER.md +337 -0
  54. package/dist/docs/methods/RELEASE_MANAGER.md +145 -0
  55. package/dist/docs/methods/SECURITY_AUDITOR.md +320 -0
  56. package/dist/docs/methods/SUB_AGENTS.md +335 -0
  57. package/dist/docs/methods/SYSTEMS_ARCHITECT.md +171 -0
  58. package/dist/docs/methods/TESTING.md +359 -0
  59. package/dist/docs/methods/THUMPER.md +175 -0
  60. package/dist/docs/methods/TIME_VAULT.md +120 -0
  61. package/dist/docs/methods/TREASURY.md +184 -0
  62. package/dist/docs/methods/TROUBLESHOOTING.md +265 -0
  63. package/dist/docs/patterns/README.md +52 -0
  64. package/dist/docs/patterns/ad-billing-adapter.ts +537 -0
  65. package/dist/docs/patterns/ad-platform-adapter.ts +421 -0
  66. package/dist/docs/patterns/ai-classifier.ts +195 -0
  67. package/dist/docs/patterns/ai-eval.ts +272 -0
  68. package/dist/docs/patterns/ai-orchestrator.ts +341 -0
  69. package/dist/docs/patterns/ai-router.ts +194 -0
  70. package/dist/docs/patterns/ai-tool-schema.ts +237 -0
  71. package/dist/docs/patterns/api-route.ts +241 -0
  72. package/dist/docs/patterns/backtest-engine.ts +499 -0
  73. package/dist/docs/patterns/browser-review.ts +292 -0
  74. package/dist/docs/patterns/combobox.tsx +300 -0
  75. package/dist/docs/patterns/component.tsx +262 -0
  76. package/dist/docs/patterns/daemon-process.ts +338 -0
  77. package/dist/docs/patterns/data-pipeline.ts +297 -0
  78. package/dist/docs/patterns/database-migration.ts +466 -0
  79. package/dist/docs/patterns/e2e-test.ts +629 -0
  80. package/dist/docs/patterns/error-handling.ts +312 -0
  81. package/dist/docs/patterns/execution-safety.ts +601 -0
  82. package/dist/docs/patterns/financial-transaction.ts +342 -0
  83. package/dist/docs/patterns/funding-plan.ts +462 -0
  84. package/dist/docs/patterns/game-entity.ts +137 -0
  85. package/dist/docs/patterns/game-loop.ts +113 -0
  86. package/dist/docs/patterns/game-state.ts +143 -0
  87. package/dist/docs/patterns/job-queue.ts +225 -0
  88. package/dist/docs/patterns/kongo-integration.ts +164 -0
  89. package/dist/docs/patterns/middleware.ts +363 -0
  90. package/dist/docs/patterns/mobile-screen.tsx +139 -0
  91. package/dist/docs/patterns/mobile-service.ts +167 -0
  92. package/dist/docs/patterns/multi-tenant.ts +382 -0
  93. package/dist/docs/patterns/oauth-token-lifecycle.ts +223 -0
  94. package/dist/docs/patterns/outbound-rate-limiter.ts +260 -0
  95. package/dist/docs/patterns/prompt-template.ts +195 -0
  96. package/dist/docs/patterns/revenue-source-adapter.ts +311 -0
  97. package/dist/docs/patterns/service.ts +224 -0
  98. package/dist/docs/patterns/sse-endpoint.ts +118 -0
  99. package/dist/docs/patterns/stablecoin-adapter.ts +511 -0
  100. package/dist/docs/patterns/third-party-script.ts +68 -0
  101. package/dist/scripts/thumper/gom-jabbar.sh +241 -0
  102. package/dist/scripts/thumper/relay.sh +610 -0
  103. package/dist/scripts/thumper/scan.sh +359 -0
  104. package/dist/scripts/thumper/thumper.sh +190 -0
  105. package/dist/scripts/thumper/water-rings.sh +76 -0
  106. package/dist/scripts/voidforge.js +1 -1
  107. package/package.json +1 -1
  108. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,511 @@
1
+ /**
2
+ * Pattern: Stablecoin Treasury Adapter (Split Interface)
3
+ *
4
+ * Key principles:
5
+ * - Split interface: StablecoinSetup (interactive CLI/Danger Room) + StablecoinAdapter (daemon runtime)
6
+ * - Single-writer constraint: only Heartbeat daemon calls the runtime adapter
7
+ * - Funding lifecycle: USDC held at provider → off-ramp instruction → fiat settlement at linked bank
8
+ * - All amounts in branded integer cents (Cents type from financial-transaction.ts)
9
+ * - Hash-chained TransferRecord for tamper-evident audit trail
10
+ * - Idempotency keys on all write operations (off-ramp, cancel)
11
+ * - Provider abstraction: Circle first-class, Bridge secondary, manual fallback
12
+ * - The adapter does NOT move money between bank and ad platform — that is ad-billing-adapter.ts
13
+ *
14
+ * Agents: Dockson (treasury), Heartbeat daemon
15
+ *
16
+ * PRD Reference: §11.1A, §12.1, §12.4, §12.5
17
+ *
18
+ * Circle API reference (v1):
19
+ * Auth: Bearer token via API key — header `Authorization: Bearer {apiKey}`
20
+ * Base URL: https://api.circle.com/v1
21
+ * GET /wallets → list wallets
22
+ * GET /wallets/{id}/balances → wallet balances (amount, currency, chain)
23
+ * GET /configuration → supported chains and assets
24
+ * GET /banks/wires → linked bank accounts
25
+ * POST /payouts → initiate wire payout (off-ramp)
26
+ * GET /payouts/{id} → transfer status
27
+ * GET /payouts?destination={bankId} → list completed payouts for reconciliation
28
+ *
29
+ * Response shape (balance):
30
+ * { data: { available: [{ amount: "1000.00", currency: "USD" }] } }
31
+ * Response shape (payout):
32
+ * { data: { id, status, amount: { amount, currency }, destination, createDate, updateDate } }
33
+ * Payout statuses: pending → processing → complete | failed
34
+ *
35
+ * Bridge API reference:
36
+ * Auth: API key header `Api-Key: {apiKey}`
37
+ * Base URL: https://api.bridge.xyz/v0
38
+ * POST /customers/{id}/liquidation_addresses → create liquidation address
39
+ * GET /customers/{id}/liquidation_addresses → list addresses
40
+ * GET /liquidation_addresses/{id} → transfer status
41
+ * Transfers settle via ACH to linked bank — status polling required
42
+ */
43
+
44
+ import { createHash } from 'node:crypto';
45
+
46
+ // ── Branded Financial Types (from financial-transaction.ts) ──
47
+
48
+ type Cents = number & { readonly __brand: 'Cents' };
49
+
50
+ function toCents(dollars: number): Cents {
51
+ return Math.round(dollars * 100) as Cents;
52
+ }
53
+
54
+ function toDollars(cents: Cents): number {
55
+ return cents / 100;
56
+ }
57
+
58
+ // ── Provider and Asset Types ─────────────────────────
59
+
60
+ type StablecoinProvider = 'circle' | 'bridge';
61
+
62
+ interface SupportedAsset {
63
+ asset: string; // e.g., 'USDC'
64
+ network: string; // e.g., 'ETH', 'SOL', 'MATIC', 'AVAX'
65
+ contractAddress: string;
66
+ minRedemption: Cents; // provider-enforced minimum off-ramp amount
67
+ }
68
+
69
+ interface ProviderCredentials {
70
+ provider: StablecoinProvider;
71
+ apiKey: string;
72
+ environment: 'sandbox' | 'production';
73
+ }
74
+
75
+ // ── Balance Types ────────────────────────────────────
76
+
77
+ interface StablecoinBalance {
78
+ provider: StablecoinProvider;
79
+ asset: string;
80
+ network: string;
81
+ balanceCents: Cents; // stablecoin value expressed in integer cents (1 USDC = 100 cents)
82
+ lastUpdated: string; // ISO 8601
83
+ }
84
+
85
+ interface FiatBalance {
86
+ provider: 'mercury' | 'external';
87
+ accountId: string;
88
+ availableCents: Cents;
89
+ pendingCents: Cents;
90
+ currency: 'USD';
91
+ lastUpdated: string;
92
+ }
93
+
94
+ interface CombinedBalances {
95
+ stablecoin: StablecoinBalance[];
96
+ fiat: FiatBalance[];
97
+ totalStablecoinCents: Cents;
98
+ totalFiatAvailableCents: Cents;
99
+ }
100
+
101
+ // ── Off-ramp Types ───────────────────────────────────
102
+
103
+ interface OfframpQuote {
104
+ provider: StablecoinProvider;
105
+ sourceAsset: string;
106
+ sourceNetwork: string;
107
+ requestedCents: Cents;
108
+ estimatedFeeCents: Cents;
109
+ estimatedNetCents: Cents; // requestedCents - estimatedFeeCents
110
+ estimatedSettlementMinutes: number;
111
+ expiresAt: string; // ISO 8601 — quote validity window
112
+ quoteId?: string; // provider-issued quote ID if applicable
113
+ }
114
+
115
+ type TransferStatus =
116
+ | 'pending' // instruction created, not yet submitted to provider
117
+ | 'processing' // provider accepted, settlement in progress
118
+ | 'completed' // fiat arrived at destination bank
119
+ | 'failed' // provider reported failure
120
+ | 'cancelled'; // cancelled before completion
121
+
122
+ interface TransferRecord {
123
+ id: string; // UUID v4
124
+ fundingPlanId: string; // links to FundingPlan in funding-plan.ts
125
+ providerTransferId: string; // provider's external ID
126
+ bankTransactionId?: string; // bank-side reference once settled
127
+ provider: StablecoinProvider;
128
+ direction: 'crypto_to_fiat';
129
+ sourceAsset: string;
130
+ sourceNetwork: string;
131
+ amountCents: Cents;
132
+ feesCents: Cents;
133
+ netAmountCents: Cents;
134
+ destinationBankId: string;
135
+ status: TransferStatus;
136
+ statusReason?: string; // failure/cancellation reason
137
+ initiatedAt: string; // ISO 8601
138
+ completedAt?: string; // ISO 8601
139
+ idempotencyKey: string; // UUID, prevents duplicate off-ramps
140
+ // Hash chain fields (per financial-transaction.ts pattern)
141
+ previousHash: string;
142
+ hash: string;
143
+ }
144
+
145
+ // ── Transfer Status Detail ──────────────────────────
146
+
147
+ interface TransferStatusDetail {
148
+ transferId: string;
149
+ providerTransferId: string;
150
+ status: TransferStatus;
151
+ amountCents: Cents;
152
+ feesCents: Cents;
153
+ initiatedAt: string;
154
+ completedAt?: string;
155
+ estimatedCompletionAt?: string;
156
+ providerRawStatus: string; // raw status string from provider for debugging
157
+ }
158
+
159
+ // ── Date Range ──────────────────────────────────────
160
+
161
+ interface DateRange {
162
+ start: string; // ISO 8601
163
+ end: string; // ISO 8601
164
+ }
165
+
166
+ // ── Interactive Setup Interface ─────────────────────
167
+ // Runs in CLI or Danger Room — requires user interaction.
168
+ // Called during `/cultivation install` when Stablecoin Treasury is selected.
169
+
170
+ interface StablecoinSetup {
171
+ /** Verify provider API key is valid and has required permissions */
172
+ authenticate(credentials: ProviderCredentials): Promise<{
173
+ valid: boolean;
174
+ accountId?: string;
175
+ permissions?: string[];
176
+ error?: string;
177
+ }>;
178
+
179
+ /** List stablecoins and networks the provider supports for off-ramp */
180
+ verifySupportedAssets(credentials: ProviderCredentials): Promise<SupportedAsset[]>;
181
+
182
+ /** Confirm the provider has a linked bank destination for wire/ACH payouts */
183
+ verifyLinkedBank(credentials: ProviderCredentials): Promise<{
184
+ linked: boolean;
185
+ bankId?: string;
186
+ bankName?: string;
187
+ accountLast4?: string;
188
+ error?: string;
189
+ }>;
190
+
191
+ /** Fetch current stablecoin balances for initial state snapshot */
192
+ getInitialBalances(credentials: ProviderCredentials): Promise<StablecoinBalance[]>;
193
+ }
194
+
195
+ // ── Runtime Adapter Interface ───────────────────────
196
+ // Runs in heartbeat daemon ONLY — non-interactive, autonomous.
197
+ // Single-writer: no other process may call these methods.
198
+
199
+ interface StablecoinAdapter {
200
+ /** Current stablecoin + fiat balances across all connected accounts */
201
+ getBalances(): Promise<CombinedBalances>;
202
+
203
+ /** Estimate fees, settlement time, and net proceeds for an off-ramp */
204
+ quoteRedemption(amountCents: Cents): Promise<OfframpQuote>;
205
+
206
+ /**
207
+ * Create an off-ramp instruction with the provider.
208
+ * Requires an idempotencyKey on the FundingPlan to prevent duplicate transfers.
209
+ * Returns the created TransferRecord (with hash chain linking).
210
+ */
211
+ initiateOfframp(plan: FundingPlanRef, previousHash: string): Promise<TransferRecord>;
212
+
213
+ /** Poll provider for transfer status updates */
214
+ getTransferStatus(transferId: string): Promise<TransferStatusDetail>;
215
+
216
+ /** Cancel a pending transfer if the provider supports cancellation */
217
+ cancelTransfer(transferId: string): Promise<{
218
+ cancelled: boolean;
219
+ reason?: string;
220
+ }>;
221
+
222
+ /** List completed transfers in a date range for reconciliation */
223
+ listCompletedTransfers(dateRange: DateRange): Promise<TransferRecord[]>;
224
+ }
225
+
226
+ // ── Funding Plan Reference ──────────────────────────
227
+ // Minimal reference passed from funding-plan.ts to avoid circular dependency.
228
+ // The full FundingPlan type lives in funding-plan.ts.
229
+
230
+ interface FundingPlanRef {
231
+ id: string;
232
+ sourceFundingId: string;
233
+ destinationBankId: string;
234
+ requiredCents: Cents;
235
+ idempotencyKey: string;
236
+ }
237
+
238
+ // ── Hash Chain Helper ───────────────────────────────
239
+
240
+ function computeTransferHash(record: Omit<TransferRecord, 'hash'>, previousHash: string): string {
241
+ const payload = JSON.stringify({
242
+ id: record.id,
243
+ fundingPlanId: record.fundingPlanId,
244
+ providerTransferId: record.providerTransferId,
245
+ amountCents: record.amountCents,
246
+ feesCents: record.feesCents,
247
+ status: record.status,
248
+ initiatedAt: record.initiatedAt,
249
+ }) + previousHash;
250
+ return createHash('sha256').update(payload).digest('hex');
251
+ }
252
+
253
+ // ── Reference Implementation Sketch: Circle ─────────
254
+ // Production implementation would live in wizard/lib/financial/stablecoin/circle.ts
255
+
256
+ class CircleAdapter implements StablecoinSetup, StablecoinAdapter {
257
+ private readonly baseUrl = 'https://api.circle.com/v1';
258
+ private apiKey: string = '';
259
+ private bankId: string = '';
260
+
261
+ constructor(private config: { apiKey: string; bankId: string; environment: 'sandbox' | 'production' }) {
262
+ this.apiKey = config.apiKey;
263
+ this.bankId = config.bankId;
264
+ if (config.environment === 'sandbox') {
265
+ // Circle sandbox base URL is the same but uses sandbox API keys
266
+ }
267
+ }
268
+
269
+ // ── Setup (interactive) ──────────
270
+
271
+ async authenticate(credentials: ProviderCredentials): Promise<{
272
+ valid: boolean; accountId?: string; permissions?: string[]; error?: string;
273
+ }> {
274
+ // GET /configuration — verifies API key works
275
+ // Circle returns { data: { payments: { masterWalletId } } }
276
+ const res = await this.apiCall('GET', '/configuration');
277
+ const data = res.data as Record<string, unknown> | undefined;
278
+ const payments = data?.payments as Record<string, unknown> | undefined;
279
+ return {
280
+ valid: true,
281
+ accountId: payments?.masterWalletId as string | undefined,
282
+ };
283
+ }
284
+
285
+ async verifySupportedAssets(_credentials: ProviderCredentials): Promise<SupportedAsset[]> {
286
+ // Circle supports USDC on ETH, SOL, MATIC, AVAX, etc.
287
+ // GET /configuration → data.payments.supportedCurrencies
288
+ // Hardcoded for pattern — real implementation queries the API
289
+ return [
290
+ { asset: 'USDC', network: 'ETH', contractAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', minRedemption: toCents(100) },
291
+ { asset: 'USDC', network: 'SOL', contractAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', minRedemption: toCents(100) },
292
+ { asset: 'USDC', network: 'MATIC', contractAddress: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359', minRedemption: toCents(100) },
293
+ ];
294
+ }
295
+
296
+ async verifyLinkedBank(_credentials: ProviderCredentials): Promise<{
297
+ linked: boolean; bankId?: string; bankName?: string; accountLast4?: string; error?: string;
298
+ }> {
299
+ // GET /banks/wires — lists linked wire destinations
300
+ // Response: { data: [{ id, description, trackingRef, bankAddress, billingDetails, status }] }
301
+ const res = await this.apiCall('GET', '/banks/wires');
302
+ const banks = res.data as Array<Record<string, unknown>> | undefined;
303
+ if (!banks || banks.length === 0) {
304
+ return { linked: false, error: 'No linked bank accounts found in Circle' };
305
+ }
306
+ const primary = banks[0];
307
+ return {
308
+ linked: true,
309
+ bankId: primary.id as string,
310
+ bankName: (primary.billingDetails as Record<string, unknown>)?.name as string | undefined,
311
+ accountLast4: primary.trackingRef
312
+ ? (primary.trackingRef as string).slice(-4)
313
+ : undefined,
314
+ };
315
+ }
316
+
317
+ async getInitialBalances(_credentials: ProviderCredentials): Promise<StablecoinBalance[]> {
318
+ return this.fetchStablecoinBalances();
319
+ }
320
+
321
+ // ── Runtime (daemon) ──────────
322
+
323
+ async getBalances(): Promise<CombinedBalances> {
324
+ const stablecoin = await this.fetchStablecoinBalances();
325
+ const totalStablecoinCents = stablecoin.reduce(
326
+ (sum, b) => (sum + b.balanceCents) as Cents, 0 as Cents
327
+ );
328
+ // Fiat balances come from the bank adapter, not the stablecoin provider.
329
+ // The caller (Treasury Planner) combines stablecoin + bank balances.
330
+ return {
331
+ stablecoin,
332
+ fiat: [],
333
+ totalStablecoinCents,
334
+ totalFiatAvailableCents: 0 as Cents,
335
+ };
336
+ }
337
+
338
+ async quoteRedemption(amountCents: Cents): Promise<OfframpQuote> {
339
+ // Circle payout fees vary by destination and amount.
340
+ // For wire payouts: typically $25 flat fee.
341
+ // Settlement: 1-2 business days for domestic wire.
342
+ const feeCents = toCents(25);
343
+ return {
344
+ provider: 'circle',
345
+ sourceAsset: 'USDC',
346
+ sourceNetwork: 'ETH',
347
+ requestedCents: amountCents,
348
+ estimatedFeeCents: feeCents,
349
+ estimatedNetCents: (amountCents - feeCents) as Cents,
350
+ estimatedSettlementMinutes: 24 * 60, // 1 business day estimate
351
+ expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
352
+ };
353
+ }
354
+
355
+ async initiateOfframp(plan: FundingPlanRef, previousHash: string): Promise<TransferRecord> {
356
+ // POST /payouts
357
+ // Body: { idempotencyKey, source: { type: "wallet", id: "master" },
358
+ // destination: { type: "wire", id: bankId },
359
+ // amount: { amount: "1000.00", currency: "USD" },
360
+ // metadata: { beneficiaryEmail: "..." } }
361
+ const res = await this.apiCall('POST', '/payouts', {
362
+ idempotencyKey: plan.idempotencyKey,
363
+ source: { type: 'wallet', id: 'master' },
364
+ destination: { type: 'wire', id: this.bankId },
365
+ amount: { amount: toDollars(plan.requiredCents).toFixed(2), currency: 'USD' },
366
+ });
367
+
368
+ const payout = res.data as Record<string, unknown>;
369
+ const now = new Date().toISOString();
370
+ const id = crypto.randomUUID();
371
+
372
+ const record: Omit<TransferRecord, 'hash'> = {
373
+ id,
374
+ fundingPlanId: plan.id,
375
+ providerTransferId: payout.id as string,
376
+ provider: 'circle',
377
+ direction: 'crypto_to_fiat',
378
+ sourceAsset: 'USDC',
379
+ sourceNetwork: 'ETH',
380
+ amountCents: plan.requiredCents,
381
+ feesCents: toCents(25),
382
+ netAmountCents: (plan.requiredCents - toCents(25)) as Cents,
383
+ destinationBankId: plan.destinationBankId,
384
+ status: 'pending',
385
+ initiatedAt: now,
386
+ idempotencyKey: plan.idempotencyKey,
387
+ previousHash,
388
+ };
389
+
390
+ const hash = computeTransferHash(record, previousHash);
391
+ return { ...record, hash };
392
+ }
393
+
394
+ async getTransferStatus(transferId: string): Promise<TransferStatusDetail> {
395
+ // GET /payouts/{id}
396
+ // Response: { data: { id, status, amount, fees, createDate, updateDate } }
397
+ // Circle statuses: pending → processing → complete | failed
398
+ const res = await this.apiCall('GET', `/payouts/${transferId}`);
399
+ const payout = res.data as Record<string, unknown>;
400
+ const amount = payout.amount as Record<string, string>;
401
+ const fees = payout.fees as Record<string, string> | undefined;
402
+
403
+ return {
404
+ transferId,
405
+ providerTransferId: payout.id as string,
406
+ status: mapCircleStatus(payout.status as string),
407
+ amountCents: toCents(parseFloat(amount.amount)),
408
+ feesCents: fees ? toCents(parseFloat(fees.amount)) : (0 as Cents),
409
+ initiatedAt: payout.createDate as string,
410
+ completedAt: payout.status === 'complete' ? payout.updateDate as string : undefined,
411
+ providerRawStatus: payout.status as string,
412
+ };
413
+ }
414
+
415
+ async cancelTransfer(transferId: string): Promise<{ cancelled: boolean; reason?: string }> {
416
+ // Circle does not support cancellation of payouts once processing begins.
417
+ // Only pending payouts may be cancellable via support — no direct API.
418
+ return {
419
+ cancelled: false,
420
+ reason: 'Circle does not support programmatic payout cancellation. Contact support for pending payouts.',
421
+ };
422
+ }
423
+
424
+ async listCompletedTransfers(dateRange: DateRange): Promise<TransferRecord[]> {
425
+ // GET /payouts?destination={bankId}&status=complete
426
+ // &pageBefore=...&pageAfter=...
427
+ // Filter by createDate within dateRange on the client side.
428
+ // Returns array of TransferRecords for reconciliation.
429
+ // In production: paginate and filter by date.
430
+ throw new Error('HTTP implementation — use node:https, no SDK dependencies');
431
+ }
432
+
433
+ // ── Private helpers ──────────
434
+
435
+ private async fetchStablecoinBalances(): Promise<StablecoinBalance[]> {
436
+ // GET /wallets/master/balances (or /wallets → pick master → /ballets/{id}/balances)
437
+ // Response: { data: { available: [{ amount: "1500.50", currency: "USD" }] } }
438
+ const res = await this.apiCall('GET', '/wallets');
439
+ const wallets = res.data as Array<Record<string, unknown>>;
440
+ const now = new Date().toISOString();
441
+
442
+ const balances: StablecoinBalance[] = [];
443
+ for (const wallet of wallets) {
444
+ const available = (wallet.balances as Array<Record<string, string>>) ?? [];
445
+ for (const bal of available) {
446
+ if (bal.currency === 'USD') {
447
+ balances.push({
448
+ provider: 'circle',
449
+ asset: 'USDC',
450
+ network: 'ETH', // Circle aggregates across chains for USD balance
451
+ balanceCents: toCents(parseFloat(bal.amount)),
452
+ lastUpdated: now,
453
+ });
454
+ }
455
+ }
456
+ }
457
+ return balances;
458
+ }
459
+
460
+ private async apiCall(
461
+ method: string,
462
+ path: string,
463
+ body?: Record<string, unknown>
464
+ ): Promise<Record<string, unknown>> {
465
+ // Implementation: raw HTTPS (no SDK — zero dependency principle)
466
+ // Headers:
467
+ // Authorization: Bearer {this.apiKey}
468
+ // Content-Type: application/json
469
+ // Accept: application/json
470
+ // Sanitize response strings per §9.19.16 before returning
471
+ throw new Error('HTTP implementation — use node:https, no SDK dependencies');
472
+ }
473
+ }
474
+
475
+ // ── Status Mapping ──────────────────────────────────
476
+
477
+ function mapCircleStatus(circleStatus: string): TransferStatus {
478
+ switch (circleStatus) {
479
+ case 'pending': return 'pending';
480
+ case 'processing': return 'processing';
481
+ case 'complete': return 'completed';
482
+ case 'failed': return 'failed';
483
+ default: return 'pending';
484
+ }
485
+ }
486
+
487
+ // ── Framework Adaptation Notes ──────────────────────
488
+ //
489
+ // Express/Node.js:
490
+ // - Setup routes in /api/treasury/setup/* (interactive, session-authed)
491
+ // - Runtime methods called by Heartbeat scheduler only (daemon-process.ts)
492
+ // - Use idempotencyKey middleware on POST routes
493
+ //
494
+ // Django/FastAPI:
495
+ // - StablecoinSetup maps to ViewSet with session auth
496
+ // - StablecoinAdapter methods become Celery/ARQ tasks (single-worker queue)
497
+ // - Use django.db.transaction.atomic() for TransferRecord persistence
498
+ //
499
+ // Next.js:
500
+ // - Setup API routes under /api/treasury/stablecoin/*
501
+ // - Server actions for interactive verification steps
502
+ // - Runtime adapter runs in separate worker process, not in Next.js server
503
+
504
+ export type {
505
+ StablecoinSetup, StablecoinAdapter,
506
+ StablecoinProvider, SupportedAsset, ProviderCredentials,
507
+ StablecoinBalance, FiatBalance, CombinedBalances,
508
+ OfframpQuote, TransferStatus, TransferRecord, TransferStatusDetail,
509
+ FundingPlanRef, DateRange,
510
+ };
511
+ export { toCents, toDollars, computeTransferHash, mapCircleStatus, CircleAdapter };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Pattern: Third-Party Script Loading
3
+ *
4
+ * When loading external scripts (Google Identity, Stripe.js, analytics SDKs),
5
+ * always define three states: loading, ready, error.
6
+ * The timeout MUST transition to 'error', not back to 'loading'.
7
+ *
8
+ * Field report #17: GIS script timeout set gsiReady=false (already false) —
9
+ * no state change, no UI update, infinite spinner.
10
+ */
11
+
12
+ // State machine: 'loading' → 'ready' | 'error'
13
+ type ScriptState = 'loading' | 'ready' | 'error';
14
+
15
+ interface ThirdPartyScript {
16
+ state: ScriptState;
17
+ error: string | null;
18
+ }
19
+
20
+ function loadExternalScript(src: string, timeoutMs = 10_000): ThirdPartyScript {
21
+ const script: ThirdPartyScript = { state: 'loading', error: null };
22
+
23
+ // Load the script
24
+ const el = document.createElement('script');
25
+ el.src = src;
26
+ el.async = true;
27
+
28
+ el.onload = () => {
29
+ script.state = 'ready';
30
+ script.error = null;
31
+ // Update UI: show ready state
32
+ };
33
+
34
+ el.onerror = () => {
35
+ script.state = 'error';
36
+ script.error = `Failed to load ${src}`;
37
+ // Update UI: show error state with retry action
38
+ };
39
+
40
+ document.head.appendChild(el);
41
+
42
+ // Timeout — MUST transition to 'error', not stay in 'loading'
43
+ setTimeout(() => {
44
+ if (script.state === 'loading') {
45
+ script.state = 'error';
46
+ script.error = `Script load timed out after ${timeoutMs}ms`;
47
+ // Update UI: show timeout error with retry action
48
+ }
49
+ }, timeoutMs);
50
+
51
+ return script;
52
+ }
53
+
54
+ /**
55
+ * UI Requirements:
56
+ * - loading: Show spinner or skeleton
57
+ * - ready: Show the feature
58
+ * - error: Show message + "Retry" button (or "Continue without [feature]")
59
+ *
60
+ * Anti-pattern:
61
+ * if (state === 'loading') showSpinner();
62
+ * // NO error state handler → infinite spinner on timeout
63
+ *
64
+ * Correct:
65
+ * if (state === 'loading') showSpinner();
66
+ * if (state === 'ready') showFeature();
67
+ * if (state === 'error') showError(script.error, retryFn);
68
+ */