Pricing Consistency

Server-side fee calculation and validation for offers

Pricing Formula

Every offer has three price fields, all computed server-side:

FieldFormulaDescription
offerAmountMinorUnitInput from buyerWhat the talent receives
zoolyFeeMinorUnitMath.round(offerAmountMinorUnit × 0.20)20% Zooly platform fee
totalBrandPriceMinorUnitofferAmountMinorUnit + zoolyFeeMinorUnitWhat the brand pays

The client sends only offerAmountMinorUnit. The server computes and stores the other two values. Client-sent fee fields are ignored.

Where Fees Are Computed

On Offer Creation

createOffer() in packages/db/src/access/offers.ts computes zoolyFeeMinorUnit and totalBrandPriceMinorUnit from the provided offerAmountMinorUnit before inserting the row.

On Counter-Offers

updateOfferStatus() in packages/db/src/access/offers.ts recomputes both fee fields whenever a countered amount is applied to the canonical offer_amount_minor_unit column:

  • A talent's first counter (→ ADMIN_REVIEW) applies the proposed deltas and recomputes immediately.
  • A private post-approval counter (→ COUNTERED) only stores the proposal; fees are recomputed when the other side accepts (COUNTERED → ACCEPTED).

updateOfferDraft() likewise recomputes fees when a draft edit changes the amount.

On Offer Submission

POST /api/offers/submit only accepts offerAmountMinorUnit (positive integer). The submitOfferBodySchema contract has no fee fields at all, so any client-sent zoolyFeeMinorUnit / totalBrandPriceMinorUnit are dropped at parse time before reaching createOffer.

Payment Validation

When a payment intent is created for an offer, getOfferProduct() in packages/srv-stripe-payment/src/getProductByPayForId.ts performs a consistency check:

  1. Loads the offer by ID
  2. Asserts the stored zoolyFeeMinorUnit matches Math.round(offerAmountMinorUnit × 0.20)
  3. Asserts the stored totalBrandPriceMinorUnit matches offerAmountMinorUnit + zoolyFeeMinorUnit
  4. If either assertion fails, the payment intent creation is rejected

This prevents price drift if the fee percentage changes or if stored values are manually edited.

Price Data for Stripe

The computePriceData(amountMinorUnit, platformFeeMinorUnit) function in packages/srv-stripe-payment/src/getProductByPayForId.ts produces the full ProductPriceData breakdown used by Stripe:

  • Total charge amount (what brand pays)
  • Stripe processing fee (calculated via calculateStripeFees(amount, currency) from @zooly/util — the fixed fee component is 0 for zero-decimal currencies)
  • Platform fee (the 20% Zooly fee)
  • Talent gross share (total minus Stripe fee minus platform fee)

This function is exported from @zooly/srv-stripe-payment for use in escrow release and other payment flows.

Key Files

FileRole
packages/db/src/access/offers.tsFee computation in createOffer and updateOfferStatus
apps/zooly-app/app/api/offers/submit/route.tsInput validation, strips client fee fields
packages/srv-stripe-payment/src/getProductByPayForId.tsPayment-time consistency assertion, computePriceData