Skip to content
AGNT

Backend · Core modules

Commerce & fees.

How AGNT charges venues for successful bookings, how the fee is calculated, how the Stripe charge is idempotent, and how the commerce_ledger table keeps the whole story auditable. File: agnt-backend/app/core/commerce.py.

The pricing model

There are two fee models, picked automatically based on whether the venue has an active Stripe subscription:

  1. Referral percent— applied to subscribed venues. The default is 7% of the booking value, with a $1.50 floor when the booking value is unknown. Every venue can override the percentage via venue.referral_fee_pct.
  2. Per-booking flat fee— applied to non-subscribers. A flat $2.50 per booking, regardless of booking value.
pythonapp/core/commerce.py
# Fee constants — keep in sync with pricing page and Stripe product config
DEFAULT_REFERRAL_FEE_PCT = 0.07  # 7% of booking value for subscribed venues
MIN_REFERRAL_FEE_USD = 1.50      # Floor when booking value unknown or zero
FLAT_FEE_USD = 2.50              # Non-subscriber per-booking flat fee
MAX_BOOKING_VALUE_USD = 1000     # Safety clamp for unreasonable values


class FeeCalc(BaseModel):
    fee_type: str  # referral_pct | per_booking_flat
    fee_pct: float
    platform_fee_usd: float


def calculate_booking_fee(
    venue: Venue,
    venue_client: VenueClient | None,
    booking_value_usd: float | None = None,
) -> FeeCalc:
    """Calculate the platform fee for a booking."""
    if booking_value_usd is not None:
        if booking_value_usd < 0:
            booking_value_usd = 0
        if booking_value_usd > MAX_BOOKING_VALUE_USD:
            booking_value_usd = MAX_BOOKING_VALUE_USD

    if (
        venue_client
        and hasattr(venue_client, "stripe_subscription_id")
        and venue_client.stripe_subscription_id
    ):
        fee_pct = venue.referral_fee_pct or DEFAULT_REFERRAL_FEE_PCT
        if booking_value_usd and booking_value_usd > 0:
            fee = round(booking_value_usd * fee_pct, 2)
        else:
            fee = MIN_REFERRAL_FEE_USD
        return FeeCalc(fee_type="referral_pct", fee_pct=fee_pct, platform_fee_usd=fee)
    return FeeCalc(fee_type="per_booking_flat", fee_pct=0, platform_fee_usd=FLAT_FEE_USD)

Safety clamps

Three clamps protect against data pollution and runaway billing:

  • Negative booking value— clamped to zero. Logged as a warning so we can spot the upstream source of the bad data.
  • Booking value over $1000 — clamped to MAX_BOOKING_VALUE_USD. A single dinner reservation shouldn't generate a $200 platform fee because a decimal slipped.
  • Missing booking value — falls back to the MIN_REFERRAL_FEE_USDfloor of $1.50. This keeps the flat fee path predictable even for venues that don't report booking value.

Stripe idempotency

Every PaymentIntent creation uses an idempotency key derived from the envelope ID. The key is stored in app/core/idempotency.py so retries never create duplicate charges. If the same envelope flows through collect_booking_fee twice (which happens on network retries), Stripe returns the existing charge instead of creating a new one.

The commerce ledger

Every fee collection writes a row into the commerce_ledger table. The row carries:

  • envelope_id— the A2A envelope that triggered the booking
  • venue_client_id— the owner being charged
  • fee_typereferral_pct or per_booking_flat
  • platform_fee_usd— the calculated amount
  • stripe_payment_intent_id— the Stripe reference
  • status — one of the FeeStatus enum values (pending, collecting, collected,failed, waived)

Reconciliation

Two scheduler jobs keep the ledger honest:

  • retry_pending_fees— runs every 30 minutes. Retries rows stuck in pending or failed status with an exponential backoff.
  • reconcile_fee_status— runs every 30 minutes. Compares local ledger status against Stripe's view and corrects drift (e.g. a charge that succeeded on Stripe but never marked the ledger as collected).

Both jobs are listed in full on the Scheduler page.

Behaviour in dev / staging

Outside production the Stripe PaymentIntent is skipped but the ledger row and envelope update are still written. This exercises the whole data pipeline end-to-end so you can test commerce flows without charging anyone. In production, if the Stripe call fails the ledger row lands in failed state and the retry job picks it up later.

Auditability

Every row in commerce_ledger is append-only. Corrections write a new row with a negative amount rather than mutating the original. This makes the ledger safe to replay for monthly invoices and gives us a clean audit trail if a venue disputes a charge.

Related