Nobody is paid here and nothing is bought, but there is still a balance, a cap and a ledger, because "posts earn points" is the sentence a points farm is built on.
The prices, in one place
Every number the economy uses is in a single file, so nothing can be changed quietly in a service:
const rewards = {
signup_bonus: 50,
profile_bonus: 30,
daily_checkin: 5,
login_bonus: 2,
comment_reward: 3,
};
A published post is worth 3 points. A check-in is 5, plus one extra for each consecutive day after the first, up to ten — so a member who never misses a day settles at 15 a day rather than letting a streak compound into a currency of its own. Signing up is worth 50 once, filling in a profile 30.
Two caps on the thing that could be farmed
The comment reward is the only one that can be looped, so it has two:
export const COMMENT_REWARD_INTERVAL_MS = 5 * 60 * 1000;
export const COMMENT_REWARD_DAILY_LIMIT = 5;
One reward every five minutes, five a day. The interval is the anti-spam rule; the daily cap is what stops a patient spammer from farming the interval all afternoon. If you fail either test the post is still published — it just does not earn anything that time.
Why a ledger and not a counter
A balance column is the obvious design and it is wrong for a forum. A counter cannot tell you where the points came from, cannot undo one, and cannot survive a job that runs twice. So every movement writes a row instead:
- the type of the movement,
- the amount,
- the balance after the movement,
- an idempotency key, unique across the table.
The idempotency key is the part that matters. The key is derived from the thing that earned the points, so a retried job, a double click or a replayed webhook asks for the same key and the database refuses the second one. Paying twice is not possible because the second write has nowhere to go.
The day boundary
"Today" is Asia/Shanghai, for check-ins and for the daily cap. That is deliberate, and it has a cost: a member in Los Angeles gets their day boundary in the middle of their afternoon. The alternative is worse, because two people in different zones would then disagree about what today means.
The ledger is visible in the account area, so if a balance looks wrong, that is where the explanation is.