---
title: "Page Curtain"
description: "The short banner that drops in on Ring pages to offer Add to Home Screen or an advertised product, how visitors dismiss it, how vendors opt products in, and how page authors earn a commission"
locale: "en"
---
# Page Curtain

The page curtain is a slim banner (about 10% of the screen height) that drops in after a visitor stops scrolling. It shows one of two kinds of offer: **Add to Home Screen** for the app, or a **product** a vendor has chosen to advertise. On news articles and member blog posts, a product added from the curtain can earn the page's author a commission when the shopper later pays.

> **Info**
> Use the **Founder** / **Developer** tabs in the docs sidebar to filter this page. Shared sections apply to both audiences.

> **Success**
> **Status (2026-10-26):** Shipped in Layer 1 and configurable per clone through the `curtain` block in `ring-config.json`. The queue now honors earlier dismissals, so a visitor who chose Not today does not see the same banner again on the next page.

| Without the curtain | With the curtain |
|---------------------|------------------|
| Install prompts depend on the browser | Ring offers Add to Home Screen itself, once per policy window |
| Authors cannot earn from products they mention | Authors earn additive credit on paid carts that started from their page |
| Vendors pay for ads elsewhere | Vendors opt a product in and set a 1% to 10% author commission |

### For founders

## Why this matters for your community

Writers bring readers; vendors want buyers. The curtain connects the two without a separate ad system. A vendor flags a product, a writer links it or writes about the same topic, and when a reader buys it, the writer is rewarded in platform credit. The vendor's payout is not reduced: the commission is minted as extra credit.

  
- **[News](/docs/features/news.md)** — Article pages are where product offers appear.

  
- **[Member Blog](/docs/features/member-blog.md)** — Member posts get the same product offers and author commission.

  
- **[Store](/docs/features/store.md)** — Vendors switch on curtain advertising in the product form.

### Typical scenarios

- **A food blogger links a farm's honey** in an article. Readers see the honey in the curtain, tap Add to cart, and pay within 30 days. The blogger receives 5% of that line as credit.
- **A first-time visitor on mobile** reads three pages. On one of them the curtain offers Add to Home Screen. They tap Not today and do not see it again for 24 hours.
- **A reader taps Not for me** on a product. That product never drops for them again.

```mermaid
flowchart LR
  A[Vendor enables advertise] --> B[Article page]
  B --> C[Curtain offer]
  C -->|Add to cart| D[Signed touch stored]
  D --> E[Paid order within 30 days]
  E --> F[Author gets credit]
```

### Where to watch results

- **Vendors:** the earnings page shows a 7 day curtain funnel: shown, resurfaced, CTA, not today, not for me, scroll away, paid, and credits minted.
- **Authors:** My News (`/my-news`) shows the same 7 day funnel for their own pages plus curtain credits earned.
- **Operators:** the admin analytics page shows the same funnel for the whole clone, next to the active curtain policy.

### For developers

## Configuration

`ring-config.template.json` ships these defaults. Override any key in your clone's `ring-config.json`. Values are read by `getCurtainPolicy()` in `lib/ring-config-core.ts` and validated by `parseCurtainPolicy` in `features/curtain/lib/policy.ts`.

| Key | Default | Allowed values |
|-----|---------|----------------|
| `curtain.enabled` | `true` | `false` turns every offer off |
| `curtain.productSource` | `page_then_pool` | `page_then_pool`, `page_only`, `pool_only` |
| `curtain.maxImpressionsPerDay` | `5` | 1 to 50 per offer, per UTC day |
| `curtain.notTodayHours` | `24` | 1 to 168 |

{`{
  "curtain": {
    "enabled": true,
    "productSource": "page_only",
    "maxImpressionsPerDay": 3,
    "notTodayHours": 48
  }
}`}

## How offers are chosen

`requestCurtainDrop` in `features/curtain/actions.ts` returns up to three offers (`CURTAIN_QUEUE_MAX`):

1. Skip entirely when the policy is off or the path starts with `/admin`, `/login`, `/register`, `/auth`, `/vendor` or `/store/checkout`.
2. Product offers only on pages that pass a `newsArticleId` through `CurtainPageProvider` (news articles and member blog posts). The article must be published and visible to the viewer under `canViewNewsArticle`.
3. **Page** products are `/store/` links found in the article body. **Pool** products are active products with `curtainAdvertiseEnabled`. Each must be advertised, active, priced, not the viewer's own vendor product, and not suppressed.
4. Sort: linked products first, then topic overlap with the article's category and tags, then fewer impressions today.
5. Fill a remaining slot with Add to Home Screen when the browser is not already running the installed app.

`curtain-host.tsx` shows the queue and applies `canShowOffer` to every queued item, which is the fix for dismissed banners coming back.

## Visitor responses

| Response | Effect |
|----------|--------|
| CTA | Add to cart (product) or install prompt (A2HS) |
| Not today | Suppressed until `notTodayHours` from now |
| Not for me | Suppressed forever for that offer |
| Scroll away | Logged, no suppression |

Preferences live in `localStorage` (`ring_curtain_prefs`) and, for signed-in members, in the `curtain_preferences` table (migration `050`, part of `schema.sql`) via `syncCurtainPrefs`.

## Attribution and commission

- Each product offer carries an HMAC `offerToken` from `features/curtain/lib/offer-token.ts`, signed with `AUTH_SECRET`. It binds offer, product, author and article.
- CTA touches are kept in the `ring_curtain_attr` cookie and storage (30 day window, at most 12 touches).
- Checkout keeps only touches that pass `verifyCurtainTouches`. Author and percent come from the token and the live product, never from the browser.
- After payment, `mintCurtainAuthorCommission` in `features/curtain/services/commission.ts` adds credit to the author: line subtotal times the product's percent (1 to 10, default 5). It skips self-purchase, the order's referrer, duplicates, and runs only when credit rewards are enabled.
- Events `curtain_shown`, `curtain_response`, `curtain_cta`, `curtain_suppress` and `curtain_paid` feed `getCurtainFunnel` in `features/analytics/lib/curtain-analytics.ts`.

## Vendor opt-in

The vendor product form includes `AdvertiseProductWidget` (`features/curtain/components/advertise-product-widget.tsx`). It writes `curtainAdvertise: { enabled, commissionPercent }` and `curtainAdvertiseEnabled` on the product. A product without a price cannot be advertised.

## Frequently asked questions

### Impact

#### Does the commission come out of the vendor's payout?

No. It is additive credit minted to the author, recorded with source `curtain_author`.

### Migration

#### Do I need a migration?

Not on a fresh install. Existing databases need `050_curtain_offers.sql` for `curtain_preferences` and the analytics index.

### Ops

#### How do I turn the curtain off for one clone?

Set `"curtain": { "enabled": false }` in that clone's `ring-config.json` and rebuild.

  
- [features/store](/docs/features/store.md) — Depends-on: products, cart and paid orders that trigger commission.

  
- [features/news](/docs/features/news.md) — Same-workflow: article pages that host product offers.

  
- [features/member-blog](/docs/features/member-blog.md) — Same-workflow: member posts host the same offers.

  
- [features/rewards](/docs/features/rewards.md) — See-also: credit rewards that must be enabled for commission.

  
- [features/refcodes](/docs/features/refcodes.md) — See-also: referral attribution, which shares the 30 day window.
