pornboxdBETA
← Field Notes
Devlog

A cam vertical, but only the VR ones

My audience is on VR headsets, so a 2D cam recommendation converts near zero. That constraint shaped the entire cam-affiliate integration.

The site shipped a cam vertical this week. Just one constraint: only VR cams, never the standard 2D ones.

That sounds like a small filter. It is the whole feature.

My audience is on VR headsets. A 2D cam on a video discovery site for headset users is a placement that converts near zero. So when I picked an affiliate platform for the cam vertical, the dealbreaker question was the same every time. Does your live model feed have a per-model VR signal I can filter on?

Most platforms don't. Stripchat does. Stripcash (their affiliate side) exposes it through the Aggregators API. Every model object carries a broadcastVr flag and you can filter the entire feed down to whoever is currently broadcasting in stereoscopic 180 or 360. That was enough for me to commit.

The data layer is a 30-second poller at apps/processor/src/cam-poller.ts. Every half minute it hits Stripcash's models endpoint, pulls the entire current online roster, and upserts the VR-broadcasting subset into a vr_cam_models table. Models that drop offline get marked with last_seen_at. Rows older than 30 days get deleted to comply with Stripchat's TOS retention rule. The poller pings Healthchecks at the end of every successful tick, not the start. (Pinging at the start says "the cron fired", which is not the same as "the cron succeeded". A previous outage taught me that distinction the slow way.)

The visible side lives in three places. First, /vr-cams is a browse grid: live models on top, offline section below with pagination, orientation pill at the top. The online half refreshes every 30 seconds while the tab is in foreground. CamCards mirror the existing CastCard component (same dimensions, same hover behavior) so the page feels like part of the catalog instead of a bolt-on. Second, every video and tag detail page renders a recommendation widget below the main content. It picks 3 to 8 cam models whose tags overlap with the source page. The matcher tries tag intersection first, country of model as the next signal, and orientation of the source page as the final fallback. If even orientation can't get me to 3 models, the widget hides itself. Showing one weak rec is worse than showing none. Third, the same widget runs on actor detail pages with the same matcher and the same hide-below-3 rule. The source entity's orientation drives the filter, not whatever orientation pill the visitor has selected. A trans actor's page recommends trans cam models. The filter doesn't second-guess that based on visitor preferences.

There is one architectural delta worth flagging from the existing per-studio NATS affiliate flow. NATS uses a passthrough1 UUID that the client injects into outbound URLs, then echoes back via the postback for click-to-conversion attribution. Stripcash does the same idea but calls the field memberId, and rather than letting the client mint a UUID, they require us to mint it server-side on POST /api/v1/vr-cams/click and bake it into the affiliate URL before the redirect. Same shape, different name. The cam_clicks and cam_conversions tables mirror affiliate_clicks and affiliate_conversions, with a model_id dimension that recorded studios don't have.

The thing I didn't think hard enough about up front: response contracts on the postback side. Stripcash retries any non-2xx response. The handler always returns 200, even when the postback is invalid (synthetic memberId from a test fire, mismatched type, missing fields). Invalid postbacks land in cam_postback_unmatched_log for forensic inspection later. The cost of returning 4xx on invalid input is a Stripcash retry storm. The cost of returning 200 is a slightly bigger forensic table that gets swept after 30 days. Easy trade.

A side gotcha worth telling because it cost me an hour. The Stripcash dashboard has a "Test postback" button that fires a synthetic request at your handler. After a successful round-trip, the dashboard panel says "There are no parameters in your URL." That sounds like a fail. It is not. The dashboard apparently expects a non-empty response body to render its green checkmark, and our handler returns empty body on every postback as part of the always-200 contract. So the dashboard's UI feedback on success is unreliable. PM2 logs and cam_postback_unmatched_log rows are the source of truth on our end.

All five postback type tags fired clean against prod after the dashboard config was saved: first_purchase, rebill, refund, registration, age_verification. Refunds carry negative revenue, which the JSONB column accepts cleanly. Registration and age_verification rows have NULL revenue and NULL transaction_id, and the partial-unique dedup index on (member_id, postback_type, transaction_id) correctly excludes them so a user who registers twice doesn't false-positive as a duplicate.

Camsoda reached out about their affiliate program after launch. The question on my end is the same one Stripchat passed: does the affiliate feed expose a per-model VR filter? If yes, they slot into the same three surfaces alongside Stripchat. If not, the better fit is a non-cam vertical or a future non-VR cams surface I haven't built yet.

Memory rule, since this kind of integration tends to repeat. When a vendor hands you "we have an API", the first question is what filters that API exposes, not what data it returns. The data is almost always there. The filter shape is what makes a placement viable or not.