BotWallet.io
API Documentation

Authorization Stream Access

BotWallet intercepts every card authorization in real time via Lithic's Auth Stream Access (ASA) protocol. This document describes the full request lifecycle, type definitions, and policy evaluation logic.

Overview

When an AI agent uses a BotWallet virtual card, Lithic sends an authorization request to BotWallet's ASA endpoint in real time. BotWallet evaluates the request against the card's policy, responds with APPROVE or DECLINE, and persists a full audit record — all within Lithic's ~2 second timeout window.

ASA Flow

Each authorization follows this six-step lifecycle:

  1. 01
    Signature verificationHMAC-SHA256 of the raw request body using LITHIC_WEBHOOK_SECRET. Fail-closed → 401 on mismatch.
  2. 02
    Idempotency checkIn-memory Map keyed on token. Durable safety net via UNIQUE constraint on transactions.token.
  3. 03
    Parallel DB fetchPolicy row + today's approved spend fetched concurrently from Supabase.
  4. 04
    Policy evaluationPure evaluateTransaction() — no I/O, deterministic, fully unit-tested.
  5. 05
    Audit log insertTransaction record persisted to Supabase. Write failures are logged but never surface to Lithic.
  6. 06
    Response{ result: "APPROVE" | "DECLINE" } returned to Lithic.

Policy & Decision Types

Three core TypeScript interfaces power the authorization flow.

typescript
// src/types/policy.ts

export interface ASARequest {
  token:             string          // Unique per-authorization token
  card_token:        string          // Identifies the virtual card
  merchant_amount:   number          // Amount in minor units (cents)
  merchant_currency: string          // ISO 4217 currency code (e.g. "USD")
  merchant: {
    descriptor:      string          // Merchant name (e.g. "OPENAI * API")
    mcc:             string          // Merchant Category Code
  }
}

export interface Policy {
  dailyLimit:        number          // Max spend per UTC day, in cents
  allowedMerchants:  string[]        // Substring-matched against descriptor
  allowedCurrencies: string[]        // Exact match against merchant_currency
}

export interface Decision {
  action:  'APPROVE' | 'DECLINE'
  ruleId:  string                    // Which rule fired
  reason:  string                    // Human-readable explanation
}

Rule IDs

Every Decision includes a ruleId identifying which rule triggered the outcome.

text
// Rule IDs returned in every Decision

RULE_ALL_PASSED           // All checks passed → APPROVE
RULE_DAILY_LIMIT          // Amount would exceed daily spending cap → DECLINE
RULE_MERCHANT_NOT_ALLOWED // Merchant descriptor not in allowedMerchants → DECLINE
RULE_CURRENCY_MISMATCH    // Currency not in allowedCurrencies → DECLINE
RULE_NO_POLICY            // No policy found for card_token → DECLINE

Policy Engine

evaluateTransaction is a pure function — no database calls, no side effects. Rules are evaluated in priority order: currency mismatch → daily limit → merchant allowlist.

typescript
// src/engine/policyEngine.ts (simplified)

export function evaluateTransaction(
  tx:         ASARequest,
  policy:     Policy,
  spentToday: number = 0,   // Sum of approved amounts today (cents)
): Decision {

  // 1. Currency check
  if (!policy.allowedCurrencies.includes(tx.merchant_currency)) {
    return {
      action: 'DECLINE',
      ruleId: 'RULE_CURRENCY_MISMATCH',
      reason: `Currency "${tx.merchant_currency}" is not permitted.`,
    }
  }

  // 2. Daily limit check
  if (spentToday + tx.merchant_amount > policy.dailyLimit) {
    return {
      action: 'DECLINE',
      ruleId: 'RULE_DAILY_LIMIT',
      reason: `Spend would exceed daily limit of ${policy.dailyLimit / 100} USD.`,
    }
  }

  // 3. Merchant allowlist check (case-insensitive substring)
  const desc = tx.merchant.descriptor.toLowerCase()
  const allowed = policy.allowedMerchants.some(m => desc.includes(m.toLowerCase()))
  if (!allowed) {
    return {
      action: 'DECLINE',
      ruleId: 'RULE_MERCHANT_NOT_ALLOWED',
      reason: `Merchant "${tx.merchant.descriptor}" is not on the allowlist.`,
    }
  }

  return {
    action: 'APPROVE',
    ruleId: 'RULE_ALL_PASSED',
    reason: 'All policy rules passed.',
  }
}

Webhook Payload

Lithic POSTs this JSON body to POST /webhooks/lithic/auth. BotWallet must respond within ~2 000 ms.

json
// Lithic sends this payload to POST /webhooks/lithic/auth

{
  "token":             "asa_01HXYZ...",
  "card_token":        "card_gpt4_prod",
  "merchant_amount":   2500,
  "merchant_currency": "USD",
  "merchant": {
    "descriptor": "OPENAI * API",
    "mcc":        "7372"
  }
}
json
// BotWallet must respond within ~2 000 ms

// Approved
{ "result": "APPROVE" }

// Declined
{ "result": "DECLINE" }

Policy API

Update a card's daily spending limit via the admin API. Changes take effect on the next authorization — no restart required.

typescript
// PATCH /api/admin/policy
// Auth: bw_admin cookie (set via /admin login)

fetch('/api/admin/policy', {
  method:  'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    card_token:           'card_gpt4_prod',
    daily_limit_dollars:  500,            // $500.00/day
  }),
})

Fail-Safe Defaults

No policy for card_tokenDECLINE + RULE_NO_POLICY audit record
Supabase unavailableDECLINE + error logged (financial fail-closed)
Audit insert failureLogged, never surfaces to Lithic (no timeout risk)
Invalid signature401 Unauthorized, request rejected immediately
Malformed payload400 Bad Request, no DB write attempted

Ready to integrate? Request sandbox access or view pricing.