Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt

Use this file to discover all available pages before exploring further.

KaireonAI’s arbitration runs in two stages: a hard-constraint filter that drops any offer that can’t be served right now, then the weighted composite score (PRIE / multi-objective) that ranks the survivors. This page covers both stages plus the optional agent-in-the-loop negotiation that can propose bounded tweaks after a winner is selected.

Hard constraints — what gets dropped before ranking

Three constraint types are enforced on every recommendation, before scoring:

Budget caps

  • Offer.budget.dailyCapCents — daily spend ceiling (auto-resets at day boundary)
  • Offer.budget.lifetimeCapCents — lifetime spend ceiling (never resets)
  • Running totals (currentDailySpentCents, currentLifetimeSpentCents) update automatically on positive outcomes via consumeBudget in the /respond flow
  • lastDailyResetDate bookkeeping is handled by the platform

Inventory counters

  • Offer.inventory.totalStock — starting stock
  • Offer.inventory.remainingStock — decrements on positive outcomes; offer drops from candidate set at 0
  • Unset = untracked (no inventory enforcement)

Frequency caps

  • Offer.frequencyCaps.perCustomer.{daily,weekly,monthly} — max impressions per customer per window
  • Counts are read from InteractionHistory with a single batched query per window (not N+1 across candidate offers)
  • Fail-open: if the counter query throws, the offer is allowed through and the failure is logged

Example offer with constraints

{
  "name": "Gold Card Q2 Promo",
  "priority": 80,
  "budget": {
    "dailyCapCents": 50000,
    "lifetimeCapCents": 5000000
  },
  "inventory": {
    "totalStock": 1000,
    "remainingStock": 1000
  },
  "frequencyCaps": {
    "perCustomer": { "daily": 1, "weekly": 3, "monthly": 6 }
  }
}
When recommend runs for this offer, it will be dropped from candidates if:
  • Today’s spend has hit $500.00
  • Lifetime spend has hit $50,000.00
  • Remaining stock is 0
  • This customer has already seen this offer today / 3 times this week / 6 times this month

What is NOT supported in V1

  • Lagrangian relaxation for multi-constraint soft-optimization — everything here is a hard binary filter.
  • Cross-offer frequency caps (e.g., “show this customer at most 5 offers total per day across all offers”) — only per-offer.
  • Budget alerting / refill workflows — you have to update dailyCapCents / lifetimeCapCents manually.
  • Time-of-day or seasonality modulation — constraints are evaluated at decision time with current state.
These are tracked on the internal roadmap as follow-on phases.

Agent-in-the-loop negotiation (shadow-mode)

After arbitration selects a winner, some offers support a negotiation pass where an LLM agent proposes bounded tweaks — a small discount, a different term length, an add-on — within hard guardrails.
Shadow-mode only in V1. Every proposal is validated against the guardrails in code (not just the prompt), and recorded in the audit log. The proposal is NEVER applied to a real customer in V1. Apply-mode will ship only after an offline eval harness proves zero guardrail violations over a production-quality sample.

Enabling negotiation

Three gates must pass, all fail-closed:
  1. Tenant opt-in:
    PUT /api/v1/ai/explanations-settings
    { "aiAnalyzerSettings": { "negotiationEnabled": true } }
    
  2. Per-offer flag: Offer.negotiable = true
  3. Per-offer guardrails: Offer.negotiationGuardrails with explicit bounds.

Guardrail schema

{
  "discount":        { "minPct": 0, "maxPct": 15 },
  "term":            { "minMonths": 6, "maxMonths": 24 },
  "priceFloorCents": 1000,
  "allowedCurrencies": ["USD", "EUR"],
  "bundleableAddons":  ["free-shipping", "extended-warranty"],
  "maxProposals":      1
}
Anything missing means “not permitted” — if discount is absent, the agent MUST NOT propose a discount.

Calling the agent

POST /api/v1/decisions/{decisionTraceId}/negotiate
{ "offerId": "...", "mode": "shadow" }
Response shape: a NegotiationSession with proposals: ValidatedProposal[]. Each proposal has:
  • valid: boolean
  • proposal: { rationale, discountPct?, termMonths?, bundleAddons?, finalPriceCents?, currency? } | null
  • violations: GuardrailViolation[] — a typed list from 9 possible violation codes
Every session writes an AuditLog entry with action="negotiate_shadow" and entityType="decision_trace" — so DPO and audit teams can see exactly what the agent considered, even when nothing was applied.

What the agent cannot do

Enforced by code (platform/src/lib/negotiation/guardrails.ts):
  • Propose a discount outside [discount.minPct, discount.maxPct]discount_below_floor / discount_above_ceiling
  • Propose a discount when none is permitted → discount_not_permitted
  • Propose a term length outside the band → term_below_floor / term_above_ceiling
  • Propose a final price below priceFloorCentsprice_below_floor
  • Use a currency not in allowedCurrenciescurrency_not_allowed
  • Bundle an add-on not in bundleableAddonsaddon_not_permitted
  • Omit the rationale field → rationale_missing
  • Exceed maxProposals in a single session → trailing proposals marked schema_invalid
A proposal with any violation is returned with valid: false and proposal: null. There is no code path that stores or surfaces a rejected proposal — the validation happens before any mutation.

Rate limits + cost

  • Negotiation endpoint: 10 requests / minute / tenant (LLM cost control)
  • No negotiation call ever runs on the /recommend hot path
  • Configured AI provider is used (Claude / GPT / Gemini / Bedrock / Ollama / LM Studio — whatever is set in AiConfig)

Promotion path to apply-mode

Not in V1. When we ship it, the gate will be:
  1. Offline eval harness runs the agent against a ≥1000-session dataset
  2. Zero guardrail violations (hard requirement)
  3. Regulatory review on the proposal distribution
  4. Tenant-level admin explicitly opts in, per-offer
  5. Real-time kill switch wired in tenant settings
Only then does mode: "apply" become reachable.