Skip to content
arch v1.0.0

Negotiation UI — Interface Proposal

Status: Implemented (schema differs) · Last updated: April 2026

The negotiation UI described here is live. However, this proposal was written before the entity consolidation in migrations 0073–0082. The implemented tables are offers, negotiations, and deals — not the deal_negotiations / deal_negotiation_terms / deal_negotiation_events schema sketched below. campaign_invitations no longer exists.

The UI component architecture, wireframes, and interaction patterns remain accurate references.

Platform Fit Analysis

Role Mapping

The generic UC document uses neutral labels. In Verifluence:

UC ActorVF RoleResponsibility
Party AStreamerProvides streaming services; has published minimum deal terms
Party BOperatorBuys streaming services; owns the campaign; funds the HTLC escrow

Either party may open a negotiation:

Who initiatesEntry pointInitial proposal author
Operator → Streamer"Invite to Campaign" on /streamers or /s/:usernameOperator
Streamer → Operator"Propose a Deal" on campaign detail /campaign/:idStreamer

Where Negotiation Sits in the Existing Flow

Current flow:
  Operator invites → Streamer accepts (auto-approved application) → HTLC funding

New flow (negotiation replaces auto-approve):
  Either party initiates


  Negotiation: invited
      │  Party B accepts invitation

  Negotiation: negotiating
      │  Per-term approve / counter-propose cycles

  Negotiation: waiting_for_funding   ← all 5 terms jointly approved
      │  Operator (Party B) funds HTLC escrow

  Negotiation: agreed
      │  Application auto-created with status = approved, source = negotiated

  Campaign delivery begins (existing submission / payout flow unchanged)

Integration Points in the Codebase

SurfaceChange
StreamerPublicProfile.tsx"Propose a Deal" replaces "Invite to campaign" or coexists as separate CTA
StreamersList.tsx"Negotiate" button on Tier 2 cards (replaces/extends "Invite")
ProfileLayout.tsx"Negotiations" nav item alongside "Invitations"
ActiveCampaigns.tsxThird tab: Negotiations (alongside Applications / Invitations)
InvitationsPage.tsxAccepting an invitation opens negotiation instead of auto-approving
campaign_invitations tablenegotiation_id FK column added when invite escalates to negotiation
New route /profile/negotiationsStreamer-side negotiation list

The Five Negotiable Terms

Term keyDisplay labelUnitVF source field
timeframeCampaign Timeframedayscampaign.end_date - start_date
deliveriesNumber of Streamsstreamscampaign_settings.spots_total
min_viewersMinimum Live Viewersviewerscampaign.req_min_viewers
min_durationMinimum Stream Durationhourscampaign.req_monthly_hours / deliveries
payment_per_streamPayment per StreamUSDCcampaign_settings.cpa_rate

Each term has three values in the negotiation context:

  • floor — campaign minimum or streamer published minimum (whichever is higher); neither party can go below this
  • proposed — the value currently on the table
  • approved_by_operator / approved_by_streamer — per-party boolean

Data Model Sketch

sql
-- Core negotiation record
CREATE TABLE deal_negotiations (
  id                BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  campaign_id       INTEGER NOT NULL REFERENCES campaigns(id),
  operator_id       INTEGER NOT NULL REFERENCES operators(id),
  streamer_id       INTEGER NOT NULL REFERENCES streamers(id),
  invitation_id     BIGINT  REFERENCES campaign_invitations(id),

  status            TEXT NOT NULL DEFAULT 'invited'
                    CHECK (status IN ('invited','negotiating','waiting_for_funding','agreed','cancelled')),
  initiated_by      TEXT NOT NULL CHECK (initiated_by IN ('operator','streamer')),
  current_version   INTEGER NOT NULL DEFAULT 1,
  funded_at         TIMESTAMPTZ,
  created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- One row per term per negotiation
CREATE TABLE deal_negotiation_terms (
  id                BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  negotiation_id    BIGINT  NOT NULL REFERENCES deal_negotiations(id),
  term_key          TEXT    NOT NULL
                    CHECK (term_key IN ('timeframe','deliveries','min_viewers','min_duration','payment_per_stream')),
  floor_value       NUMERIC(18,4) NOT NULL,    -- immutable lower bound
  proposed_value    NUMERIC(18,4) NOT NULL,
  proposed_by       TEXT    NOT NULL CHECK (proposed_by IN ('operator','streamer')),
  version           INTEGER NOT NULL DEFAULT 1,
  approved_by_operator BOOLEAN NOT NULL DEFAULT false,
  approved_by_streamer BOOLEAN NOT NULL DEFAULT false,
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (negotiation_id, term_key)
);

-- Append-only audit trail
CREATE TABLE deal_negotiation_events (
  id                BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  negotiation_id    BIGINT  NOT NULL REFERENCES deal_negotiations(id),
  actor             TEXT    NOT NULL CHECK (actor IN ('operator','streamer','system')),
  event_type        TEXT    NOT NULL,  -- invitation_sent | invitation_accepted | term_approved |
                                       -- term_countered | all_terms_agreed | deal_funded | cancelled
  term_key          TEXT,              -- NULL for non-term events
  old_value         NUMERIC(18,4),
  new_value         NUMERIC(18,4),
  version_at        INTEGER,
  created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

UI Component Architecture

NegotiationWidget (top-level)
├── NegotiationHeader          ← status badge, parties, version
├── NegotiationStepper         ← 4-step progress bar
├── InvitedView                ← only when status = invited
│   ├── InitialProposalCard    ← read-only term summary
│   └── InviteResponseActions  ← Accept / Decline CTAs
├── NegotiatingView            ← only when status = negotiating
│   ├── TermsMatrix            ← the core negotiation table
│   │   ├── TermRow (×5)
│   │   │   ├── TermLabel
│   │   │   ├── FloorBadge
│   │   │   ├── ProposedValue
│   │   │   ├── ApprovalPip (operator)
│   │   │   ├── ApprovalPip (streamer)
│   │   │   └── TermActions (approve / counter)
│   │   └── MatrixSummaryBar   ← "3 of 5 terms agreed"
│   └── CounterProposeDrawer   ← slide-in panel for counter input
├── WaitingForFundingView      ← only when status = waiting_for_funding
│   ├── AgreedTermsSummary
│   └── FundDealPanel          ← operator-side funding CTA
├── AgreedView                 ← only when status = agreed
│   ├── DealSummaryCard
│   └── GoToCampaignButton
└── ActivityLog                ← collapsible timeline, always visible
    └── ActivityEvent (×n)

Flowbite Block Mapping

UI elementFlowbite component / block
4-step progress stepperStepper
Status badge (invited / negotiating / …)Badge
Terms matrix tableTable with inline badges
Approval pip (✓ / ⏳ / ↩)Badge · icon variant
Counter-propose input panelDrawer — slides from right
"Fund the Deal" primary CTAButton · gradient variant
Activity logTimeline
Accept / Decline pairButton group
Floor value tooltipTooltip
"All terms agreed" transition noticeAlert · success variant
Party identity (avatar + name)Avatar + Text
Negotiation card containerCard
Responsive 2-column layoutFlowbite Blocks — Side navigation layout

Wireframes — All States

State 0 · invited (Streamer receives the invitation)

┌─────────────────────────────────────────────────────────────────────┐
│  🔔  DEAL PROPOSAL — Casino Royale × @streamername                  │
│  ─────────────────────────────────────────────────────────────────  │
│                                                                       │
│  ●────────────○────────────○────────────○                            │
│  Invited   Negotiating   Funding    Agreed                           │
│                                                                       │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  Initial Proposal from Casino Royale (Operator)              │   │
│  │  ─────────────────────────────────────────────────────────── │   │
│  │                                                               │   │
│  │  ┌─────────────────────┬───────────────┬──────────────────┐  │   │
│  │  │ Term                │ Your Minimum  │ Proposed         │  │   │
│  │  ├─────────────────────┼───────────────┼──────────────────┤  │   │
│  │  │ 📅 Campaign timeframe│ 14 days       │ 30 days  ✓ above │  │   │
│  │  │ 🎬 Streams           │ 4             │ 8        ✓ above │  │   │
│  │  │ 👁 Min viewers       │ 500           │ 1 000    ✓ above │  │   │
│  │  │ ⏱ Stream duration    │ 2 h           │ 3 h      ✓ above │  │   │
│  │  │ 💰 Payment / stream  │ $150 USDC     │ $280 USDC✓ above │  │   │
│  │  └─────────────────────┴───────────────┴──────────────────┘  │   │
│  │                                                               │   │
│  │  This proposal meets all your published minimums.            │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                                                                       │
│  [  ✓  Accept & Enter Negotiation  ]   [  ✕  Decline  ]             │
│                                                                       │
│  ▸ Activity (1 event)                                                │
└─────────────────────────────────────────────────────────────────────┘

Design notes:

  • "Above minimum" chips use Flowbite success badge variant in green
  • If any term is below minimum (validation failure that shouldn't reach UI but is shown defensively), it uses failure badge
  • CTA pair uses Flowbite Button (green gradient) + Button (outline red)

State 1 · negotiating — Active negotiation (your turn to act)

┌─────────────────────────────────────────────────────────────────────┐
│  ⚡  NEGOTIATING  v3 · Casino Royale × @streamername                │
│  ─────────────────────────────────────────────────────────────────  │
│                                                                       │
│  ●────────────●────────────○────────────○                            │
│  Invited   Negotiating   Funding    Agreed                           │
│                                                                       │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │  Terms · 3 of 5 agreed                  [Progress: ████░░ 60%] │  │
│  ├────────┬──────────────┬───────────────┬────────────┬──────────┤  │
│  │ Term   │ Floor (min)  │ On the table  │ Operator   │ Streamer │  │
│  ├────────┼──────────────┼───────────────┼────────────┼──────────┤  │
│  │📅 Time  │ 14 days ℹ   │ 30 days       │ ✅ Agreed  │ ✅ Agreed│  │
│  │        │              │               │            │          │  │
│  │🎬 Strms │ 4 strms  ℹ  │ 6 streams     │ ✅ Agreed  │ ✅ Agreed│  │
│  │        │              │               │            │          │  │
│  │👁 View  │ 500 avg  ℹ  │ 800 avg       │ ✅ Agreed  │ ✅ Agreed│  │
│  │        │              │               │            │          │  │
│  │⏱ Dur   │ 2 h      ℹ  │ ↩ 2.5 h      │ ⏳ Pending │ ⏳ Countr│  │
│  │        │              │ (you proposed)│            │          │  │
│  │        │              │               │ [Approve]  │[Waiting] │  │
│  │        │              │               │            │          │  │
│  │💰 Pay   │ $150     ℹ  │ $280 USDC     │ ⏳ Pending │ ✅ Agreed│  │
│  │        │              │               │ [Approve]  │          │  │
│  │        │              │               │ [Counter]  │          │  │
│  ├────────┴──────────────┴───────────────┴────────────┴──────────┤  │
│  │  2 terms need your attention                                   │  │
│  └────────────────────────────────────────────────────────────────┘  │
│                                                                       │
│  ▾ Activity log (8 events)                                           │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │  ↩ You proposed  Duration: 3h → 2.5h          2 min ago        │  │
│  │  ✅ Streamer approved  Payment per stream      5 min ago        │  │
│  │  ↩ Streamer proposed  Payment: $240 → $280    12 min ago       │  │
│  │  ✅ You approved  Min viewers                  18 min ago       │  │
│  │  ...                                                            │  │
│  └────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────┘

Row states:

IconMeaningColor
✅ AgreedBoth parties approved this termGreen
⏳ PendingThis party has not yet approvedAmber
↩ CounteredThis party proposed a new valuePurple
[Approve]Active CTA — click to approveGreen button
[Counter]Secondary CTA — propose a different valueOutline button
[Waiting]Greyed out — other party must respondDisabled

Counter-propose Drawer (slides from right):

┌────────────────────────────────────────┐
│  ↩  Counter-Propose: Payment per Stream │
│  ────────────────────────────────────── │
│                                          │
│  Current proposed value                 │
│  ┌──────────────────────────────────┐   │
│  │  $280.00 USDC / stream            │   │
│  └──────────────────────────────────┘   │
│                                          │
│  Floor (your published minimum)          │
│  ┌──────────────────────────────────┐   │
│  │  $150.00 USDC  ← cannot go below │   │
│  └──────────────────────────────────┘   │
│                                          │
│  Your new proposed value                 │
│  ┌──────────────────────────────────┐   │
│  │  $ 320.00                         │   │
│  └──────────────────────────────────┘   │
│  Must be ≥ $150.00                      │
│                                          │
│  [  Submit Counter-Proposal  ]           │
│  [  Cancel                   ]           │
└────────────────────────────────────────┘

State 2 · waiting_for_funding (Operator's turn to fund)

Streamer view:

┌─────────────────────────────────────────────────────────────────────┐
│  🎉  ALL TERMS AGREED · Casino Royale × @streamername               │
│  ─────────────────────────────────────────────────────────────────  │
│                                                                       │
│  ●────────────●────────────●────────────○                            │
│  Invited   Negotiating   Funding    Agreed                           │
│                                                                       │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  ✅  Deal terms — all agreed (v4)                             │   │
│  │  ─────────────────────────────────────────────────────────── │   │
│  │  📅  Campaign timeframe    30 days                            │   │
│  │  🎬  Number of streams     6 streams                          │   │
│  │  👁   Min live viewers      800 avg viewers                   │   │
│  │  ⏱   Min stream duration   2.5 hours                          │   │
│  │  💰  Payment per stream    $280.00 USDC                       │   │
│  │  ──────────────────────────────────────────────────────────  │   │
│  │  Total potential payout:   $1,680.00 USDC                    │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                                                                       │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  ⏳  Waiting for Casino Royale to fund the escrow             │   │
│  │  The operator has been notified. You'll receive a confirmation│   │
│  │  email once the HTLC deposit is confirmed on-chain.           │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                                                                       │
│  ▸ Activity log (12 events)                                          │
└─────────────────────────────────────────────────────────────────────┘

Operator view:

┌─────────────────────────────────────────────────────────────────────┐
│  🎉  ALL TERMS AGREED · Casino Royale × @streamername               │
│  ─────────────────────────────────────────────────────────────────  │
│                                                                       │
│  ●────────────●────────────●────────────○                            │
│  Invited   Negotiating   Funding    Agreed                           │
│                                                                       │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  ✅  Agreed terms (v4)              Total: $1,680.00 USDC     │   │
│  │  ─────────────────────────────────────────────────────────── │   │
│  │  📅  30 days  ·  🎬 6 streams  ·  👁 800 avg  ·  ⏱ 2.5 h     │   │
│  │  💰  $280.00 per stream × 6 = $1,680.00 USDC                 │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                                                                       │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  Fund the Deal                                                │   │
│  │                                                               │   │
│  │  This will lock $1,680.00 USDC in escrow.                    │   │
│  │  @streamername will be able to claim payouts per confirmed   │   │
│  │  stream once you review their session submissions.           │   │
│  │                                                               │   │
│  │  ┌──────────────────────────────────────────────────────┐   │   │
│  │  │  Connect wallet to fund                               │   │   │
│  │  │  (or use existing deposit from campaign escrow)       │   │   │
│  │  └──────────────────────────────────────────────────────┘   │   │
│  │                                                               │   │
│  │  [  ◆  Fund  $1,680.00 USDC  →  Agreed  ]                   │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                                                                       │
│  ▸ Activity log (12 events)                                          │
└─────────────────────────────────────────────────────────────────────┘

State 3 · agreed (Both views — read-only)

┌─────────────────────────────────────────────────────────────────────┐
│  ✅  DEAL AGREED · Casino Royale × @streamername                    │
│  ─────────────────────────────────────────────────────────────────  │
│                                                                       │
│  ●────────────●────────────●────────────●                            │
│  Invited   Negotiating   Funding    Agreed                           │
│                                                                       │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  Deal is live · Funded on 30 Mar 2026 · 4 negotiations v     │   │
│  │  ─────────────────────────────────────────────────────────── │   │
│  │  📅  Timeframe          30 days (ends 29 Apr 2026)            │   │
│  │  🎬  Streams             6 streams agreed                     │   │
│  │  👁   Min live viewers    800 avg viewers per stream           │   │
│  │  ⏱   Min stream duration  2.5 hours                           │   │
│  │  💰  Payment per stream  $280.00 USDC                         │   │
│  │  ──────────────────────────────────────────────────────────  │   │
│  │  💎  Total locked in escrow:  $1,680.00 USDC                  │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                                                                       │
│  [  Go to Campaign  →  ]   (streamer) or [  View Application  →  ]  │
│                                                                       │
│  ▸ Activity log (14 events)                                          │
└─────────────────────────────────────────────────────────────────────┘

TermsMatrix — Detailed Row Design

Each row is a Flowbite <TableRow> with five cells plus an inline action cell.

Approval pip states

┌──────────────────────────────────────────────────────────────────┐
│                   Approval Pip Visual States                     │
├───────────────────────┬──────────────────────────────────────────┤
│  ✅  green badge      │  approvedBy[role] = true                 │
│  ⏳  yellow badge     │  approvedBy[role] = false, no action yet │
│  ↩   purple badge     │  this party just submitted a counter      │
│  🔒  grey badge       │  term locked (status ≠ negotiating)      │
└───────────────────────┴──────────────────────────────────────────┘

Row interaction logic

If status ≠ 'negotiating'  →  all rows read-only, no action buttons
If current user is operator:
    if !approved_by_operator →  show [Approve] + [Counter]
    if  approved_by_operator →  show ✅, no buttons
    if  just countered       →  show ↩ "You proposed", no buttons (wait for streamer)
If current user is streamer:  (mirror of above)

"Proposed by" indicator

When one party proposes a new value, the ProposedValue cell shows:

  2.5 h  ← countered by you ↩

or

  2.5 h  ← operator proposed ↩

This prevents confusion about who last moved the needle.


Activity Log — Event Types and Icons

Event typeIconCopy template
invitation_sent📨Operator sent an invitation with initial terms
invitation_acceptedStreamer accepted the invitation — negotiation started
invitation_declinedStreamer declined the invitation
term_approvedOperator approved Payment per stream ($280)
term_counteredStreamer countered Stream duration: 3h → 2.5h
all_terms_agreed🎉All 5 terms approved — waiting for funding
deal_funded💎Operator funded $1,680 USDC — deal is live
cancelledNegotiation cancelled by Operator

The timeline uses Flowbite's <Timeline> component with icon pills and relative timestamps (2 min ago).


Negotiation List Views

Streamer — /profile/negotiations

Extends the existing ProfileLayout sidebar. Each row is a compact card:

┌─────────────────────────────────────────────────────────────────────┐
│  Casino Royale                          ⚡ NEGOTIATING  v3          │
│  Started 28 Mar · 2 terms need your attention                       │
│  💰 $280/stream · 🎬 6 streams · Total: $1,680                      │
│                                               [ Open negotiation → ] │
├─────────────────────────────────────────────────────────────────────┤
│  Betway Sports                          ✅ AGREED                   │
│  Agreed 25 Mar · 4 streams · $240/stream                            │
│  💎 $960.00 USDC locked                                             │
│                                               [ View campaign →     ] │
└─────────────────────────────────────────────────────────────────────┘

Sidebar badge shows count of negotiations with pending actions (same pattern as Invitations).

Operator — ActiveCampaigns Negotiations tab

Third tab in the existing Applications / Invitations / Negotiations tabset.

Each row shows streamer, status badge, version, and the count of terms needing the operator's response. A "Quick actions" inline section allows approving outstanding terms directly without opening the full widget.


Micro-interaction: Inline Term Approval

For terms where the operator/streamer just needs to approve (no counter), a one-click approval flow avoids navigation:

  💰 Payment per stream  $280 USDC   [  ✓ Approve  ]  [  ↩ Counter  ]
                                          ↓ click
  💰 Payment per stream  $280 USDC   ✅ You approved
                                         (optimistic update, then server confirmation)

This uses a loading spinner for the button on click (Flowbite Button with isProcessing prop), then replaces with the green badge on success.


Stepper Design

Flowbite's stepper component maps to the four states:

Step 1: Invited           ● (filled when status ≠ invited)
Step 2: Negotiating       ● (filled when status ∈ {negotiating, waiting_for_funding, agreed})
Step 3: Waiting for funds ● (filled when status ∈ {waiting_for_funding, agreed})
Step 4: Agreed            ● (filled when status = agreed)

Current active step pulses (Flowbite ring animation). Completed steps show a white checkmark in a filled purple circle.


States × Roles Matrix (UI behaviour)

StatusStreamer seesOperator sees
invitedInitial terms + Accept / DeclineSent invitation + pending notice
negotiatingTerms matrix + per-row Approve / CounterTerms matrix + per-row Approve / Counter
waiting_for_funding"Waiting for operator to fund" noticeAgreed terms + Fund the Deal CTA
agreedDeal summary + Go to CampaignDeal summary + View Application
cancelledCancellation notice + reasonCancellation notice

Transition Notification Banners

Flowbite <Alert> variants appear at the top of the widget on status change:

TransitionBanner variantText
All terms agreedsuccess🎉 All 5 terms agreed — the operator can now fund the deal
Deal fundedsuccess💎 Deal is live! The escrow is funded and the campaign has started
Counter receivedwarning↩ The streamer proposed new terms — 2 terms need your attention
Invitation declinedfailureThe streamer declined this invitation

Open Questions for Engineering

#QuestionProposed default
OQ-1Should negotiation replace the current auto-approve-on-accept flow, or coexist as an optional "advanced mode"?Optional: invitation without negotiation stays as is; operator can choose "Invite with Negotiation" from the modal
OQ-2Can either party cancel a negotiation mid-flow?Yes, any time before agreed; triggers cancelled event and notifies both parties
OQ-3Expiry: should negotiations expire if idle too long?Yes — 14-day inactivity timeout; system sets status to cancelled and logs expired event
OQ-4Is there a maximum number of negotiation rounds (versions)?Soft cap of 20 versions; beyond this the UI warns "This negotiation has many rounds — consider a fresh start"
OQ-5How does the streamer publish minimums?Via Profile → Personal Info → Deal Preferences (already has deal_cpa_rate, deal_revshare_rate, req_min_viewers); extend with deal_min_duration, deal_min_deliveries
OQ-6Is payment_per_stream the CPA rate, or is revenue share also negotiable?MVP: CPA only (payment_per_stream); rev-share added in Phase 2 as a sixth term
OQ-7Does funding happen via the existing HTLC flow or a new mechanism?Existing HTLC flow: allocation_id stored on the resulting campaign_application row

Verifluence Documentation