The Offers System is the physical record of a licensing deal. Most live
deals start as a conversation on a public Z-link. When terms settle, Offer-Gen
mints a one-page offer. Both sides Approve on that document (brand first
is thanks only; talent Approve is accept). The brand then opens Send
Payment, pays the due slice plus a 20% Zooly fee, and Stripe holds the card
(PENDING_PAY_CAPTURE → later PAID). Talents deliver; brands mark complete
(or the auto-release timer does). Stripe Connect is how talents get paid
out, not how they accept or how the brand pays in.
The full click-by-click path is Offer approval and brand payment.
A second path still exists: authenticated Z-link forms (ZLinkPage) that
submit POST /api/offers/submit with campaign type, brief, and optional
image/voice candidates. Talents can still reject or counter (including
multi-field counters: price, usage, sharing, script, picked sample, picked
voice, image kind).
Public Z-links emit server-rendered Open Graph and Twitter metadata using the talent name (for example, “Make an offer to Alex Rivera via their AI agent”), so the intended call to action is preserved when a link is shared. In the dashboard, both the live and preview URL fields are links that open in a new tab; the adjacent copy and QR actions remain available.
buyerAccountId is resolved from the session cookie; perspective=admin requires the admin role.OFFER_TRANSITIONS (packages/types/src/types/Offer.ts). See Architecture for the diagram.saveAsDraft: true, resume from /dashboard/drafts, and submit when ready. Drafts are exempt from the "candidate session required" rule.IMAGE campaigns fan out to multiple image models (OpenAI, Nano-Banana, Flux); VOICE campaigns generate voice reads; VIDEO requires both. The brand picks a model key (and optionally flips imageKind between DRAFT/FINAL) at submit. skipImageGeneration lets a brand bypass image generation.*_minor_unit (integer) plus a currency ISO 4217 code. Zero-decimal currencies handled in packages/util/src/currency.ts. (6a07f00f)counter_changes jsonb carries a whitelisted partial of fields a talent (or brand) may propose. Applied onto the canonical columns and fees recomputed. (3b6f6cbf)DRAFT → APPROVED); counters stay between brand and talent.flag, ask_principal, auto_counter, or auto_reject (the last two never reach the seller as a normal offer).brand_approved_at) and does not charge. A material rewrite of those terms clears that stamp; the brand must Approve again. Talent Approve is accept (ACCEPTED) and does not require Connect. Conversation Counter / Reject are messages to Z; a counter reaches the written offer only after Z's last-look in the chat. The brand then opens View payment / Send Payment for the due slice plus a 20% fee. See Offer approval and brand payment.PENDING_PAY_CAPTURE); capture to PAID is admin/auto. An admin queue + cron handle release and 7-day auth voiding.expires_at / expire_policy; a cron sends stale reminders and transitions to EXPIRED.ef054780)Idempotency-Key.packages/db)offers table with the 13-state offer_status enum, the offer_expire_policy enum, and the image_kind enum. counter_changes jsonb for multi-field counters.createOffer, getOfferById, updateOfferStatus, updateOfferDraft, listOffersBySellerAccount, listOffersByBuyerAccount, listAllOffers, the payment/escrow helpers, the below-threshold auto-actions, and the cron query helpers. See API reference.packages/contracts)submitOfferBodySchema, respondToOfferBodySchema (with offerCounterChangesSchema), deliverOfferBodySchema, completeOfferBodySchema, requestRevisionBodySchema, disputeOfferBodySchema, the admin capture schemas, getOfferResponseSchema, listOffersQuerySchema / listOffersResponseSchema.OfferStatus, ExpirePolicy, OfferCounterChanges, OFFER_TRANSITIONS, Offer, NewOffer, OfferHistory types in @zooly/types.apps/zooly-app)Core lifecycle endpoints under /api/offers/:
POST /api/offers/submit)GET /api/offers/list) — seller / buyer / admin perspectivesGET /api/offers/[id])POST /api/offers/respond) — talent accept / reject / counter (with counterChanges). Accept does not require Connect. Conversation Offer Counter / Reject never call this on the click; a counter confirmed in the chat reaches the same transition after Z's last-look.POST /api/offers/approve) — sets brand_approved_at without changing status; wakes the talent-side agent.POST /api/offers/deliver)POST /api/offers/complete)POST /api/offers/revision), Dispute (POST /api/offers/dispute), Cancel (POST /api/offers/cancel)Plus admin, candidate-session, blast, and cron routes — see the full endpoint list.
Stripe Connect onboarding for talent payouts: /api/payments/connect/start + /api/payments/connect/return plus the account.updated webhook (packages/srv-stripe-payment). Full walkthrough in Stripe Connect (Talent Payouts).
packages/offers/client)submitOffer(), fetchOffers(), respondToOffer(), fetchPayoutRoute(), startStripeConnect() in packages/offers/client/src/lib/appApi.ts. New code routes through @zooly/api-client (W1-01 foundations).
The offers UI is a single React SPA in packages/offers/client (exported as App), mounted by both the Next.js zooly-app and the Vite zooly-app2 over the route subtrees it owns (/z, /agent, /dashboard).
/z/:slug. Formal offers are usually minted from that conversation (Offer-Gen), not from a campaign form. A legacy form path still calls submitOffer() with image/voice candidates.ConversationsPage at /dashboard/agent/conversations/:roomId?view=offer. The Offer tab stays disabled until a formal offer exists. Approve is the only direct write. Counter / Reject / Add on a Z recommend switch to Conversation as chat lines; Z asks last-look, and a counter reaches the written offer only after the answer. After Z writes, a one-sided brand stamp is cleared. After talent Approve the brand sees View payment → Send Payment. See Offer approval and brand payment.?view=agreement): lawyer-reviewable draft that AgreementAg writes from the live Offer.needs-connect / needs-completion / connected). Not part of Accept or brand pay-in.packages/ui-offers (OfferActionBar, OfferStatusBadge, OfferSummaryCard, DeliverablePreview, …).