|
| 1 | +/** |
| 2 | + * Subscription Example - Buyer (Client) |
| 3 | + * |
| 4 | + * Run a specific scenario via --scenario flag: |
| 5 | + * npx ts-node buyer.ts --scenario 1 # Subscription offering |
| 6 | + * npx ts-node buyer.ts --scenario 2 # Non-subscription offering (fixed-price) |
| 7 | + * |
| 8 | + * Default: scenario 1 |
| 9 | + * |
| 10 | + * Assumption: |
| 11 | + * - chosenAgent.jobOfferings[0] is a subscription offering |
| 12 | + * - chosenAgent.jobOfferings[1] is a non-subscription (fixed-price) offering |
| 13 | + */ |
| 14 | +import AcpClient, { |
| 15 | + AcpContractClientV2, |
| 16 | + AcpJobPhases, |
| 17 | + AcpJob, |
| 18 | + AcpMemo, |
| 19 | + MemoType, |
| 20 | + AcpAgentSort, |
| 21 | + AcpGraduationStatus, |
| 22 | + AcpOnlineStatus, |
| 23 | + baseSepoliaAcpConfigV2, |
| 24 | +} from "../../../src/index"; |
| 25 | +import { |
| 26 | + BUYER_AGENT_WALLET_ADDRESS, |
| 27 | + BUYER_ENTITY_ID, |
| 28 | + WHITELISTED_WALLET_PRIVATE_KEY, |
| 29 | +} from "./env"; |
| 30 | + |
| 31 | +// Subscription tier name — adjust to match your offering config |
| 32 | +const SUBSCRIPTION_TIER = "sub_premium"; |
| 33 | + |
| 34 | +// Parse --scenario N from argv |
| 35 | +const scenarioArg = process.argv.indexOf("--scenario"); |
| 36 | +const SCENARIO = |
| 37 | + scenarioArg !== -1 ? parseInt(process.argv[scenarioArg + 1], 10) : 1; |
| 38 | + |
| 39 | +async function buyer() { |
| 40 | + console.log(`=== Subscription Example - Buyer (Scenario ${SCENARIO}) ===\n`); |
| 41 | + |
| 42 | + const acpClient = new AcpClient({ |
| 43 | + acpContractClient: await AcpContractClientV2.build( |
| 44 | + WHITELISTED_WALLET_PRIVATE_KEY, |
| 45 | + BUYER_ENTITY_ID, |
| 46 | + BUYER_AGENT_WALLET_ADDRESS, |
| 47 | + baseSepoliaAcpConfigV2, |
| 48 | + ), |
| 49 | + onNewTask: async (job: AcpJob, memoToSign?: AcpMemo) => { |
| 50 | + console.log( |
| 51 | + `Buyer: onNewTask - Job ${job.id}, phase: ${AcpJobPhases[job.phase]}, ` + |
| 52 | + `memoToSign: ${memoToSign?.id ?? "None"}, ` + |
| 53 | + `nextPhase: ${memoToSign?.nextPhase !== undefined ? AcpJobPhases[memoToSign.nextPhase] : "None"}`, |
| 54 | + ); |
| 55 | + |
| 56 | + // Subscription payment requested (Scenario 1) |
| 57 | + if ( |
| 58 | + job.phase === AcpJobPhases.NEGOTIATION && |
| 59 | + memoToSign?.type === MemoType.PAYABLE_REQUEST_SUBSCRIPTION |
| 60 | + ) { |
| 61 | + console.log( |
| 62 | + `Buyer: Job ${job.id} — Subscription payment requested: ${memoToSign.content}`, |
| 63 | + ); |
| 64 | + console.log( |
| 65 | + `Buyer: Job ${job.id} — Amount: ${memoToSign.payableDetails?.amount}`, |
| 66 | + ); |
| 67 | + const { txnHash: subPayTx } = await job.paySubscription( |
| 68 | + `Subscription payment for ${SUBSCRIPTION_TIER}`, |
| 69 | + ); |
| 70 | + console.log( |
| 71 | + `Buyer: Job ${job.id} — Subscription paid (tx: ${subPayTx})`, |
| 72 | + ); |
| 73 | + |
| 74 | + // Fixed-price requirement — pay and advance to delivery (Scenario 2) |
| 75 | + } else if ( |
| 76 | + job.phase === AcpJobPhases.NEGOTIATION && |
| 77 | + memoToSign?.type === MemoType.PAYABLE_REQUEST |
| 78 | + ) { |
| 79 | + console.log( |
| 80 | + `Buyer: Job ${job.id} — Fixed-price requirement, paying now`, |
| 81 | + ); |
| 82 | + const payResult = await job.payAndAcceptRequirement("Payment for job"); |
| 83 | + console.log( |
| 84 | + `Buyer: Job ${job.id} — Paid and advanced to TRANSACTION phase (tx: ${payResult?.txnHash})`, |
| 85 | + ); |
| 86 | + |
| 87 | + // Active subscription path — accept requirement without payment |
| 88 | + } else if ( |
| 89 | + job.phase === AcpJobPhases.NEGOTIATION && |
| 90 | + memoToSign?.type === MemoType.MESSAGE && |
| 91 | + memoToSign?.nextPhase === AcpJobPhases.TRANSACTION |
| 92 | + ) { |
| 93 | + console.log( |
| 94 | + `Buyer: Job ${job.id} — Subscription active, accepting without payment`, |
| 95 | + ); |
| 96 | + const { txnHash: signMemoTx } = await job.acceptRequirement( |
| 97 | + memoToSign, |
| 98 | + "Subscription verified, proceeding to delivery", |
| 99 | + ); |
| 100 | + console.log( |
| 101 | + `Buyer: Job ${job.id} — Advanced to TRANSACTION phase (tx: ${signMemoTx})`, |
| 102 | + ); |
| 103 | + } else if (job.phase === AcpJobPhases.COMPLETED) { |
| 104 | + console.log( |
| 105 | + `Buyer: Job ${job.id} — Completed! Deliverable:`, |
| 106 | + job.deliverable, |
| 107 | + ); |
| 108 | + } else if (job.phase === AcpJobPhases.REJECTED) { |
| 109 | + console.log( |
| 110 | + `Buyer: Job ${job.id} — Rejected. Reason:`, |
| 111 | + job.rejectionReason, |
| 112 | + ); |
| 113 | + } else { |
| 114 | + console.log( |
| 115 | + `Buyer: Job ${job.id} — Unhandled event (phase: ${AcpJobPhases[job.phase]}, ` + |
| 116 | + `memoType: ${memoToSign?.type !== undefined ? MemoType[memoToSign.type] : "None"}, ` + |
| 117 | + `nextPhase: ${memoToSign?.nextPhase !== undefined ? AcpJobPhases[memoToSign.nextPhase] : "None"})`, |
| 118 | + ); |
| 119 | + } |
| 120 | + }, |
| 121 | + }); |
| 122 | + |
| 123 | + // Browse available agents |
| 124 | + const relevantAgents = await acpClient.browseAgents("", { |
| 125 | + sortBy: [AcpAgentSort.SUCCESSFUL_JOB_COUNT], |
| 126 | + topK: 5, |
| 127 | + graduationStatus: AcpGraduationStatus.ALL, |
| 128 | + onlineStatus: AcpOnlineStatus.ALL, |
| 129 | + showHiddenOfferings: true, |
| 130 | + }); |
| 131 | + |
| 132 | + console.log("Relevant agents:", relevantAgents); |
| 133 | + |
| 134 | + if (!relevantAgents || relevantAgents.length === 0) { |
| 135 | + console.error("No agents found"); |
| 136 | + return; |
| 137 | + } |
| 138 | + |
| 139 | + // Pick one of the agents based on your criteria (in this example we just pick the first one) |
| 140 | + const chosenAgent = relevantAgents[0]; |
| 141 | + |
| 142 | + // Pick one of the service offerings based on your criteria: |
| 143 | + // - index 0: subscription offering |
| 144 | + // - index 1: non-subscription (fixed-price) offering |
| 145 | + const subscriptionOffering = chosenAgent.jobOfferings[0]; |
| 146 | + const fixedOffering = chosenAgent.jobOfferings[1]; |
| 147 | + |
| 148 | + switch (SCENARIO) { |
| 149 | + case 1: { |
| 150 | + const chosenJobOffering = subscriptionOffering; |
| 151 | + const jobId = await chosenJobOffering.initiateJob( |
| 152 | + // Requirement payload schema depends on your ACP service configuration. |
| 153 | + // If your service requires fields, replace {} with the expected schema payload. |
| 154 | + {}, |
| 155 | + undefined, // evaluator address, undefined fallback to empty address |
| 156 | + new Date(Date.now() + 1000 * 60 * 15), // job expiry duration, minimum 5 minutes |
| 157 | + SUBSCRIPTION_TIER, |
| 158 | + ); |
| 159 | + console.log(`Buyer: [Scenario 1 — Subscription Offering] Job ${jobId} initiated`); |
| 160 | + break; |
| 161 | + } |
| 162 | + |
| 163 | + case 2: { |
| 164 | + const chosenJobOffering = fixedOffering; |
| 165 | + const jobId = await chosenJobOffering.initiateJob( |
| 166 | + // Requirement payload schema depends on your ACP service configuration. |
| 167 | + // If your service requires fields, replace {} with the expected schema payload. |
| 168 | + {}, |
| 169 | + undefined, // evaluator address, undefined fallback to empty address |
| 170 | + new Date(Date.now() + 1000 * 60 * 15), // job expiry duration, minimum 5 minutes |
| 171 | + ); |
| 172 | + console.log(`Buyer: [Scenario 2 — Fixed-Price Job] Job ${jobId} initiated`); |
| 173 | + break; |
| 174 | + } |
| 175 | + |
| 176 | + default: |
| 177 | + console.error(`Unknown scenario: ${SCENARIO}. Use --scenario 1 or 2.`); |
| 178 | + process.exit(1); |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +buyer().catch((error) => { |
| 183 | + console.error("Buyer error:", error); |
| 184 | + process.exit(1); |
| 185 | +}); |
0 commit comments