Skip to main content
Points are the other half of the onboarding checklist. That list measures what somebody did once; this measures what they keep doing — and it uses the same machinery, including the rule that gives it all its value: nothing is ticked by anybody.

A rule is a fact, an amount and a ceiling

It is created inactive. While you are arguing about what it is worth nobody is credited — and turning it on is the moment counting starts, from then rather than from what already happened.

Two kinds of trigger

event is a fact the platform already publishes about one person: verified an address, joined a team, left the waitlist. There is nothing to instrument. The full list travels on the rules read itself, as event_types. product_event is a name your product sends through track(). The vocabulary is yours, so nothing here can refuse a name for not existing — what the panel does instead is say whether this environment has ever seen it, which is the honest half of a check that cannot fail.
A product_event carries a weaker guarantee than an event, and it is the same difference the checklist already has. Analytics drops events when its buffer is full; a dropped event is a point nobody will ever pay, because the ledger has no idea it was owed. For anything that has to be exact, prefer a platform fact or a grant from your own backend.
There is no “entitlement” trigger. A point is an event and an entitlement is a state. Paying for a state would need a clock asking “do they still hold it” and a reversal when they stop — and the answer belongs to a team, of which one person can be on several. A balance whose value depends on which tab resolved it is not a balance.

The ceiling has no default, and that is deliberate

award_limit is required: The first rule anybody points at a high-frequency event with no ceiling is an unbounded loop with a table at the end of it. The windows are rolling rather than calendar days: nobody gets a fresh allowance at a midnight we picked for them. On the list read, every rule comes back with awards and earners. Many awards over few people is the shape of this module’s characteristic mistake: a ceiling left at unlimited on a name the product sends on every page.

What cannot change afterwards

key, kind and event_type are frozen. The ledger lines a rule already wrote say “you earned this for X”, so re-aiming it would rewrite everybody’s history without touching a row. Retire the rule and write another; the old lines keep the name the rule had at the time. Re-pricing is allowed and is not retroactive. Changing 10 to 5 changes what the next award is worth, and nothing that already happened.

Metrics: what you count besides points

While the only unit was the point, “gold is 4 courses” could only be written as “a rule paying 1 point per course, and gold is 4 points” — and the unit died at the door: by the time the fact reached the ledger it had stopped being a course. A metric gives the unit back.
A rule credits the metric instead of the balance — same ledger, same ceiling, same idempotency, one more field:
With metric_id, points is how much of that metric one occurrence is worth: 1 course, 1 e-mail. The balance does not move, and a points_total badge is not unlocked by it — a metric credit moved no balance. Creating a metric does nothing on its own. With no rule crediting it, it stays at zero for everybody and every rung asking for it stands empty — which is why the list read carries lines and holders, the pair that says so before somebody opens a ticket about the loyalty programme not working. The order that works is metric, then rule, then level.
A metric that has ever credited anybody cannot be deleted, and that is a decision rather than a limitation. A ledger line survives the rule that wrote it — it carries the frozen name, and nobody’s balance moves. It cannot survive its metric: without it, the line would stop being a course and silently become a point. Deleting “e-mails” would hand a thousand points to everybody who ever sent a thousand e-mails, and move them up the ladder for it.

A level is a function of what somebody holds

A rung is a set of conditions, and all of them must hold:
Five kinds of condition, each answered by a different machine: The last is the only one that can stop being true: leaving the audience drops the level. If you meant “whoever reached Pro” rather than “whoever is on Pro”, use a segment badge — that one is written down forever the moment it is earned. A level is stored on nobody: it is derived on every read as the highest rank whose every condition holds. That is what keeps it from ever disagreeing with the ledger, the tallies and the badges it describes.

The order became a field

The threshold was the order while a rung was one number. It stopped being able to be: no arithmetic ranks “400 points and 4 courses” against “400 points and 10 courses”. rank is the only thing that orders the ladder, and it is unique per environment. Omitting it on create puts the rung above the last one — which is what adding a level almost always is. Two rungs at the same balance are now allowed, and that is exactly what this exists for: gold can be 400 points and 4 courses while platinum is 400 and 10.

Things worth reading before you need them

  • A rung with no conditions is reached by NOBODY, not by everybody. The other reading would promote the whole environment the moment somebody creates a level and goes to lunch.
  • A negative line drops the level with the balance, immediately and with no announcement — and so does leaving an audience a rung asks for. There is no level_lost: demoting somebody is not a message any product wants sent on its behalf.
  • The congratulation happens once. Somebody who reaches three rungs at once — a large correction, a merge of two records, a badge that was the last condition of two levels — gets one points.level_reached, for the highest. Three facts in the same second would be three emails about one moment.
  • Editing the conditions moves who is in the level immediately, with no ledger line anywhere. It is the difference between a level and a rule: a rule describes what happened, and a level describes what is true now.

Achievements

Three shapes, each satisfied by a different machine: The third is the cheapest to get right, because it reuses the audience engine you already use — and the only difference between a badge and a segment is that a badge does not come off. Somebody who was an enterprise trialist in March still has the badge in June; re-deciding it from the definition would take it back. hidden keeps a badge off the shelf of anybody who has not earned it. It is half the pleasure of the mechanic for one boolean — and it works because the condition never reaches the browser: the API sends a name, a picture and whether it is theirs. Saying “unlock at 40 invites” hands somebody the rule to farm. The condition is frozen too. Whoever already holds the badge earned the old one, and editing what it means would change what every existing unlock claims.
Deleting a rule that an event_count badge counts is refused, and the error says how many badges are in the way. A badge whose rule is gone is a badge on the shelf that nobody can ever earn.

Showing it to the person

<PointsPill /> is <OnboardingLauncher />’s twin — same pill, same ring — for the places a card does not fit. It opens nothing: there is no floating points panel, so onClick is for the product that has its own page. In a product with no gamification configured it draws nothing at all. Or through the client, if you draw your own screen:
level is null when no rung is reached — including the ordinary case of a product with no levels at all. Render the null: a zero would be a rank nobody configured. points_to_next is still only the distance in points, and it is zero when the next rung asks nothing about points. On a ladder with metrics, draw the bar from next_level.requirements: “60 points to go” on a rung that also wants four courses is a bar that fills and promotes nobody.
A level’s conditions travel to the browser and a badge’s do not, on the same response, and it is the same principle twice. A badge is a surprise your product is entitled to keep; a rung is a goal it is asking somebody to reach. The one thing withheld is the name of an audience — that is your own internal copy about your users, and it was never written for them to read.
An anonymous visitor is answered zeros rather than a 401: the widget runs on your landing page. What they earn after signing up follows them — the merge of the visitor into the account moves the whole ledger.

There is no write on this surface

No endpoint on the customer plane moves a balance, and there is not going to be one. It is the checklist’s rule for the checklist’s reason: the moment a page can ask for points, that page’s network tab is the game.

Granting points from your backend

The exception, and it lives on the machine surface — a program you control, not a page you ship:
request_key is required, unlike the optional handle on /v1/usage. A retried grant with no key is a second gift, and the caller that most wants this endpoint is a script somebody runs twice. A replay answers 200 with duplicate: true and the balance as it stands: the state you asked for is true, so an error would be a retry to handle for a correct outcome. With metric, the same endpoint credits a tally instead of the balance — the door for what happened outside your product:
It is a field rather than a second endpoint because it is the same act: the same recipient, the same required request_key, the same line in the same ledger. The answer’s balance becomes that metric’s tally, and metric beside it says which one moved. The line lands with source: "manual", which is the column to group by when a balance looks impossible. An anonymous contact is refused: a balance on a visitor belongs to a page load rather than to a person.

The facts that leave

Three, and all three reach your webhooks: points.awarded is the busiest fact in the catalogue by construction — it fires per line. What bounds it is the rule’s award_limit, which is exactly why that field has no default. A rule may not listen to these three. Paying for being paid is a loop that runs at the speed of the queue, and the API refuses the rule at the moment you create it.

What is erased when somebody asks to be forgotten

The whole statement, the badges, and the record of which levels were announced to them. The panel’s numbers — what each rule paid, how many people hold each badge — are aggregates over whoever is present, so they move rather than break. That is the correct arithmetic: the person who earned those points is no longer here.