@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

EventACP EndpointDescription
searchPOST /acp/searchSemantic product discovery
quotePOST /acp/quotePrice quote and negotiation
checkoutPOST /acp/checkout/initInitialize checkout session
finalizePOST /acp/checkout/finalizeProcess 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 safety
import { 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.