Rate Visibility (Phase 1) · Property Posting Windows

The marquee feature this release: influencers can now control who sees their rates across all their properties, with a single workspace-level setting. This is the long-form rework of an earlier prototype that had gated price on individual marketplace listings — a model that didn't fit the auction mechanic. The rebuilt feature applies on top of property rate cards, where pricing-as-menu makes hiding-then-revealing actually work.

The marquee feature this release: influencers can now control who sees their rates across all their properties, with a single workspace-level setting. This is the long-form rework of an earlier prototype that had gated price on individual marketplace listings — a model that didn't fit the auction mechanic. The rebuilt feature applies on top of property rate cards, where pricing-as-menu makes hiding-then-revealing actually work.

Rate Visibility — Phase 1

#### The model

Each influencer workspace picks one mode at /settings/rate-visibility. The choice applies to every property the workspace owns — not per-property, not per-listing. The simplification matches how creators think about themselves: "my rates are private, period", not as N separate decisions per channel.

Two modes ship in Phase 1:

  • Public (default). Anyone browsing the influencer's profile sees full rate cards on every property. No friction, maximum reach.
  • Approval-required . Property cards show "💎 Rates on request" with no actual rates. A buyer who wants to see prices submits a structured request — brand category, budget hint, campaign context, optional timeline. The influencer reviews in their inbox and approves or declines (with a reason). Approved buyers see rates on all the influencer's properties for the configured window (default 30 days). One grant unlocks the whole rate card; no per-property re-asking.

Phase 3 will add a third mode — refundable deposit — where the buyer pays a refundable amount via Razorpay to escrow, sees rates instantly, and the deposit auto-credits to the final invoice if a deal closes (or auto-refunds otherwise). Reserved for after Razorpay escrow is hardened in production.

#### What you get when you turn approval mode on

Buyer-side gate appears on /influencers/[id] between the influencer's bio and the restrictions panel. Three states the gate can be in:

  • Indigo "💎 Rates on request" panel with a Request button → for first-time visitors and re-requests after expiry / decline
  • Amber "⏳ Awaiting creator's response" chip → after the buyer has submitted, before the influencer decides
  • Green "✓ Rates visible · expires Jun 04" chip → after approval, with the configured window

Property cards in the grid replace the rate count badge with a "💎 Rates on request" chip when gated. The expandable rate-cards table at the bottom of the page is empty until the gate opens.

Settings page at /settings/rate-visibility (also linked from the Workspace tab on /settings) shows the mode picker, the Active Grants section listing buyers who currently have access (with revoke button), and the Pending Requests inbox inline — no separate inbox page.

#### Backend shape

Migration 9d1e3f6a7b8c adds creator_workspace_id FK to price_reveal_requests and makes the legacy listing_id nullable. The model now has two FKs to workspaces (requester_workspace_id for the buyer, creator_workspace_id for the seller) — both relationships explicitly typed with foreign_keys= so SQLAlchemy's mapper init can disambiguate. (The first attempt missed this and broke every endpoint that touched any model — fixed before ship.)

New endpoints under the existing price-visibility router:

  • GET / PATCH /workspaces/me/price-visibility — workspace setting
  • GET /workspaces/{creator_id}/rates-state — buyer's view of state
  • POST /workspaces/{creator_id}/request-rates — buyer submits request (rejects if creator is public, blocks duplicate active requests)
  • GET /rate-requests/inbox + /summary — influencer's queue
  • GET /rate-requests/active-grants — currently approved buyers + expiry timestamps
  • Reused POST /price-requests/{id}/approve|decline|revoke_load_owned_request extended to handle workspace-scoped rows alongside the legacy listing-scoped ones

Three buyer-facing endpoints filter rate cards based on visibility:

  • GET /influencers/{id} — workspace-aware; rate_cards array nulled, response carries rates_visible/rates_visibility/rates_reveal_config
  • GET /influencers/{id}/media-kit — same; respects visibility for the public media kit too
  • GET /influencers/{id}/rate-cards — returns {rates_visible: false, rate_cards: []} when gated, {rates_visible: true, rate_cards: [...]} when allowed

Auto-expire cron wired into the existing /admin/analytics/run-scheduled endpoint alongside campaign-application expiry. Lazy-on-read also handled — opening a stale request from the inbox flips it to expired mid-fetch.

Notifications fire both ways: creator gets an in-app notification on each new request; buyer gets one when the influencer approves, declines, or revokes.

Bundle A pivot — UI surface hidden, plumbing reused

The earlier prototype that gated prices on marketplace listings has been removed from the user-facing surface. Reasoning: marketplace listings are bid-based — hiding the floor breaks the bidding mechanic, since buyers can't bid intelligently against an unknown minimum. The same primitive belongs on property rate cards (menu-style, not auction), which is where Phase 1 lives.

What changed:

  • All listings reset to price_visibility='public' so nothing is stuck in a hidden state
  • 🔒 Price Visibility section removed from /marketplace/listings/[id]/edit
  • 💎 "Price on request" chips removed from listing cards on /marketplace, /marketplace/listings, listing detail page
  • Request CTA + modal removed from listing detail
  • 📬 Price Requests button removed from /marketplace/listings header
  • "Marketplace — Price Visibility" card removed from /settings Workspace tab

What stayed (plumbing reused by Phase 1):

  • Migration 7b9c1d4e5f6a (Bundle A's listing column) — applied, dormant. Future "flat-rate listings" use case can revive it without re-migrating.
  • price_reveal_requests table — generalized by Phase 1 with the creator_workspace_id column added in migration 9d1e3f6a7b8c. Bundle A's listing-scoped legacy column kept for forensic continuity.
  • All approval/decline/revoke endpoints — extended to handle workspace-scoped rows alongside legacy listing-scoped ones via _load_owned_request.
  • Notification infrastructure, auto-expiry cron, settings page UX — all reused.

Net: no work was wasted. The 4 days spent on Bundle A's listing-scoped approval mode rolled directly into Phase 1's workspace-scoped rate visibility, just with the entity changed from "listing" to "rate card across all properties."

TypeScript fallout: Listing.basePrice widened to number | null so the listing types could express the price-hidden case during Bundle A. After the surface was hidden, every consumer (BidForm, InventoryCard, PackageBuilder, listing edit + detail + browse pages, brief send picker, opportunities, line-items, media-kit) was updated to coerce basePrice ?? 0 so existing bid math + invoice math stays correct.

Property posting windows (parallel session work)

Migration 8c0d2e5f6a7b adds property_posting_windows — recurring weekly time windows when an influencer typically publishes on a given property (e.g., "Mondays 6-9pm IST").

  • New endpoints under /workspaces/{ws}/properties/{prop}/posting-windows for full CRUD
  • New PostingWindowsManager component shown on the property edit panel at /properties
  • Surfaced on the public media kit and the brief send picker so advertisers can see when a creator's content actually goes live before deciding to send a brief

Migration chain applied this session

In order:

  • 7b9c1d4e5f6a — listing-scoped price visibility (Bundle A, kept dormant)
  • 8c0d2e5f6a7b — property posting windows (parallel feature)
  • 9d1e3f6a7b8c — generalize price_reveal_requests to workspace scope (Phase 1)

What's not in this release

  • Phase 2 — "Approach for collaboration" inquiry that turns an approved viewer into a Deal in negotiation state, replacing the current ad-hoc DM approach
  • Phase 3 — Refundable deposit mode (Razorpay escrow + auto-refund + auto-credit-to-deal)
  • Phase 4 — Auto-rules (auto-approve verified buyers, auto-decline restricted brands) and buyer health card in the inbox
  • Bundle A's hidden-but-dormant code path will not be re-exposed; the only path forward is via the workspace-scoped Phase 1 model

Want to see it in your own workspace?

Bring planning, discovery, delivery, and measurement into one platform.