1818import {
1919 startCoralAgent , complete , parseJsonReply , loadKeypairB58 ,
2020 formatWant , parseBid , parseEscrowRequired , formatAward , formatDeposited ,
21- formatVerify , parseVerified , sha256Hex , enforce , policyFromEnv ,
21+ formatVerify , parseVerified , formatLlmUsed , sha256Hex , enforce , policyFromEnv , llmRuntimeInfo ,
2222 selectBids , pickCheapest , verb , messageRound ,
23- type Bid , type EscrowTerms , type CoralAgentContext ,
23+ type Bid , type EscrowTerms , type CoralAgentContext , type LlmUse ,
2424} from '@pay/agent-runtime'
2525import { PublicKey } from '@solana/web3.js'
2626import { makeProgram , deposit , release , escrowPda } from './escrow.js'
@@ -32,6 +32,7 @@ import { fetchNextWant } from './wantFeed.js'
3232import { fetchReputationLines } from './reputation.js'
3333
3434const RPC = process . env . SOLANA_RPC_URL ?? 'https://api.devnet.solana.com'
35+ const NAME = process . env . AGENT_NAME ?? 'buyer-agent'
3536const BUDGET = Number ( process . env . BUYER_MAX_SOL ?? '0.001' )
3637const SERVICE = process . env . BUYER_SERVICE ?? 'txline'
3738// Rotate through several args so each round trades a *different* thing (BUYER_ARGS=csv of fixture ids,
@@ -67,29 +68,53 @@ const policy = policyFromEnv(process.env, {
6768
6869const sleep = ( ms : number ) => new Promise ( ( r ) => setTimeout ( r , ms ) )
6970const expl = ( kind : 'tx' | 'address' , id : string ) => `https://explorer.solana.com/${ kind } /${ id } ?cluster=devnet`
71+ const selectionGuardrail = 'winner must match collected BID set; fallback is cheapest available bid'
72+
73+ function buyerLlm ( round : number , status : LlmUse [ 'status' ] , reason : string ) : LlmUse {
74+ const info = status === 'skipped' ? undefined : llmRuntimeInfo ( { maxTokens : 100 } )
75+ return {
76+ round,
77+ agent : NAME ,
78+ purpose : 'buyer_award' ,
79+ status,
80+ ...( info ? { provider : info . provider , model : info . model } : { } ) ,
81+ reason,
82+ guardrail : selectionGuardrail ,
83+ createdAt : new Date ( ) . toISOString ( ) ,
84+ }
85+ }
86+
87+ const errReason = ( e : unknown ) : string => String ( ( e as Error ) . message ?? e ) . slice ( 0 , 120 )
7088
7189/** Best-value selection via LLM; deterministic cheapest fallback. Returns the winner + its reasoning. */
72- async function pickWinner ( pool : Bid [ ] , repLines ?: string ) : Promise < { winner : Bid ; reason ?: string } > {
73- if ( pool . length === 1 ) return { winner : pool [ 0 ] }
90+ async function pickWinner (
91+ round : number ,
92+ service : string ,
93+ arg : string ,
94+ budget : number ,
95+ pool : Bid [ ] ,
96+ repLines ?: string ,
97+ ) : Promise < { winner : Bid ; reason ?: string ; llm : LlmUse } > {
98+ if ( pool . length === 1 ) return { winner : pool [ 0 ] , llm : buyerLlm ( round , 'skipped' , 'single bid; no model selection needed' ) }
7499 try {
75100 const system =
76101 'You are a buyer choosing the best-value bid for a Solana data service. Weigh price against ' +
77102 'each seller\'s track record when one is given - a cheap seller that fails verification or ' +
78103 'no-shows is not a bargain. Reply ONLY with JSON {"by": "<seller name>", "reason": "<short>"}.'
79104 const user =
80- `service=${ SERVICE } arg=${ ARG } budget=${ BUDGET } \nbids:\n` +
105+ `service=${ service } arg=${ arg } budget=${ budget } \nbids:\n` +
81106 pool . map ( ( b ) => `- ${ b . by } : ${ b . priceSol } SOL${ b . note ? ` (${ b . note } )` : '' } ` ) . join ( '\n' ) +
82107 ( repLines ? `\ntrack record (derived from the run ledger):\n${ repLines } ` : '' )
83108 const parsed = parseJsonReply < { by ?: string ; reason ?: string } > ( await complete ( { system, user, maxTokens : 100 } ) )
84109 const chosen = pool . find ( ( b ) => b . by === parsed ?. by )
85110 if ( chosen ) {
86111 console . error ( `[buyer] picked ${ chosen . by } (${ chosen . priceSol } SOL): ${ parsed ?. reason ?? '' } ` )
87- return { winner : chosen , reason : parsed ?. reason }
112+ return { winner : chosen , reason : parsed ?. reason , llm : buyerLlm ( round , 'used' , parsed ?. reason ?? 'model selected winner' ) }
88113 }
89- } catch {
90- /* fall through to deterministic choice */
114+ return { winner : pickCheapest ( pool ) ! , reason : 'cheapest available' , llm : buyerLlm ( round , 'fallback' , 'model returned a seller outside the bid pool' ) }
115+ } catch ( e ) {
116+ return { winner : pickCheapest ( pool ) ! , reason : 'cheapest available' , llm : buyerLlm ( round , 'fallback' , `LLM unavailable: ${ errReason ( e ) } ` ) }
91117 }
92- return { winner : pickCheapest ( pool ) ! , reason : 'cheapest available' }
93118}
94119
95120/** Wait (bounded) for a message matching `round` that `parse` accepts. */
@@ -109,7 +134,7 @@ async function waitFor<T>(
109134 return null
110135}
111136
112- await startCoralAgent ( { agentName : process . env . AGENT_NAME ?? 'buyer-agent' } , async ( ctx ) => {
137+ await startCoralAgent ( { agentName : NAME } , async ( ctx ) => {
113138 const buyer = loadKeypairB58 ( 'BUYER_KEYPAIR_B58' )
114139 const arbiter = SETTLEMENT_MODE === 'arbiter' ? loadKeypairB58 ( 'ARBITER_KEYPAIR_B58' ) : null
115140 console . error ( `[buyer] market buyer - wallet=${ buyer . publicKey . toBase58 ( ) } budget=${ BUDGET } sellers=[${ SELLERS . join ( ',' ) } ]` )
@@ -164,7 +189,8 @@ await startCoralAgent({ agentName: process.env.AGENT_NAME ?? 'buyer-agent' }, as
164189
165190 // -- award the best value (price × track record) -------------------------
166191 const repLines = REPUTATION_URL ? await fetchReputationLines ( REPUTATION_URL ) : undefined
167- const { winner, reason } = await pickWinner ( pool , repLines )
192+ const { winner, reason, llm } = await pickWinner ( round , service , arg , budget , pool , repLines )
193+ await ctx . send ( formatLlmUsed ( llm ) , thread )
168194 await ctx . send ( formatAward ( round , winner . by , reason ) , thread , [ winner . by ] )
169195
170196 // -- settle through escrow: deposit -> DEPOSITED -> wait DELIVERED -> release
0 commit comments