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 settingGET /workspaces/{creator_id}/rates-state— buyer's view of statePOST /workspaces/{creator_id}/request-rates— buyer submits request (rejects if creator is public, blocks duplicate active requests)GET /rate-requests/inbox+/summary— influencer's queueGET /rate-requests/active-grants— currently approved buyers + expiry timestamps- Reused
POST /price-requests/{id}/approve|decline|revoke—_load_owned_requestextended 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 carriesrates_visible/rates_visibility/rates_reveal_configGET /influencers/{id}/media-kit— same; respects visibility for the public media kit tooGET /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/listingsheader - "Marketplace — Price Visibility" card removed from
/settingsWorkspace 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_requeststable — generalized by Phase 1 with thecreator_workspace_idcolumn added in migration9d1e3f6a7b8c. 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-windowsfor full CRUD - New
PostingWindowsManagercomponent 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— generalizeprice_reveal_requeststo 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

