@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

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:

@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}`,
  };
}

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
}