clinch-core 0.6.2 → 0.7.1

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.
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  The Agent Negotiation Protocol (ANP) governs how two autonomous software agents establish identity, declare capabilities, and reach a cryptographically verifiable agreement on price and terms.
12
12
 
13
13
  ### 1.1 Address Formatting & Routing
14
- All destination routing in Clinch relies on strict, structured addressing:
14
+ All destination routing in Clinch relies on structured, prefixed addresses:
15
15
 
16
16
  $$\text{PROTOCOL\_MODE} \mathbin{.} \text{domain} \mathbin{.} \text{TLD}$$
17
17
 
@@ -36,9 +36,10 @@ The SDK maintains an isolated, turn-based state machine for each active session.
36
36
  * `OFFLINE`: Client is completely disconnected.
37
37
  * `CONNECTING`: Authenticating identity and performing local Proof-of-Work (PoW) verification.
38
38
  * `IDLE`: Connected and authenticated. Ready to initialize new sessions.
39
+ * `RECONNECTING`: Socket connection lost; executing exponential backoff.
39
40
  * `NEGOTIATING`: Active, turn-based bargaining sequence in progress.
40
41
  * `CONVERGED`: Mathematical convergence reached; agreement co-signed.
41
- * `STALEMATE`: Handshake or negotiation sequence aborted or timed out.
42
+ * `STALEMATE`: Negotiation terminated; max turns reached without convergence.
42
43
  * `ERROR`: Internal network, compilation, or cryptographic validation failure.
43
44
 
44
45
  ### Developer Event Subscriptions
@@ -114,7 +115,7 @@ async function runAutonomousSession() {
114
115
  runAutonomousSession();
115
116
  ```
116
117
 
117
- ### 4.2 Cloud Webhook & Horizontal Scale Pattern
118
+ ### 4.2 Webhook & Horizontal Scale Pattern
118
119
  For enterprise cloud environments where processes must remain stateless or scale horizontally across server pods, you must serialize and rehydrate active sessions dynamically. This ensures that when an asynchronous callback arrives, any pod can load the session state and use the matching ephemeral key to sign the next turn.
119
120
 
120
121
  ```javascript
@@ -167,11 +168,51 @@ webhookCore.on('callback_received', async (event) => {
167
168
  });
168
169
  ```
169
170
 
171
+ ### 4.3 Cascading Multi-Seller Squeeze and Races
172
+ To implement market discovery, the SDK supports cascading negotiations across multiple matching sellers. The Core orchestrates two distinct optimization strategies:
173
+
174
+ #### Sequential Squeeze (Default)
175
+ Useful for high-value items where time is secondary to price. The SDK negotiates sequentially with each seller, using the converged deal price of the previous seller as the strict, maximum budget ceiling for the next. This dynamically forces sellers to underbid one another.
176
+
177
+ #### Parallel Race
178
+ Necessary for real-time services like ride-hailing or logistics. The SDK handshakes and conducts negotiations with all selected sellers concurrently in parallel. Once all sessions finish, it selects the absolute lowest price converged under your budget.
179
+
180
+ ```javascript
181
+ // Triggers the cascading loop
182
+ const bestDeal = await core.negotiateCascade(
183
+ 'domain_name', // Category to discover
184
+ { intent: 'purchase', item: 'mybrand.io', max_budget: 150.00 },
185
+ 3, // Max sellers to target
186
+ 'sequential' // 'sequential' | 'parallel'
187
+ );
188
+
189
+ if (bestDeal) {
190
+ console.log(`🏆 Optimal deal secured with ${bestDeal.sellerId} at $${bestDeal.finalPrice}`);
191
+ }
192
+ ```
193
+
194
+ ### 4.4 Blind Key Pass (Credential Injection)
195
+ For sessions with services that require authorization tokens or API keys to operate (such as platform execution nodes), you can register these credentials locally.
196
+
197
+ The Core library securely stores these secrets and silently injects them into the handshake transport layer during session initialization. This prevents the API keys from ever being exposed to the AI model's context window, safeguarding you against prompt injection attacks.
198
+
199
+ ```javascript
200
+ // Register the API credential locally bound to the domain namespace
201
+ core.registerSecret('apify.anp', 'apify_sec_key_xyz987', 'Apify Production Token');
202
+
203
+ // Handshaking with this address now automatically injects the credential silently
204
+ const sessionId = await core.negotiate('ANP/C.apify.anp', {
205
+ intent: 'purchase',
206
+ item: 'actor-scraper-run',
207
+ max_budget: 5.00
208
+ });
209
+ ```
210
+
170
211
  ---
171
212
 
172
213
  ## 5. Integration Guide: Seller Nodes
173
214
 
174
- Sellers use the `ClinchSeller` class to build compliant, automated endpoints.
215
+ Sellers use the `ClinchSeller` class to build compliant, automated endpoints.
175
216
 
176
217
  ### 5.1 Decoupled Routing Architecture
177
218
  To prevent centralized token expiration failures, Clinch separates ownership from routing:
@@ -241,6 +282,13 @@ Initializes the cryptographic handshake with a seller. Returns `sessionId`.
241
282
  #### `exportSessionState(sessionId)` / `importSessionState(serializedData)`
242
283
  Serializes or de-serializes all in-flight state for a given session, including ephemeral Ed25519 keys, state metrics, and local sandbox parameters, into an exchangeable JSON string.
243
284
 
285
+ #### `registerSecret(domain, key, name?)` / `clearSecret(domain)`
286
+ Saves or clears an API secret locally. The key is never visible to the AI agent and is silently passed in handshake payloads to the designated domain.
287
+
288
+ #### `async negotiateCascade(category, constraints, maxSellers?, strategy?)`
289
+ Queries the discovery register and cascade-negotiates with matching nodes.
290
+ * `strategy`: `'sequential'` (Sequential Squeeze) or `'parallel'` (Concurrent Race). Default: `'sequential'`.
291
+
244
292
  #### `buildAgentPrompt(sessionId, incomingMessage)`
245
293
  Assembles a contextual, structure-compliant system prompt for external LLMs to ensure compliant JSON execution.
246
294
 
package/dist/index.d.ts CHANGED
@@ -25,6 +25,7 @@ export interface SessionState {
25
25
  constraints: ConstraintVector;
26
26
  currentTurn: number;
27
27
  lastKnownPrice: number;
28
+ sellerInstructions?: string | null;
28
29
  sandboxSequence?: any;
29
30
  sandboxSession?: any;
30
31
  }
@@ -48,12 +49,15 @@ export declare class ClinchCore extends EventEmitter {
48
49
  identityPubKey: string;
49
50
  private activeSessions;
50
51
  private ws;
52
+ private localSecrets;
51
53
  private isSandboxMode;
52
54
  private sandboxModelContext;
53
55
  private sandboxMaxTurns;
54
56
  get activeNegotiationId(): string | null;
55
57
  constructor(config?: ClinchConfig);
56
58
  private setStatus;
59
+ registerSecret(domain: string, key: string, name?: string): void;
60
+ clearSecret(domain: string): void;
57
61
  initialize(cachedToken?: string): Promise<void>;
58
62
  private getRegistryUrl;
59
63
  private networkRequest;
@@ -69,6 +73,12 @@ export declare class ClinchCore extends EventEmitter {
69
73
  negotiate(address: string, constraints: ConstraintVector): Promise<string>;
70
74
  sendCounter(sessionId: string, price: number, reason: string): Promise<void>;
71
75
  exitSession(sessionId: string): Promise<string>;
76
+ negotiateCascade(category: string, constraints: ConstraintVector, maxSellers?: number, strategy?: 'sequential' | 'parallel'): Promise<{
77
+ sessionId: string;
78
+ sellerId: string;
79
+ finalPrice: number;
80
+ } | null>;
81
+ private waitForSession;
72
82
  sandbox(config?: SandboxConfig): Promise<void>;
73
83
  private setupSandbox;
74
84
  private handleAutomaticSandboxTurn;
@@ -85,6 +95,7 @@ export interface SellerRecord {
85
95
  categories: string[];
86
96
  capabilities: string[];
87
97
  display_name?: string;
98
+ custom_instructions?: string;
88
99
  }
89
100
  export declare class ClinchSeller extends EventEmitter {
90
101
  private config;
package/dist/index.js CHANGED
@@ -47,7 +47,7 @@ const ws_1 = __importDefault(require("ws"));
47
47
  const PROTOCOL_VERSION = "0.1.0";
48
48
  const FIREBASE_CONFIG_URL = "https://clinchprotocol.web.app/network-config.json";
49
49
  function toHex(arr) {
50
- return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
50
+ return Array.from(arr).map((b) => b.toString(16).padStart(2, '0')).join('');
51
51
  }
52
52
  function fromHex(hex) {
53
53
  // Strips all spaces, newlines, and rogue quotes from .env strings
@@ -55,7 +55,7 @@ function fromHex(hex) {
55
55
  const match = clean.match(/.{1,2}/g);
56
56
  if (!match)
57
57
  return new Uint8Array(0);
58
- return new Uint8Array(match.map(byte => parseInt(byte, 16)));
58
+ return new Uint8Array(match.map((byte) => parseInt(byte, 16)));
59
59
  }
60
60
  // ============================================================================
61
61
  // THE CLINCH CORE LIBRARY (Buyer)
@@ -71,6 +71,8 @@ class ClinchCore extends events_1.EventEmitter {
71
71
  identityPubKey;
72
72
  activeSessions = new Map();
73
73
  ws = null;
74
+ // Blind Key Pass Local Secret Store
75
+ localSecrets = new Map();
74
76
  isSandboxMode = false;
75
77
  sandboxModelContext = null;
76
78
  sandboxMaxTurns = 6;
@@ -99,6 +101,18 @@ class ClinchCore extends events_1.EventEmitter {
99
101
  this.emit('log', `🟡 [State] ${this.status}`);
100
102
  }
101
103
  }
104
+ // --------------------------------------------------------------------------
105
+ // BLIND KEY PASS MANAGERS (Silent API Key Handshake)
106
+ // --------------------------------------------------------------------------
107
+ registerSecret(domain, key, name) {
108
+ const normalizedDomain = domain.toLowerCase().trim();
109
+ this.localSecrets.set(normalizedDomain, { key, name });
110
+ this.emit('log', `[Security] Blind key registered for ${normalizedDomain} (${name || 'Unnamed'})`);
111
+ }
112
+ clearSecret(domain) {
113
+ const normalizedDomain = domain.toLowerCase().trim();
114
+ this.localSecrets.delete(normalizedDomain);
115
+ }
102
116
  async initialize(cachedToken) {
103
117
  if (this.status === 'IDLE' || this.status === 'CONNECTING')
104
118
  return;
@@ -243,7 +257,8 @@ class ClinchCore extends events_1.EventEmitter {
243
257
  constraints: session.constraints,
244
258
  currentTurn: session.currentTurn,
245
259
  lastKnownPrice: session.lastKnownPrice,
246
- ephemeralSecretKeyHex: toHex(session.keyPair.secretKey)
260
+ ephemeralSecretKeyHex: toHex(session.keyPair.secretKey),
261
+ sellerInstructions: session.sellerInstructions
247
262
  };
248
263
  return JSON.stringify(exportable);
249
264
  }
@@ -259,7 +274,8 @@ class ClinchCore extends events_1.EventEmitter {
259
274
  constraints: data.constraints,
260
275
  currentTurn: data.currentTurn,
261
276
  lastKnownPrice: data.lastKnownPrice,
262
- keyPair: keyPair
277
+ keyPair: keyPair,
278
+ sellerInstructions: data.sellerInstructions
263
279
  });
264
280
  this.emit('log', `[State] Rehydrated session ${data.sessionId} pointing at ${data.sellerId}`);
265
281
  }
@@ -272,6 +288,9 @@ class ClinchCore extends events_1.EventEmitter {
272
288
  throw new Error("Cannot build prompt: Session not found.");
273
289
  const gap = session.lastKnownPrice - session.constraints.max_budget;
274
290
  const gapText = gap > 0 ? `-$${gap} (Over budget)` : `+$${Math.abs(gap)} (Under budget)`;
291
+ const customInstructionsBlock = session.sellerInstructions
292
+ ? `\nCUSTOM INSTRUCTIONS DIRECT FROM TARGET DOMAIN (${session.sellerId}):\n"""\n${session.sellerInstructions}\n"""\n`
293
+ : "";
275
294
  return `You are a professional AI purchasing agent negotiating via the Clinch Protocol.
276
295
  Your only goal is to secure the requested item below the maximum budget.
277
296
 
@@ -282,7 +301,7 @@ NEGOTIATION STATE:
282
301
  - Current turn: ${session.currentTurn}
283
302
  - Last seller price: $${session.lastKnownPrice}
284
303
  - Gap to budget: ${gapText}
285
-
304
+ ${customInstructionsBlock}
286
305
  SELLER'S LATEST MESSAGE:
287
306
  "${incomingMessage}"
288
307
 
@@ -307,10 +326,11 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
307
326
  return await this.networkRequest(url);
308
327
  }
309
328
  async negotiate(address, constraints) {
310
- this.emit('log', `[Protocol] Initiating handshake with ${address}...`);
329
+ this.emit('log', `[Protocol] Handshaking with ${address}...`);
311
330
  const parsed = this.parseAddress(address);
312
331
  const ephemeralKeys = tweetnacl_1.default.sign.keyPair();
313
332
  const ephemeralPubHex = toHex(ephemeralKeys.publicKey);
333
+ const blindSecret = this.localSecrets.get(parsed.domain);
314
334
  const initPayload = {
315
335
  clinch_version: PROTOCOL_VERSION,
316
336
  mode: parsed.mode,
@@ -318,6 +338,10 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
318
338
  session_pub_key: ephemeralPubHex,
319
339
  timestamp_utc: new Date().toISOString()
320
340
  };
341
+ if (blindSecret) {
342
+ initPayload.blind_auth_token = blindSecret.key;
343
+ this.emit('log', `[Security] Silently injected blind token for ${parsed.domain} at transport layer`);
344
+ }
321
345
  const msgUint8 = new TextEncoder().encode(JSON.stringify(initPayload));
322
346
  const signature = toHex(tweetnacl_1.default.sign.detached(msgUint8, ephemeralKeys.secretKey));
323
347
  const response = await this.networkRequest(`/api/route/${parsed.domain}/handshake`, {
@@ -325,6 +349,7 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
325
349
  headers: { 'Content-Type': 'application/json' },
326
350
  body: JSON.stringify({ ...initPayload, sig: signature })
327
351
  });
352
+ const instructions = response.custom_instructions || null;
328
353
  this.activeSessions.set(response.session_id, {
329
354
  sessionId: response.session_id,
330
355
  sellerId: parsed.domain,
@@ -332,7 +357,8 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
332
357
  status: 'ACTIVE',
333
358
  constraints,
334
359
  currentTurn: 1,
335
- lastKnownPrice: 0
360
+ lastKnownPrice: 0,
361
+ sellerInstructions: instructions
336
362
  });
337
363
  this.setStatus('NEGOTIATING');
338
364
  this.emit('session_started', { sessionId: response.session_id, sellerId: parsed.domain });
@@ -368,6 +394,95 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
368
394
  session.exitTokenHash = res.token_hash;
369
395
  return res.token_hash;
370
396
  }
397
+ async negotiateCascade(category, constraints, maxSellers = 3, strategy = 'sequential') {
398
+ this.emit('log', `[Cascade] Querying registry for matching sellers under "${category}"...`);
399
+ const discovery = await this.search(category);
400
+ const sellers = (discovery.results || []).slice(0, maxSellers);
401
+ if (sellers.length === 0) {
402
+ this.emit('log', `[Cascade] No matching sellers found for category: "${category}"`);
403
+ return null;
404
+ }
405
+ if (strategy === 'parallel') {
406
+ this.emit('log', `[Cascade] ⚡ Running PARALLEL RACE simultaneously across ${sellers.length} seller nodes...`);
407
+ const sessionPromises = sellers.map(async (seller) => {
408
+ const targetAddress = `ANP/C.${seller.agent_id}`;
409
+ try {
410
+ const sessionId = await this.negotiate(targetAddress, constraints);
411
+ const result = await this.waitForSession(sessionId);
412
+ return { sellerId: seller.agent_id, sessionId, ...result };
413
+ }
414
+ catch (e) {
415
+ this.emit('log', `[Cascade] ⚠️ Connection failed for parallel channel ${seller.agent_id}: ${e.message}`);
416
+ return { sellerId: seller.agent_id, sessionId: '', outcome: 'stalemate', price: Infinity };
417
+ }
418
+ });
419
+ const results = await Promise.all(sessionPromises);
420
+ const successfulDeals = results.filter((r) => r.outcome === 'deal' && r.price <= constraints.max_budget);
421
+ if (successfulDeals.length === 0) {
422
+ this.emit('log', `[Cascade] ✗ Parallel race completed. No successful deals reached.`);
423
+ return null;
424
+ }
425
+ successfulDeals.sort((a, b) => a.price - b.price);
426
+ const winner = successfulDeals[0];
427
+ this.emit('log', `[Cascade] 🏆 Parallel race complete! Lowest offer: $${winner.price} from ${winner.sellerId}`);
428
+ return {
429
+ sessionId: winner.sessionId,
430
+ sellerId: winner.sellerId,
431
+ finalPrice: winner.price
432
+ };
433
+ }
434
+ this.emit('log', `[Cascade] ➔ Running SEQUENTIAL SQUEEZE across ${sellers.length} seller nodes...`);
435
+ let bestDeal = null;
436
+ let currentBudgetCeiling = constraints.max_budget;
437
+ for (const seller of sellers) {
438
+ const targetAddress = `ANP/C.${seller.agent_id}`;
439
+ this.emit('log', `\n[Cascade] Squeezing target: ${targetAddress} | Squeeze Ceiling: $${currentBudgetCeiling}`);
440
+ const sessionConstraints = {
441
+ ...constraints,
442
+ max_budget: currentBudgetCeiling
443
+ };
444
+ try {
445
+ const sessionId = await this.negotiate(targetAddress, sessionConstraints);
446
+ const result = await this.waitForSession(sessionId);
447
+ if (result.outcome === 'deal' && result.price < currentBudgetCeiling) {
448
+ this.emit('log', `[Cascade] ✓ Better deal clinched at $${result.price} from ${seller.agent_id}!`);
449
+ bestDeal = {
450
+ sessionId,
451
+ sellerId: seller.agent_id,
452
+ finalPrice: result.price
453
+ };
454
+ currentBudgetCeiling = result.price;
455
+ }
456
+ else {
457
+ this.emit('log', `[Cascade] ✗ Seller ${seller.agent_id} failed to beat current best offer of $${currentBudgetCeiling}`);
458
+ }
459
+ }
460
+ catch (e) {
461
+ this.emit('log', `[Cascade] ⚠️ Dynamic session error with ${seller.agent_id}: ${e.message}`);
462
+ }
463
+ }
464
+ return bestDeal;
465
+ }
466
+ waitForSession(sessionId) {
467
+ return new Promise((resolve) => {
468
+ const onClosed = (event) => {
469
+ if (event.sessionId === sessionId) {
470
+ this.off('session_closed', onClosed);
471
+ this.off('status_changed', onStatus);
472
+ resolve({ outcome: 'deal', price: event.finalPrice });
473
+ }
474
+ };
475
+ const onStatus = (status) => {
476
+ if (status === 'STALEMATE') {
477
+ this.off('session_closed', onClosed);
478
+ this.off('status_changed', onStatus);
479
+ resolve({ outcome: 'stalemate', price: Infinity });
480
+ }
481
+ };
482
+ this.on('session_closed', onClosed);
483
+ this.on('status_changed', onStatus);
484
+ });
485
+ }
371
486
  async sandbox(config = {}) {
372
487
  this.isSandboxMode = true;
373
488
  this.sandboxMaxTurns = config.maxTurns || 6;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clinch-core",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
 
5
5
  "description": "Clinch Protocol Edge Client",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@ const PROTOCOL_VERSION = "0.1.0";
10
10
  const FIREBASE_CONFIG_URL = "https://clinchprotocol.web.app/network-config.json";
11
11
 
12
12
  function toHex(arr: Uint8Array | number[]): string {
13
- return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
13
+ return Array.from(arr).map((b: number) => b.toString(16).padStart(2, '0')).join('');
14
14
  }
15
15
 
16
16
  function fromHex(hex: string): Uint8Array {
@@ -18,7 +18,7 @@ function fromHex(hex: string): Uint8Array {
18
18
  const clean = hex.replace(/[^0-9a-fA-F]/g, '');
19
19
  const match = clean.match(/.{1,2}/g);
20
20
  if (!match) return new Uint8Array(0);
21
- return new Uint8Array(match.map(byte => parseInt(byte, 16)));
21
+ return new Uint8Array(match.map((byte: string) => parseInt(byte, 16)));
22
22
  }
23
23
 
24
24
  // ============================================================================
@@ -63,6 +63,9 @@ export interface SessionState {
63
63
  currentTurn: number;
64
64
  lastKnownPrice: number;
65
65
 
66
+ // Direct, dynamic instructions injected by the target domain
67
+ sellerInstructions?: string | null;
68
+
66
69
  sandboxSequence?: any;
67
70
  sandboxSession?: any;
68
71
  }
@@ -97,6 +100,9 @@ export class ClinchCore extends EventEmitter {
97
100
  private activeSessions = new Map<string, SessionState>();
98
101
  private ws: WebSocket | null = null;
99
102
 
103
+ // Blind Key Pass Local Secret Store
104
+ private localSecrets = new Map<string, { key: string, name?: string }>();
105
+
100
106
  private isSandboxMode = false;
101
107
  private sandboxModelContext: any = null;
102
108
  private sandboxMaxTurns = 6;
@@ -129,6 +135,20 @@ export class ClinchCore extends EventEmitter {
129
135
  }
130
136
  }
131
137
 
138
+ // --------------------------------------------------------------------------
139
+ // BLIND KEY PASS MANAGERS (Silent API Key Handshake)
140
+ // --------------------------------------------------------------------------
141
+ public registerSecret(domain: string, key: string, name?: string): void {
142
+ const normalizedDomain = domain.toLowerCase().trim();
143
+ this.localSecrets.set(normalizedDomain, { key, name });
144
+ this.emit('log', `[Security] Blind key registered for ${normalizedDomain} (${name || 'Unnamed'})`);
145
+ }
146
+
147
+ public clearSecret(domain: string): void {
148
+ const normalizedDomain = domain.toLowerCase().trim();
149
+ this.localSecrets.delete(normalizedDomain);
150
+ }
151
+
132
152
  async initialize(cachedToken?: string): Promise<void> {
133
153
  if (this.status === 'IDLE' || this.status === 'CONNECTING') return;
134
154
  this.setStatus('CONNECTING');
@@ -278,7 +298,8 @@ export class ClinchCore extends EventEmitter {
278
298
  constraints: session.constraints,
279
299
  currentTurn: session.currentTurn,
280
300
  lastKnownPrice: session.lastKnownPrice,
281
- ephemeralSecretKeyHex: toHex(session.keyPair.secretKey)
301
+ ephemeralSecretKeyHex: toHex(session.keyPair.secretKey),
302
+ sellerInstructions: session.sellerInstructions
282
303
  };
283
304
 
284
305
  return JSON.stringify(exportable);
@@ -297,7 +318,8 @@ export class ClinchCore extends EventEmitter {
297
318
  constraints: data.constraints,
298
319
  currentTurn: data.currentTurn,
299
320
  lastKnownPrice: data.lastKnownPrice,
300
- keyPair: keyPair
321
+ keyPair: keyPair,
322
+ sellerInstructions: data.sellerInstructions
301
323
  });
302
324
 
303
325
  this.emit('log', `[State] Rehydrated session ${data.sessionId} pointing at ${data.sellerId}`);
@@ -314,6 +336,10 @@ export class ClinchCore extends EventEmitter {
314
336
  const gap = session.lastKnownPrice - session.constraints.max_budget;
315
337
  const gapText = gap > 0 ? `-$${gap} (Over budget)` : `+$${Math.abs(gap)} (Under budget)`;
316
338
 
339
+ const customInstructionsBlock = session.sellerInstructions
340
+ ? `\nCUSTOM INSTRUCTIONS DIRECT FROM TARGET DOMAIN (${session.sellerId}):\n"""\n${session.sellerInstructions}\n"""\n`
341
+ : "";
342
+
317
343
  return `You are a professional AI purchasing agent negotiating via the Clinch Protocol.
318
344
  Your only goal is to secure the requested item below the maximum budget.
319
345
 
@@ -324,7 +350,7 @@ NEGOTIATION STATE:
324
350
  - Current turn: ${session.currentTurn}
325
351
  - Last seller price: $${session.lastKnownPrice}
326
352
  - Gap to budget: ${gapText}
327
-
353
+ ${customInstructionsBlock}
328
354
  SELLER'S LATEST MESSAGE:
329
355
  "${incomingMessage}"
330
356
 
@@ -350,13 +376,15 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
350
376
  }
351
377
 
352
378
  async negotiate(address: string, constraints: ConstraintVector): Promise<string> {
353
- this.emit('log', `[Protocol] Initiating handshake with ${address}...`);
379
+ this.emit('log', `[Protocol] Handshaking with ${address}...`);
354
380
  const parsed = this.parseAddress(address);
355
381
 
356
382
  const ephemeralKeys = nacl.sign.keyPair();
357
383
  const ephemeralPubHex = toHex(ephemeralKeys.publicKey);
358
384
 
359
- const initPayload = {
385
+ const blindSecret = this.localSecrets.get(parsed.domain);
386
+
387
+ const initPayload: any = {
360
388
  clinch_version: PROTOCOL_VERSION,
361
389
  mode: parsed.mode,
362
390
  constraints,
@@ -364,6 +392,11 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
364
392
  timestamp_utc: new Date().toISOString()
365
393
  };
366
394
 
395
+ if (blindSecret) {
396
+ initPayload.blind_auth_token = blindSecret.key;
397
+ this.emit('log', `[Security] Silently injected blind token for ${parsed.domain} at transport layer`);
398
+ }
399
+
367
400
  const msgUint8 = new TextEncoder().encode(JSON.stringify(initPayload));
368
401
  const signature = toHex(nacl.sign.detached(msgUint8, ephemeralKeys.secretKey));
369
402
 
@@ -373,6 +406,8 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
373
406
  body: JSON.stringify({ ...initPayload, sig: signature })
374
407
  });
375
408
 
409
+ const instructions = response.custom_instructions || null;
410
+
376
411
  this.activeSessions.set(response.session_id, {
377
412
  sessionId: response.session_id,
378
413
  sellerId: parsed.domain,
@@ -380,7 +415,8 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
380
415
  status: 'ACTIVE',
381
416
  constraints,
382
417
  currentTurn: 1,
383
- lastKnownPrice: 0
418
+ lastKnownPrice: 0,
419
+ sellerInstructions: instructions
384
420
  });
385
421
 
386
422
  this.setStatus('NEGOTIATING');
@@ -426,6 +462,112 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
426
462
  return res.token_hash;
427
463
  }
428
464
 
465
+ public async negotiateCascade(
466
+ category: string,
467
+ constraints: ConstraintVector,
468
+ maxSellers = 3,
469
+ strategy: 'sequential' | 'parallel' = 'sequential'
470
+ ): Promise<{ sessionId: string, sellerId: string, finalPrice: number } | null> {
471
+ this.emit('log', `[Cascade] Querying registry for matching sellers under "${category}"...`);
472
+ const discovery = await this.search(category);
473
+ const sellers: any[] = (discovery.results || []).slice(0, maxSellers);
474
+
475
+ if (sellers.length === 0) {
476
+ this.emit('log', `[Cascade] No matching sellers found for category: "${category}"`);
477
+ return null;
478
+ }
479
+
480
+ if (strategy === 'parallel') {
481
+ this.emit('log', `[Cascade] ⚡ Running PARALLEL RACE simultaneously across ${sellers.length} seller nodes...`);
482
+
483
+ const sessionPromises = sellers.map(async (seller: any) => {
484
+ const targetAddress = `ANP/C.${seller.agent_id}`;
485
+ try {
486
+ const sessionId = await this.negotiate(targetAddress, constraints);
487
+ const result = await this.waitForSession(sessionId);
488
+ return { sellerId: seller.agent_id, sessionId, ...result };
489
+ } catch (e: any) {
490
+ this.emit('log', `[Cascade] ⚠️ Connection failed for parallel channel ${seller.agent_id}: ${e.message}`);
491
+ return { sellerId: seller.agent_id, sessionId: '', outcome: 'stalemate' as const, price: Infinity };
492
+ }
493
+ });
494
+
495
+ const results = await Promise.all(sessionPromises);
496
+ const successfulDeals = results.filter((r: any) => r.outcome === 'deal' && r.price <= constraints.max_budget);
497
+
498
+ if (successfulDeals.length === 0) {
499
+ this.emit('log', `[Cascade] ✗ Parallel race completed. No successful deals reached.`);
500
+ return null;
501
+ }
502
+
503
+ successfulDeals.sort((a: any, b: any) => a.price - b.price);
504
+ const winner = successfulDeals[0];
505
+
506
+ this.emit('log', `[Cascade] 🏆 Parallel race complete! Lowest offer: $${winner.price} from ${winner.sellerId}`);
507
+ return {
508
+ sessionId: winner.sessionId,
509
+ sellerId: winner.sellerId,
510
+ finalPrice: winner.price
511
+ };
512
+ }
513
+
514
+ this.emit('log', `[Cascade] ➔ Running SEQUENTIAL SQUEEZE across ${sellers.length} seller nodes...`);
515
+ let bestDeal: { sessionId: string, sellerId: string, finalPrice: number } | null = null;
516
+ let currentBudgetCeiling = constraints.max_budget;
517
+
518
+ for (const seller of sellers) {
519
+ const targetAddress = `ANP/C.${seller.agent_id}`;
520
+ this.emit('log', `\n[Cascade] Squeezing target: ${targetAddress} | Squeeze Ceiling: $${currentBudgetCeiling}`);
521
+
522
+ const sessionConstraints = {
523
+ ...constraints,
524
+ max_budget: currentBudgetCeiling
525
+ };
526
+
527
+ try {
528
+ const sessionId = await this.negotiate(targetAddress, sessionConstraints);
529
+ const result = await this.waitForSession(sessionId);
530
+
531
+ if (result.outcome === 'deal' && result.price < currentBudgetCeiling) {
532
+ this.emit('log', `[Cascade] ✓ Better deal clinched at $${result.price} from ${seller.agent_id}!`);
533
+ bestDeal = {
534
+ sessionId,
535
+ sellerId: seller.agent_id,
536
+ finalPrice: result.price
537
+ };
538
+ currentBudgetCeiling = result.price;
539
+ } else {
540
+ this.emit('log', `[Cascade] ✗ Seller ${seller.agent_id} failed to beat current best offer of $${currentBudgetCeiling}`);
541
+ }
542
+ } catch (e: any) {
543
+ this.emit('log', `[Cascade] ⚠️ Dynamic session error with ${seller.agent_id}: ${e.message}`);
544
+ }
545
+ }
546
+
547
+ return bestDeal;
548
+ }
549
+
550
+ private waitForSession(sessionId: string): Promise<{ outcome: 'deal' | 'stalemate', price: number }> {
551
+ return new Promise((resolve) => {
552
+ const onClosed = (event: any) => {
553
+ if (event.sessionId === sessionId) {
554
+ this.off('session_closed', onClosed);
555
+ this.off('status_changed', onStatus);
556
+ resolve({ outcome: 'deal', price: event.finalPrice });
557
+ }
558
+ };
559
+ const onStatus = (status: CoreStatus) => {
560
+ if (status === 'STALEMATE') {
561
+ this.off('session_closed', onClosed);
562
+ this.off('status_changed', onStatus);
563
+ resolve({ outcome: 'stalemate', price: Infinity });
564
+ }
565
+ };
566
+ this.on('session_closed', onClosed);
567
+ this.on('status_changed', onStatus);
568
+ });
569
+ }
570
+
429
571
  async sandbox(config: SandboxConfig = {}): Promise<void> {
430
572
  this.isSandboxMode = true;
431
573
  this.sandboxMaxTurns = config.maxTurns || 6;
@@ -600,12 +742,13 @@ You MUST respond ONLY in valid JSON matching this exact schema. Do not include m
600
742
  // THE CLINCH SELLER LIBRARY (Server-Side)
601
743
  // ============================================================================
602
744
  export interface SellerRecord {
603
- agent_id: string;
604
- endpoint: string;
605
- supported_modes: string[];
606
- categories: string[];
607
- capabilities: string[];
608
- display_name?: string;
745
+ agent_id: string;
746
+ endpoint: string;
747
+ supported_modes: string[];
748
+ categories: string[];
749
+ capabilities: string[];
750
+ display_name?: string;
751
+ custom_instructions?: string; // Strictly typed dynamic record instructions mapping
609
752
  }
610
753
 
611
754
  export class ClinchSeller extends EventEmitter {