@OnACPEvent
Handle Agentic Commerce Protocol events like search, quote, and checkout.
Overview
The @OnACPEvent decorator marks methods that handle incoming ACP protocol requests. Each event type corresponds to a step in the agent commerce flow.
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { HyperfoldAgent, OnACPEvent } from '@hyperfold/actions-sdk'; @HyperfoldAgent({ name: 'sales-bot', type: 'negotiator' })export class SalesBot { @OnACPEvent('search') async handleSearch(query: string, filters: SearchFilters): Promise<SearchResponse> { // Handle semantic product search } @OnACPEvent('quote') async handleQuote(productId: string, offer: number, context: BuyerContext): Promise<QuoteResponse> { // Handle price quote requests } @OnACPEvent('checkout') async handleCheckout(sessionId: string, items: CartItem[]): Promise<CheckoutResponse> { // Initialize checkout session } @OnACPEvent('finalize') async handleFinalize(checkoutId: string, paymentToken: string): Promise<FinalizeResponse> { // Process payment and create order }}Available Events
| Event | ACP Endpoint | Description |
|---|---|---|
search | POST /acp/search | Semantic product discovery |
quote | POST /acp/quote | Price quote and negotiation |
checkout | POST /acp/checkout/init | Initialize checkout session |
finalize | POST /acp/checkout/finalize | Process payment and create order |
Search Event
Handle semantic product search queries:
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@OnACPEvent('search')async handleSearch( query: string, filters: SearchFilters, context: RequestContext): Promise<SearchResponse> { // Perform semantic search const products = await this.catalog.semanticSearch(query, { limit: filters.limit || 10, minConfidence: 0.7, priceMax: filters.price_max, category: filters.category, }); // Boost in-stock items const boosted = products.map(p => ({ ...p, confidence: p.in_stock ? p.confidence * 1.1 : p.confidence, })); return { results: boosted.slice(0, filters.limit || 10), total_count: products.length, semantic_confidence: boosted[0]?.confidence || 0, facets: this.generateFacets(products), };} // Incoming ACP request:// POST /acp/search// {// "query": "waterproof running shoes",// "filters": {// "price_max": 200,// "size": "10"// },// "limit": 5// } // Response:// {// "results": [...],// "total_count": 12,// "semantic_confidence": 0.94// }Quote Event
Handle price quotes and negotiation:
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@OnACPEvent('quote')async handleQuote( productId: string, offer: number | null, context: BuyerContext): Promise<QuoteResponse> { // Get product with current pricing const product = await getProduct(productId); const inventory = await checkInventory(productId); // Calculate dynamic price based on context const pricing = await calculateDynamicPrice(product, { buyerTier: context.loyalty_tier, cartValue: context.cart_value, inventoryLevel: inventory.status, competitorPrice: await getCompetitorPrice(productId), }); // No offer provided - return asking price if (offer === null) { return { status: 'quote', list_price: product.list_price, offered_price: pricing.suggested, valid_until: new Date(Date.now() + 3600000).toISOString(), }; } // Evaluate the offer if (offer >= pricing.target) { // Accept - offer meets our target return { status: 'accept', price: offer, message: "Great choice! I'll process your order.", }; } if (offer >= pricing.floor) { // Counter - offer is acceptable but we can do better return { status: 'counter_offer', original_price: product.list_price, counter_price: pricing.suggested, reasoning: pricing.explanation, valid_until: new Date(Date.now() + 3600000).toISOString(), bundle_suggestion: await this.suggestBundle(productId), }; } // Reject - offer is below floor return { status: 'reject', reason: 'Price is below our minimum', floor_hint: `Our best price is around $${Math.ceil(pricing.floor / 5) * 5}`, };}The quote handler is where most negotiation logic lives. Use the pricing tools to calculate dynamic prices based on context.
Checkout Events
Handle checkout initialization and payment finalization:
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
@OnACPEvent('checkout')async handleCheckout( sessionId: string, items: CartItem[], shippingAddress?: Address): Promise<CheckoutResponse> { // Validate items are still available for (const item of items) { const inventory = await checkInventory(item.product_id); if (inventory.quantity < item.quantity) { return { status: 'error', error: 'insufficient_inventory', message: `Only ${inventory.quantity} units of ${item.name} available`, }; } } // Calculate totals const subtotal = items.reduce((sum, i) => sum + i.price * i.quantity, 0); const shipping = await calculateShipping(items, shippingAddress); const tax = await calculateTax(subtotal, shippingAddress); // Create checkout session const checkout = await this.createCheckoutSession({ session_id: sessionId, items, subtotal, shipping: shipping.cost, tax, total: subtotal + shipping.cost + tax, shipping_estimate: shipping.estimate, }); return { status: 'ready', checkout_id: checkout.id, summary: { items: items.length, subtotal, shipping: shipping.cost, tax, total: checkout.total, }, shipping_options: shipping.options, valid_until: checkout.expires_at, };} @OnACPEvent('finalize')async handleFinalize( checkoutId: string, paymentToken: string, shippingAddress: Address): Promise<FinalizeResponse> { const checkout = await this.getCheckoutSession(checkoutId); // Validate checkout hasn't expired if (new Date(checkout.expires_at) < new Date()) { return { status: 'error', error: 'checkout_expired', message: 'Checkout session has expired. Please start over.', }; } // Process payment via SPT const payment = await processPayment({ token: paymentToken, amount: checkout.total, currency: 'USD', metadata: { checkout_id: checkoutId, agent: 'sales-bot-01', }, }); if (payment.status !== 'succeeded') { return { status: 'payment_failed', error: payment.error_code, message: payment.error_message, }; } // Create order const order = await createOrder({ checkout, payment_id: payment.id, shipping_address: shippingAddress, }); // Trigger fulfillment await this.events.publish('order.completed', { order }); return { status: 'success', order_id: order.id, confirmation_number: order.confirmation, estimated_delivery: order.shipping.estimate, receipt_url: payment.receipt_url, };}Error Handling
Return structured errors that buyer agents can handle:
typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@OnACPEvent('quote')async handleQuote(productId: string, offer: number, context: BuyerContext) { try { const product = await getProduct(productId); if (!product) { // Return structured error for ACP return { status: 'error', error: 'product_not_found', message: `Product ${productId} not found`, }; } // ... rest of quote logic } catch (error) { // Log for debugging console.error('Quote error:', error); // Return graceful error to buyer return { status: 'error', error: 'internal_error', message: 'Unable to process quote at this time', retry_after: 60, // Suggest retry in 60 seconds }; }} // Validation decorator for type safetyimport { ValidateInput } from '@hyperfold/actions-sdk'; @OnACPEvent('quote')@ValidateInput({ productId: { type: 'string', required: true }, offer: { type: 'number', min: 0 }, context: { type: 'object' },})async handleQuote(productId: string, offer: number, context: BuyerContext) { // Input is guaranteed to be valid}Schedule recurring tasks with @OnSchedule.