Selling VIP & packs (Tebex)
RivalityFX turns its content into something sellable without pay-to-win: you sell a VIP pass (a recurring subscription) and standalone cosmetic packs (one-off buys). Everything purchasable is a privilege or a cosmetic — never a weapon, never an edge in a match. Purchases are delivered Tebex → your cloud API → the game server.
Prerequisite: the
apistore. Entitlements are delivered through the stats API (rfx_store=api). On the defaultkvpstore there is no purchase ingress. Stand up the website layer first — Deploy §10 and Recipe 5.
1. The model — two orthogonal axes
| Axis | What it is | Independent of |
|---|---|---|
| VIP tier | A live subscription that resolves to the vip tier. It folds into the engine's tier system, so every existing tier gate just works — arena creation, private arenas. On cosmetics it is a master key: a subscriber owns every locked pack in the catalogue. | pack ownership |
| Pack ownership | A standalone one-off purchase of a single cosmetic pack. A non-VIP who buys pack X owns exactly X. | the VIP tier |
The two axes differ in what survives when the money stops — that difference is the commercial model, so make it explicit to your players:
- VIP is rented. The subscription carries an expiry (
vipUntil), resolved at the gate — no refresh loop, no cron. The day it lapses, the player loses access to every pack they didn't buy outright. Their equipped skin simply stops being equippable. - A bought pack is owned. It is a persisted id in the player's owned set, checked before the tier, so no expiry can ever take it back.
These are not competing offers. A pack can sit on both axes at once (
access='vip'andsku): subscribers get it as part of the catalogue, and anyone who prefers to own it can buy it once. The shippedstorypack is the reference example.
2. What VIP grants
Every locked cosmetic pack, always. This one is not configurable and needs no descriptor
flag: an active subscriber owns the entire locked catalogue — the packs you marked
access='vip' and the ones you put on the shelf with a sku. Adding a pack to your server
automatically adds it to what VIP is worth.
Beyond that, VIP is what you decide to make a perk. Most capabilities are convar-gated on
the tier ladder (player < vip < staff < admin); set the convar to vip to turn it into a
subscriber benefit.
| Perk | Convar | Default | Make it a VIP perk |
|---|---|---|---|
| Every locked cosmetic pack | — | always on | nothing to configure |
| Open/host an arena at all | rfx_arena_creation (per-mode: rfx_create_tier_<id>) | staff | set rfx_arena_creation vip |
| Host a private (password) arena | rfx_arena_private | all | set rfx_arena_private vip |
| A single VIP item inside a free pack | item locked=true, lock='VIP' | — | set on the item |
Maps are not gated. Battlegrounds carry no ownership fields — every loaded map is available to every player, and the only filter is the operator's rotation allowlist (
rfx_battlegrounds). If you advertise VIP, don't promise maps.
Ranked play is not a sellable perk. A ranked arena is opened by the system/console only (
boot.cfg), never by a player — there is no purchasable ranked tier. Opening ranked ladders is an operator action, deliberately kept apart from "who paid".
The convar defaults above are the code defaults (an unset convar = the documented default). Wiring these into a coherent ladder is Recipe 4 — VIP-perks server. The full convar reference is Server configuration §6.
3. How a pack declares its price
A cosmetic pack is the single source of truth for how it sells — there is no separate catalogue. The sale model lives on the pack descriptor:
| Descriptor | Meaning | Who owns it |
|---|---|---|
status = 'owned' | Free | everyone |
status = 'locked' | Gated | active subscribers (VIP is a master key) |
status = 'locked', sku = '<tebex-id>' | Gated and on the shelf | subscribers or anyone who bought it (for life) |
access ('player' | 'vip') is a label, not a gate: it tells the store front how the
pack is meant to be acquired, but it cannot exclude VIP from a locked pack. Use it honestly —
'vip' for a subscription pack, 'player' for one you mainly sell outright.
-- a subscription-only pack
{ id = 'tactical', status = 'locked', access = 'vip', unlock = 'VIP members only', … }
-- a standalone paid pack (the sku is the Tebex package id — see §5). Still covered by VIP.
{ id = 'neon', status = 'locked', sku = '6543210', unlock = 'Buy the Neon pack', … }
-- the dual-route pack: included with the sub, or bought once and kept for life
{ id = 'story', status = 'locked', access = 'vip', sku = 'rfx_pack_story',
unlock = 'Included with VIP — or buy it once, yours for life', … }Ownership resolves as free OR bought-standalone OR VIP. This is enforced
server-authoritatively at the equip gate — a player cannot equip a cosmetic they don't
own. Outfits (kind='outfit') and weapon wraps (kind='weaponTex') ride the same
two axes with no new gating; see the pack descriptor. Building a
pack is the cosmetics SDK.
⚠ Selling outfit packs adds a running dependency. Streamed freemode clothing (outfit packs) only mounts for clients while the server's license key holds a Cfx.re Element Club subscription (Argentum+, any server above 8 slots). If it lapses, players who bought an outfit pack "for life" can no longer equip it — treat the subscription as an operating cost of the outfit catalogue, not a convenience.
4. The in-game store front
A locked pack the player doesn't own means they aren't a subscriber, so the cosmetics menu
offers the route that fits: Buy ▸ when the pack carries a sku (own this one for life),
Go VIP ▸ otherwise (rent the whole catalogue). A NUI page can't open the system browser,
so the CTA copies the checkout link for the player to paste:
- Set
rfx_store_url(replicated —setr) to your Tebex store base. A standalone pack's link is built as<store>/package/<sku>; VIP points at the store landing. Empty → the CTA just nudges players toward the store. See Server configuration §2. - A pack may ship a
presentshowcase block (hero art + an info/buy link) shown in the unlock panel — see Presentation block. - Card thumbnails come from your assets CDN (
rfx_assets_url) — see cosmetics → thumbnails.
5. The Tebex pipeline
With rfx_store=api, your API exposes POST /tebex as the purchase ingress. It verifies
Tebex's X-Signature over the raw body (HMAC-SHA256 keyed by your webhook secret), answers
the one-time validation.webhook setup handshake, de-dupes re-deliveries (Tebex retries
until it gets a 2xx), then dispatches into the same entitlement store as the admin command:
Tebex purchase ──▶ POST /tebex (verify signature, de-dupe)
│ map package id → grant (TEBEX_PACKAGES)
▼
entitlement store ──▶ player record (vipUntil / packs)
│ at the player's next join (or live, if connected)
▼
Engine.tierOf folds VIP in · equip gate honours owned packs| Tebex event | Action |
|---|---|
payment.completed | grant the pack-type packages bought (one-offs) |
recurring-payment.started / .renewed | grant/extend the VIP package's period (stacks onto remaining time) |
payment.refunded / payment.dispute.lost | revoke whatever was bought |
recurring-payment.ended / .cancellation.* | nothing — VIP lapses naturally at vipUntil |
VIP comes only from the recurring events and packs only from
payment.completed, so a subscription's first payment is never double-counted, and a VIP renewal stacks rather than resetting the clock.
Operator config (no hardcoding)
Two settings on the API service make it yours:
TEBEX_PACKAGESmaps each Tebex package id to what it grants — compact, comma-separated:TEBEX_PACKAGES="111111=vip:30,222222=pack:neon,333333=pack:tactical"vip:<days>for the subscription package;pack:<packId>for a standalone. Four identifiers must agree for a sale to deliver: the Tebex panel package id = the pack descriptor'ssku= the key of the entry here, and the entry'spack:<packId>value = the descriptor'sid(NOT its sku). Every mismatch is silent at buy time. Unmapped packages are logged and skipped — never silently granted.TEBEX_WEBHOOK_SECRET— the signing secret from your Tebex webhook settings, used to verifyX-Signature. Keep it in your secret store, not in code.
⚠ The Tebex store MUST key customers by FiveM license. Entitlements are keyed by
license:<hex>, and the webhook reads the customer username as the player identity (a bare 40-hex value is auto-prefixedlicense:). Configure your Tebex package/store to collect the player's FiveM license as the username — a Steam- or cfx-keyed store will never match a player, and grants will land on nobody.
6. Granting by hand (testing & comps)
Before (or instead of) Tebex, an admin can grant entitlements live. This emits the exact same persisted deltas the webhook does, so it exercises the whole path:
/rfx ent grant <id> vip [days] # grant/extend VIP (days default 30)
/rfx ent grant <id> pack <packId> # grant a standalone pack
/rfx ent revoke <id> vip # revoke VIP
/rfx ent revoke <id> pack <packId> # revoke a pack
/rfx ent show <id> # inspect: vip (+ expiry) and owned packsThese target a connected player (they resolve the license and update the live cache). Offline grants flow through Tebex / the API. The command is admin-only.
7. Honest limits (FiveM reality)
A skin is a client-applied ped model (SetPlayerModel is a client-only native — there is
no server native to set a player's model). So the server can never force a skin; it ships
two server-authoritative layers instead:
- Ownership is server-authoritative — the record lives server-side and is pushed to the UI.
- The legitimate paths are gated — equipping round-trips to the server, which denies an unowned cosmetic. A normal player therefore cannot equip what they didn't buy.
There is deliberately no model-sweep anti-cheat: cosmetics aren't pay-to-win, and a modded client can wear any GTA model regardless of our system (that's general anti-cheat territory, not a cosmetic-specific concern). One accepted edge: a lapsed-VIP player keeps their last skin until they reselect — fine UX, not an exploit.
End-to-end checklist
[ ] rfx_store=api + a deployed stats API # the delivery substrate
[ ] Tebex store keyed by FiveM LICENSE as username # or grants match nobody
[ ] TEBEX_PACKAGES maps every package → vip/pack # unmapped = skipped
[ ] TEBEX_WEBHOOK_SECRET set (signature verify) # in your secret store
[ ] pack descriptors: access='vip' and/or sku=… # the sale model
[ ] Tebex package id == sku == TEBEX_PACKAGES key, # the four must agree
and its value pack:<id> == the descriptor id # (value is the id, NOT the sku)
[ ] sku is the NUMERIC Tebex package id # a placeholder sku matches nothing
[ ] rfx_store_url set # in-game Buy/Go-VIP CTAs
[ ] VIP perk convars set (Recipe 4) # what the subscription buysSee also
- Recipe 4 — VIP-perks server — the convar half.
- Pack descriptor · Cosmetics SDK — building packs.
- Server configuration §5 — the
apistore + tokens.