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, anddeals— not thedeal_negotiations/deal_negotiation_terms/deal_negotiation_eventsschema sketched below.campaign_invitationsno 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 Actor | VF Role | Responsibility |
|---|---|---|
| Party A | Streamer | Provides streaming services; has published minimum deal terms |
| Party B | Operator | Buys streaming services; owns the campaign; funds the HTLC escrow |
Either party may open a negotiation:
| Who initiates | Entry point | Initial proposal author |
|---|---|---|
| Operator → Streamer | "Invite to Campaign" on /streamers or /s/:username | Operator |
| Streamer → Operator | "Propose a Deal" on campaign detail /campaign/:id | Streamer |
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
| Surface | Change |
|---|---|
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.tsx | Third tab: Negotiations (alongside Applications / Invitations) |
InvitationsPage.tsx | Accepting an invitation opens negotiation instead of auto-approving |
campaign_invitations table | negotiation_id FK column added when invite escalates to negotiation |
New route /profile/negotiations | Streamer-side negotiation list |
The Five Negotiable Terms
| Term key | Display label | Unit | VF source field |
|---|---|---|---|
timeframe | Campaign Timeframe | days | campaign.end_date - start_date |
deliveries | Number of Streams | streams | campaign_settings.spots_total |
min_viewers | Minimum Live Viewers | viewers | campaign.req_min_viewers |
min_duration | Minimum Stream Duration | hours | campaign.req_monthly_hours / deliveries |
payment_per_stream | Payment per Stream | USDC | campaign_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
-- 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 element | Flowbite component / block |
|---|---|
| 4-step progress stepper | Stepper |
| Status badge (invited / negotiating / …) | Badge |
| Terms matrix table | Table with inline badges |
| Approval pip (✓ / ⏳ / ↩) | Badge · icon variant |
| Counter-propose input panel | Drawer — slides from right |
| "Fund the Deal" primary CTA | Button · gradient variant |
| Activity log | Timeline |
| Accept / Decline pair | Button group |
| Floor value tooltip | Tooltip |
| "All terms agreed" transition notice | Alert · success variant |
| Party identity (avatar + name) | Avatar + Text |
| Negotiation card container | Card |
| Responsive 2-column layout | Flowbite 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
successbadge variant in green - If any term is below minimum (validation failure that shouldn't reach UI but is shown defensively), it uses
failurebadge - 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:
| Icon | Meaning | Color |
|---|---|---|
| ✅ Agreed | Both parties approved this term | Green |
| ⏳ Pending | This party has not yet approved | Amber |
| ↩ Countered | This party proposed a new value | Purple |
| [Approve] | Active CTA — click to approve | Green button |
| [Counter] | Secondary CTA — propose a different value | Outline button |
| [Waiting] | Greyed out — other party must respond | Disabled |
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 type | Icon | Copy template |
|---|---|---|
invitation_sent | 📨 | Operator sent an invitation with initial terms |
invitation_accepted | ✅ | Streamer accepted the invitation — negotiation started |
invitation_declined | ✕ | Streamer declined the invitation |
term_approved | ✅ | Operator approved Payment per stream ($280) |
term_countered | ↩ | Streamer 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 |
cancelled | ✕ | Negotiation 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)
| Status | Streamer sees | Operator sees |
|---|---|---|
invited | Initial terms + Accept / Decline | Sent invitation + pending notice |
negotiating | Terms matrix + per-row Approve / Counter | Terms matrix + per-row Approve / Counter |
waiting_for_funding | "Waiting for operator to fund" notice | Agreed terms + Fund the Deal CTA |
agreed | Deal summary + Go to Campaign | Deal summary + View Application |
cancelled | Cancellation notice + reason | Cancellation notice |
Transition Notification Banners
Flowbite <Alert> variants appear at the top of the widget on status change:
| Transition | Banner variant | Text |
|---|---|---|
| All terms agreed | success | 🎉 All 5 terms agreed — the operator can now fund the deal |
| Deal funded | success | 💎 Deal is live! The escrow is funded and the campaign has started |
| Counter received | warning | ↩ The streamer proposed new terms — 2 terms need your attention |
| Invitation declined | failure | The streamer declined this invitation |
Open Questions for Engineering
| # | Question | Proposed default |
|---|---|---|
| OQ-1 | Should 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-2 | Can either party cancel a negotiation mid-flow? | Yes, any time before agreed; triggers cancelled event and notifies both parties |
| OQ-3 | Expiry: should negotiations expire if idle too long? | Yes — 14-day inactivity timeout; system sets status to cancelled and logs expired event |
| OQ-4 | Is 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-5 | How 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-6 | Is 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-7 | Does funding happen via the existing HTLC flow or a new mechanism? | Existing HTLC flow: allocation_id stored on the resulting campaign_application row |