@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.
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:
@OnACPEvent('search')
async handleSearch(
query: string,
filters: SearchFilters,
context: RequestContext
): Promise<SearchResponse> {
const products = await this.catalog.semanticSearch(query, {
limit: filters.limit || 10,
minConfidence: 0.7,
priceMax: filters.price_max,
category: filters.category,
});
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
// Response: { results: [...], total_count: 12, semantic_confidence: 0.94 }
Quote Event
Handle price quotes and negotiation:
@OnACPEvent('quote')
async handleQuote(
productId: string,
offer: number | null,
context: BuyerContext
): Promise<QuoteResponse> {
const product = await getProduct(productId);
const inventory = await checkInventory(productId);
const pricing = await calculateDynamicPrice(product, {
buyerTier: context.loyalty_tier,
cartValue: context.cart_value,
inventoryLevel: inventory.status,
competitorPrice: await getCompetitorPrice(productId),
});
if (offer === null) {
return {
status: 'quote',
list_price: product.list_price,
offered_price: pricing.suggested,
valid_until: new Date(Date.now() + 3600000).toISOString(),
};
}
if (offer >= pricing.target) {
return {
status: 'accept',
price: offer,
message: "Great choice! I'll process your order.",
};
}
if (offer >= pricing.floor) {
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),
};
}
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:
@OnACPEvent('checkout')
async handleCheckout(
sessionId: string,
items: CartItem[],
shippingAddress?: Address
): Promise<CheckoutResponse> {
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`,
};
}
}
const subtotal = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
const shipping = await calculateShipping(items, shippingAddress);
const tax = await calculateTax(subtotal, shippingAddress);
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);
if (new Date(checkout.expires_at) < new Date()) {
return {
status: 'error',
error: 'checkout_expired',
message: 'Checkout session has expired. Please start over.',
};
}
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,
};
}
const order = await createOrder({
checkout,
payment_id: payment.id,
shipping_address: shippingAddress,
});
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:
@OnACPEvent('quote')
async handleQuote(productId: string, offer: number, context: BuyerContext) {
try {
const product = await getProduct(productId);
if (!product) {
return {
status: 'error',
error: 'product_not_found',
message: `Product ${productId} not found`,
};
}
// ... rest of quote logic
} catch (error) {
console.error('Quote error:', error);
return {
status: 'error',
error: 'internal_error',
message: 'Unable to process quote at this time',
retry_after: 60,
};
}
}
// 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.