TEASEDocs
ProductsClubTroubleshooting

PPV composer money bugs

Three composer-layer mistakes that are invisible on screen — dollars not converted to cents, a dropped paywall lock, and a preview id outside the attached media.

The client code that builds a PPV send's request body calls these out explicitly as "errors invisible on screen" — none of them throw, none of them look wrong in the composer UI, and each one is a real-money or real-content mistake if it slips through.

1. Dollars not converted to cents

The price input is dollars; the API takes price_cents. Skip the conversion (or round it wrong) and a fan gets charged 100× too little — a $20 send going out priced at $0.20, silently accepted by everything downstream because $0.20 is still a valid non-negative integer.

The guard: the composer's own price function always does Math.round(parseFloat(input) * 100) and clamps garbage or an empty field to 0 — it never passes a raw dollar figure through to the send payload.

2. locked dropped when price is set

A PPV message needs locked: true for OnlyFans to actually paywall it. Send a price without the lock flag and the attachment can ship unlocked — a fully paid file the fan gets for free, with the price still showing on the message as if it worked.

The guard: the composer derives whether a send is a real PPV from one pair of conditions — "paid-content mode is on" and "price > 0" — and only in that case does it set price_cents, locked: true, and any preview_ids at all. A send that fails either half of that pair goes out as a plain free message, byte-identical to one from before PPV existed — there's no in-between state where a price is set but the lock silently isn't.

3. A preview id outside the attached media

OnlyFans requires every teaser/preview id to be a subset of the message's attached media ids. If you unmark or remove an attachment that was still flagged as a preview, and the stale preview id survives into the request, OnlyFans rejects the send outright rather than silently ignoring the bad id.

The guard: the composer explicitly filters preview_ids down to whatever's still in media_ids before building the payload, on every attachment change — a preview can't outlive the attachment it belongs to.

Where these guards actually live

All three rules sit together in the composer's own send-payload code, deliberately pulled out of the screen component and covered by tests specifically because — per the code's own comment — this is exactly the kind of mistake that "passes silently and is expensive." If you're extending the composer, that's the one file to touch, not the screen.

What's next

On this page