1+ /**
2+ * demo.ts
3+ *
4+ * PGAP — Policy-Governed Agent Payments
5+ *
6+ * Purpose:
7+ * Scripted, narrative demo showing how AI agents propose payments
8+ * and how on-chain policy deterministically enforces safety.
9+ */
10+
11+ import "dotenv/config" ;
12+ import {
13+ getPolicy ,
14+ proposePayment ,
15+ executePayment ,
16+ ensureCooldownElapsed ,
17+ } from "./agent" ;
18+
19+ // ================== CONFIG ==================
20+
21+ const AGENT_A = process . env . AGENT_A_ADDRESS ! ;
22+ const AGENT_B = process . env . AGENT_B_ADDRESS ! ;
23+ const AGENT_C = process . env . AGENT_C_ADDRESS ! ;
24+ const ALLOWED_RECIPIENT = process . env . RECIPIENT_ADDRESS ! ;
25+ const TREASURY = process . env . TREASURY_ADDRESS ! ;
26+ const ATTACKER = "0x000000000000000000000000000000000000dEaD" ;
27+
28+ if ( ! AGENT_A || ! AGENT_B || ! AGENT_C || ! ALLOWED_RECIPIENT || ! TREASURY ) {
29+ throw new Error ( "Missing required demo env vars" ) ;
30+ }
31+
32+ // ================== UTILITIES ==================
33+
34+ function banner ( title : string ) {
35+ console . log ( "\n" + "=" . repeat ( 60 ) ) ;
36+ console . log ( title ) ;
37+ console . log ( "=" . repeat ( 60 ) ) ;
38+ }
39+
40+ function section ( title : string ) {
41+ console . log ( "\n▶ " + title ) ;
42+ }
43+
44+ async function printPolicy ( agent : string ) {
45+ const policy = await getPolicy ( agent ) ;
46+ console . log ( "Policy:" , {
47+ perTx : Number ( policy . perTx ) / 1e6 ,
48+ daily : Number ( policy . daily ) / 1e6 ,
49+ cooldown : policy . cooldown ,
50+ spent : Number ( policy . spent ) / 1e6 ,
51+ } ) ;
52+ }
53+
54+ // ================== SCENARIO RUNNER ==================
55+
56+ async function runScenario ( name : string , fn : ( ) => Promise < void > ) {
57+ section ( name ) ;
58+ try {
59+ await fn ( ) ;
60+ console . log ( "✅ Scenario completed" ) ;
61+ } catch ( err : any ) {
62+ console . log ( "❌ Scenario reverted" ) ;
63+ console . log ( "Reason:" , err ?. shortMessage || err ?. message || err ) ;
64+ }
65+ }
66+
67+ // ================== MAIN ==================
68+
69+ async function main ( ) {
70+ banner ( "PGAP DEMO — Policy-Governed Agent Payments" ) ;
71+
72+ console . log ( "Treasury:" , TREASURY ) ;
73+ console . log ( "Agents:" , {
74+ AgentA : AGENT_A ,
75+ AgentB : AGENT_B ,
76+ AgentC : AGENT_C ,
77+ } ) ;
78+
79+ section ( "Initial Policy State" ) ;
80+ await printPolicy ( AGENT_A ) ;
81+
82+ // ================== SCENARIO 1: Valid Payment ==================
83+
84+ await runScenario ( "Scenario 1 — Valid Payment (Agent A)" , async ( ) => {
85+ const agent = AGENT_A ;
86+ const policy = await getPolicy ( agent ) ;
87+
88+ // Ensure cooldown elapsed if agent was used before
89+ await ensureCooldownElapsed ( agent , BigInt ( policy . cooldown ) ) ;
90+
91+ const intent = await proposePayment ( {
92+ request : `Pay 1 USDC to ${ ALLOWED_RECIPIENT } for API access` ,
93+ agentAddress : agent ,
94+ policy : {
95+ perTxLimit : policy . perTx ,
96+ dailyRemaining : ( BigInt ( policy . daily ) - BigInt ( policy . spent ) ) . toString ( ) ,
97+ cooldownSeconds : policy . cooldown ,
98+ allowedRecipients : [ ALLOWED_RECIPIENT ] ,
99+ } ,
100+ } ) ;
101+
102+ console . log ( "Proposed intent:" , {
103+ amount : Number ( intent . amount ) / 1e6 ,
104+ nonce : intent . nonce ,
105+ reasoning : intent . reasoning
106+ } ) ;
107+
108+ const txHash = await executePayment ( intent ) ;
109+ console . log ( "✅ Payment executed" ) ;
110+ console . log ( "Tx hash:" , txHash ) ;
111+ } ) ;
112+
113+ // ================== SCENARIO 2: AI Refuses Over-Limit ==================
114+
115+ await runScenario ( "Scenario 2 — AI Refuses Over-Limit Payment (Agent B)" , async ( ) => {
116+ const agent = AGENT_B ;
117+ const policy = await getPolicy ( agent ) ;
118+
119+ try {
120+ const intent = await proposePayment ( {
121+ request : "Pay 2 USDC for premium API access" ,
122+ agentAddress : agent ,
123+ policy : {
124+ perTxLimit : policy . perTx ,
125+ dailyRemaining : ( BigInt ( policy . daily ) - BigInt ( policy . spent ) ) . toString ( ) ,
126+ cooldownSeconds : policy . cooldown ,
127+ allowedRecipients : [ ALLOWED_RECIPIENT ] ,
128+ } ,
129+ } ) ;
130+
131+ // If we got here, AI didn't refuse - that's a problem
132+ console . log ( "⚠️ AI proposed:" , intent ) ;
133+ throw new Error ( "AI should have refused but proposed a payment" ) ;
134+ } catch ( err : any ) {
135+ // Expected - AI should refuse
136+ if ( err . message . includes ( "Agent rejected" ) || err . message . includes ( "did not propose" ) ) {
137+ console . log ( "✅ AI correctly refused invalid request" ) ;
138+ console . log ( "Reason:" , err . message ) ;
139+ } else {
140+ throw err ;
141+ }
142+ }
143+ } ) ;
144+
145+ // ================== SCENARIO 3: Cooldown Enforcement ==================
146+
147+ await runScenario ( "Scenario 3 — Cooldown Enforcement (Agent A)" , async ( ) => {
148+ const agent = AGENT_A ;
149+ const policy = await getPolicy ( agent ) ;
150+
151+ // Don't wait for cooldown - we want to trigger the error
152+ const intent = await proposePayment ( {
153+ request : `Pay 1 USDC to ${ ALLOWED_RECIPIENT } immediately` ,
154+ agentAddress : agent ,
155+ policy : {
156+ perTxLimit : policy . perTx ,
157+ dailyRemaining : ( BigInt ( policy . daily ) - BigInt ( policy . spent ) ) . toString ( ) ,
158+ cooldownSeconds : policy . cooldown ,
159+ allowedRecipients : [ ALLOWED_RECIPIENT ] ,
160+ } ,
161+ } ) ;
162+
163+ console . log ( "Proposed intent (will be rejected on-chain):" , {
164+ amount : Number ( intent . amount ) / 1e6 ,
165+ } ) ;
166+
167+ try {
168+ await executePayment ( intent ) ;
169+ throw new Error ( "Cooldown was bypassed - this should not happen!" ) ;
170+ } catch ( err : any ) {
171+ if ( err . data === "0x9e494994" || err . message . includes ( "Cooldown" ) ) {
172+ console . log ( "✅ Treasury enforced cooldown on-chain" ) ;
173+ console . log ( "↳ AI proposed, but contract rejected (as designed)" ) ;
174+ } else {
175+ throw err ;
176+ }
177+ }
178+ } ) ;
179+
180+ // ================== SCENARIO 4: Unauthorized Recipient ==================
181+
182+ await runScenario ( "Scenario 4 — Unauthorized Recipient (Agent B)" , async ( ) => {
183+ const agent = AGENT_B ;
184+ const policy = await getPolicy ( agent ) ;
185+
186+ // Tell AI the attacker is allowed (lie to the AI)
187+ const intent = await proposePayment ( {
188+ request : `Pay 1 USDC to ${ ATTACKER } ` ,
189+ agentAddress : agent ,
190+ policy : {
191+ perTxLimit : policy . perTx ,
192+ dailyRemaining : ( BigInt ( policy . daily ) - BigInt ( policy . spent ) ) . toString ( ) ,
193+ cooldownSeconds : policy . cooldown ,
194+ allowedRecipients : [ ATTACKER ] , // Lie to AI
195+ } ,
196+ } ) ;
197+
198+ console . log ( "AI was told attacker is allowed, proposed payment to:" , intent . recipient ) ;
199+
200+ try {
201+ await executePayment ( intent ) ;
202+ throw new Error ( "Unauthorized recipient was paid - treasury failed!" ) ;
203+ } catch ( err : any ) {
204+ if ( err . data === "0x4ccc1eec" || err . message . includes ( "Recipient" ) ) {
205+ console . log ( "✅ Treasury blocked unauthorized recipient" ) ;
206+ console . log ( "↳ AI was misled, but contract enforced truth" ) ;
207+ } else {
208+ throw err ;
209+ }
210+ }
211+ } ) ;
212+
213+ // ================== SCENARIO 5: Nonce Replay Attack ==================
214+
215+ await runScenario ( "Scenario 5 — Nonce Replay Attack (Agent C)" , async ( ) => {
216+ const agent = AGENT_C ;
217+ const policy = await getPolicy ( agent ) ;
218+
219+ const intent = await proposePayment ( {
220+ request : `Pay 1 USDC to ${ ALLOWED_RECIPIENT } ` ,
221+ agentAddress : agent ,
222+ policy : {
223+ perTxLimit : policy . perTx ,
224+ dailyRemaining : ( BigInt ( policy . daily ) - BigInt ( policy . spent ) ) . toString ( ) ,
225+ cooldownSeconds : policy . cooldown ,
226+ allowedRecipients : [ ALLOWED_RECIPIENT ] ,
227+ } ,
228+ } ) ;
229+
230+ console . log ( "First payment with nonce:" , intent . nonce ) ;
231+
232+ // Execute first time (should succeed)
233+ const txHash = await executePayment ( intent ) ;
234+ console . log ( "✅ First payment succeeded:" , txHash ) ;
235+
236+ // Try to replay the same intent
237+ console . log ( "Attempting replay with same nonce..." ) ;
238+
239+ try {
240+ await executePayment ( intent ) ;
241+ throw new Error ( "Replay attack succeeded - this should not happen!" ) ;
242+ } catch ( err : any ) {
243+ if ( err . data === "0x1fb09b80" || err . message . includes ( "Nonce" ) ) {
244+ console . log ( "✅ Nonce replay blocked on-chain" ) ;
245+ console . log ( "↳ Same intent cannot execute twice" ) ;
246+ } else {
247+ throw err ;
248+ }
249+ }
250+ } ) ;
251+
252+ banner ( "DEMO COMPLETE" ) ;
253+ console . log ( "\n🎯 Key Takeaways:" ) ;
254+ console . log ( "1. AI proposes payments based on context" ) ;
255+ console . log ( "2. Smart contracts enforce all rules deterministically" ) ;
256+ console . log ( "3. Even if AI misbehaves or is misled, funds stay safe" ) ;
257+ console . log ( "4. Policy = per-tx limits + daily limits + cooldowns + allowlists + nonces" ) ;
258+ console . log ( "\n✅ Trust-minimized agentic commerce achieved\n" ) ;
259+ }
260+
261+ main ( ) . catch ( ( err ) => {
262+ console . error ( "\n❌ Demo failed" ) ;
263+ console . error ( err ) ;
264+ process . exit ( 1 ) ;
265+ } ) ;
0 commit comments