이 글은 현재 영어로만 제공됩니다.

(아티클)

Reward Campaign Platforms: Ledger Integrity, Abuse Prevention, and Budget Controls

Treat campaign rewards as ledger entries: give each entry a unique key and lock the member before writing. Prevent abuse without making new users wait, and enforce the budget as an admin check so an over-budget setting cannot be saved.

Polish Design engineering team・2026-10-23

Contents

A prize campaign gives away cost, not traffic. Points can be redeemed, rankings can pay out cash, and raffle tickets can turn into physical goods. When rewards are issued twice, issued in excess, or collected in bulk by a few accounts, the failure shows up in the ledger rather than on the screen. On campaign platform builds and ongoing operations, the POLISH Engineering Team treats every reward as an accounting entry.

Other parts of the same kind of campaign each have a dedicated article:

When a reward carries cost, a bug is an accounting problem

Three kinds of loss recur on campaign platforms, and all three happen in the ledger. The first is the same reward issued twice, usually after a network retry or a scheduled job that ran again. The second is one member sending two requests at the same instant, which drives a balance negative or redeems one coupon twice. The third is a small group using many accounts to collect ranking prizes, so genuine participants receive nothing.

A person in glasses holds a pointer beside an easel; the bar chart on it shows four bars rising from left to right.

The three share a pattern: none appears in testing, all appear once traffic grows after launch, and afterward it is hard to say who took what. The answer is therefore not more prompts on the front end. It is a back end with rules for how rewards move, where every entry can be reconciled. The practices below come from the team's work on campaign platforms where rewards carry real cost.

Three ledger rules: unique keys, member locks, fixed lock order

For rewards with cash cost, the team follows three rules and writes the rules into shared ledger code. Every campaign feature posts through that code. None may bypass it and write to the points table directly.

Every entry has a unique key, so a retry never pays twice

Each entry carries a unique key. When the same key arrives a second time, the ledger code answers that it was already processed and issues nothing. The key should be built from the event itself, such as which campaign, which period, and which member. A random key generated on each attempt would fail here: when a scheduled job reruns, the same reward would receive a new key and count as a new entry. The requirement has a theoretical basis. At the application level, idempotent handling requires the receiver to remember messages already processed, because messages are mostly delivered at least once and redelivery and duplicate processing are common (Helland, 2007). The author labels the paper a statement of personal opinion.

Payment services follow the same approach. Stripe's API documentation explains that a client retrying with an idempotency key does not create an object twice or update it twice after a connection error, and that the server compares parameters and returns an error when the parameters differ from the first request (Stripe docs, Idempotent requests). The documentation also notes that API v2 retries behave differently, so only the basic concept is cited here. The IETF HTTPAPI working group has a draft of an Idempotency-Key header that lets POST and similar requests be retried safely (Jena et al., 2026). That document is still a working draft, not an RFC. The draft does not use payments as its example; applying one key to reward issuance so that nothing is credited twice is this article's own application.

Lock the member before writing, so simultaneous requests queue

One member sending two requests at the same instant is routine, whether from a double tap or from two devices. If both requests read the balance first and then each deducts from it, both pass the check and the balance goes negative. The fix is to lock that member's row inside the same transaction, before anything is written to the ledger. The later request waits and reads the updated balance once the first one finishes. Redemption features follow this pattern as the standard.

Transactions between two members lock in a fixed order

Invitations and gifts touch two members at once. If member A sends to member B while B sends to A, and each locks its own row and then waits for the other, the two wait on each other and deadlock. The rule is to lock by member ID from lowest to highest, regardless of who started the transaction. Advice for InnoDB deadlocks points the same way: when transactions touch several sets of rows, run the operations in the same order every time so transactions queue in order, and put those operations in application functions or stored procedures (MySQL 8.0 manual, 2026 edition).

Locks handle queuing, and the unique key is the backstop. A "seen" record written with a single INSERT … SELECT … NOT EXISTS statement, for example, can deadlock on gap locks with a second flow that writes to the same row. A steadier approach is to insert one row at a time, skip the row when it hits the primary key, and retry once on deadlock. The unique key is the last line of defense here, not the first.

Two people stand on either side of a conveyor; several yellow-capped containers move along it and each person reaches for one.

Rules written in a document are not enough. The team uses a static-scan check script to find code that bypasses the shared ledger code. When existing code is checked against the rules, three kinds of gaps are common: a redemption flow without a lock, where repeated taps by one member could push a balance negative; a ranking prize without a duplicate-prevention key, where a scheduler running twice on the same day could pay twice; and a reconciliation payout that does not check whether the reward has already been claimed. A flaw that lets one coupon be redeemed more than once is fixed by wrapping the whole flow in a transaction, locking the member row, and adding a duplicate-prevention key to the ledger entries.

Fig. 1Reward services post only through the shared ledger code and never write the points table directly

Reward services

  • Redemption and rafflesCall the ledger code, never touch the points table
  • Ranking settlement and payoutsRun on a schedule; each entry carries a key built from the event
Reward request and unique keyResult: posted, duplicate, or rejected

Shared ledger code

  • Unique key checkA repeated key is answered as already processed and pays nothing
  • Member lock and lock orderLock the member first; two-member transactions lock by ID, lowest first
  • One adjustment functionEvery credit and debit passes through this single place
Writes inside one transactionCurrent balance

Database

  • Member rowsLocked inside the transaction
  • Ledger entriesEach entry carries a duplicate-prevention key

Stays inside the boundaryWrite access to the points table belongs to the shared ledger code only. Reward services cannot write to it directly, and the check script scans for code that tries.

Scope: points, coupons, and ranking prizes that carry cash cost

Abuse prevention should not make new users wait

One person opening many accounts is the oldest problem in prize campaigns. Douceur argued, for distributed systems with no central authority, that a single entity can forge many identities and so control a large part of the system (Douceur, 2002). A campaign platform has a central account system, so the setting differs, but the shape of the problem is the same: the number of accounts is not the number of people, and if rewards go out per account, a few people can collect most of the rewards.

The first defense that comes to mind is requiring an account to be a certain number of days old. The rule is simple, but it blocks every genuine new user, on the first day, when keeping that user matters most. The team does not use an age gate as the main defense. At most it remains on features that new users cannot reach anyway, and then for no more than about three days. Four practices replace it, and none of the four penalizes a new user:

  1. A cap on transfers between each pair of friends. Two linked accounts can pass each other only a fixed amount, so alt accounts sending to each other cannot reach any scale.
  2. Merged counting for the same device and network. After login, the front end generates an anonymous device code, stores it in the browser, and sends it with every request; it does no fingerprinting. The back end records each account's recent network address and device code for about three months, and accounts that share either one are merged into a group.
  3. Delayed crediting for high-value rewards. A ranking prize is first recorded as pending and paid out after a default of about one day, and the admin can claw it back during that period.
  4. Anomaly detection with manual review. Within a group of linked accounts, only the highest-ranked one is paid. The rest move to pending review for an operator to decide.

Ranking prizes also have a headcount threshold. When too few members appear on a leaderboard, the top places receive only cosmetic rewards that carry no cost, and the threshold defaults to three times the number of prizes. A board with very few members is easy for a handful of accounts to take over. Every threshold, including the transfer cap, the delay, and the headcount floor, is an admin parameter with upper and lower guardrails, and every change leaves an audit trail. Who set a threshold, when it changed, and to what value can always be looked up.

ApproachWhat it blocksEffect on new users
Account must be a set number of days oldFreshly registered alt accountsNew users can claim nothing on the first day
Transfer cap per pair of friendsLinked accounts passing rewards to each otherSmall; it limits only large transfers between pairs
Merged counting by device and networkOne person with several accountsNew users play as usual; accounts are grouped only when rewards are paid
Delayed crediting for high-value rewardsAnomalies found after the reward is takenNew users play as usual; high-value rewards arrive about a day later
Manual review of flagged accountsGroups of accounts that slipped throughNormal accounts are unaffected; flagged accounts are paid a step later

A reward with conditions must show those conditions to participants. Pending credit, manual review, and transfer caps belong in the campaign rules. The handling principles of Taiwan's Fair Trade Commission for online advertising require that promotional or prize-draw advertisements describe campaign rules accurately and fully disclose attached conditions, obligations, or limits, without layout that makes conditions hard for consumers to notice (Fair Trade Commission, 2023). These are handling principles from an administrative agency, not statutory provisions. The organizer's legal counsel should confirm how the campaign rules are worded.

Fig. 2Every reward request ends in one of three outcomes: posted, pending, or rejected
  1. Check the unique keyA key already processed is answered as a duplicate and pays nothing
  2. Lock the memberRequests from the same member queue
  3. Apply the rulesTransfer cap, device grouping, budget, and balance
  • Rules pass and the reward is not high-value
    Post immediatelyWritten to the ledger
  • The reward is high-value, or the account sits in an anomalous group
    Record as pendingCredited after a delay, or sent to an operator for review; the admin can claw it back meanwhile
  • A cap is exceeded, the key is a duplicate, or the balance is short
    RejectThe reason is returned and nothing is posted

Write the budget as a validator, so an over-budget setting cannot be saved

Operators adjust prize values in the admin when the prizes carry cash cost. If the budget depends on operators remembering it, one wrong figure goes unnoticed until settlement. The team writes the budget as a check in the code: if a prize's unit price multiplied by the expected headcount exceeds the budget, the admin refuses to save. The check runs again before settlement.

The prize table has two more validations. Rank ranges must be continuous, so no place is left without a prize. Different prizes must be converted to one pricing unit before prizes can be compared and summed; cash, points, and physical goods share no common unit, so total cost cannot be checked until the conversion is done.

The value anchor also needs to be a rule. A raffle ticket's value at its redemption price and its value derived from the grand-prize budget can differ by several times. The team's rule is that thresholds are always recalculated as the grand-prize budget divided by the expected number of tickets entered, never carried over from an old value. The tradeoff is that the budget is enforced at the input: an over-budget setting cannot be saved, and adding a prize means adjusting the budget first. That friction is deliberate.

Campaign states are part of the product, and each state needs a screen and a switch

A campaign platform moves through warm-up, open, closed, and archived. A gamified campaign has a clear start and end, and a results page must remain after it closes; the team's gamified campaign case is one such platform. That campaign went from planning and design to finished development in under 2 months. Designing only the open state leaves what happens after closing to chance.

The team builds every state as a screen and a switch from the first day. During warm-up the whole site is a countdown page. The home page responds with normal content, so share previews and search engines receive a complete page, while every other page returns 503. The countdown page loads no 3D code; the approach to 3D performance is covered in web 3D performance engineering. After the campaign ends, the leaderboard and results remain viewable and features such as the raffle close automatically. Archiving is the switch that stops campaign and leaderboard calculation. The front-end build calls the back end's configuration interface, so release order is fixed: back end first, front end second.

Fig. 3Each state is switched by the front-end screen, the admin switch, and the operator action together
Warm-up
Open
Closed
Archived
Front end
Admin and settings
Operations
1
Front endCountdown pageThe home page can still be shared and found in search; every other page returns 503
2
Admin and settingsSet the launch timeControlled by a runtime variable; pages open only at that time
3
OperationsCheck prizes and budgetThe prize table opens only after it passes budget validation
4
Front endCampaign pages and leaderboardRewards always post through the shared ledger code
5
Admin and settingsAdjust admin parametersEvery threshold has guardrails; changes leave an audit trail
6
OperationsWatch the dashboard, run reviewsHandle accounts pending review and rewards pending credit
7
Front endResults page and leaderboardStill viewable; raffles and similar features are closed
8
Admin and settingsClose features that move moneyValidate the budget once more before settlement
9
OperationsSettle and claw backPending rewards can be clawed back within the window
10
Admin and settingsStop campaign and leaderboard calculation
11
OperationsKeep the ledger entriesFor later reconciliation

States: warm-up, open, closed, archived

Trustworthy numbers: page views from behavior, concurrency from heartbeats

A page view counts only when the page actually changes

During a campaign, the organizer watches turnout and results, and how a number is calculated matters more than the number itself. A common way to deduplicate page views is to count one person once within 2 seconds, which is a guess based on time. The team's approach has the front end remember the identity of the current page and send one event only when that identity changes. Moving to another page counts once. Switching tabs on the same page or closing a dialog to return to the original page sends nothing. Entering the site by URL makes the arrival event the page itself, so it is not sent again. A dialog without its own URL sends a different event that counts as a screen opening, not a page view. The condition for a page view is written once and shared by every query, so the per-page totals add up exactly to total page views.

The period before tracking went live is estimated from each page's open events, with the first precise event as the boundary. Estimated cells are hatched on the chart, and the explanation states the boundary time, so estimates are never mixed in and passed off as precise.

Concurrent users are counted by heartbeat

A browser in the foreground sends a signal about every 30 seconds, with one more on initialization and one at the moment it returns to the foreground. A background tab sends nothing. A tab sent to the background, a locked screen, or a closed tab therefore drops out of the count within a short time. Logged-in members are counted as people, so one person on two devices counts once. Visitors who are not logged in are counted by device. The peak cell on the chart also names the minute in which the peak occurred. For the period before heartbeats existed, the count is estimated from the number of people who acted within a window of time; the estimate runs low and is hatched in the same way.

State where the numbers differ from GA4

Two sets of numbers that do not match is normal, and the definitions have to be separated first. In the setup of projects like these, GA4 counts only visitors who consented to statistics, so its figures run low and cannot be tied to member accounts. The self-built tracking covers all visitors and attributes visits to members after login. The session count in GA4 is itself an estimate: Google's help page states that session counts in reports are approximations based on unique session IDs and differ slightly from exact BigQuery calculations (Google Analytics Help, About Analytics sessions). Each metric therefore carries its full calculation next to it, along with where it differs from GA4.

Lists kept on both front end and back end are guarded by a script

The tracking allowlist exists on both the front end and the back end, and relying on people to keep the two in sync guarantees a miss. The team writes a script that compares the lists verbatim and fails outright, rather than skipping silently, when the two disagree or when one side cannot be found. The check itself also goes stale: after fields are added, an assertion that was not updated keeps the check failing, so the script is reviewed against changes on a regular basis.

When this approach does not fit

  • A one-off small giveaway. A ready-made raffle tool is enough, and there is no need to build a ledger.
  • Rewards with no real cost, such as virtual badges only. The price of a duplicate or an over-claim is low, and the three ledger rules can be simplified.
  • A campaign that cannot accept any delayed crediting and accepts only instant payout. Without delayed crediting and manual review, abuse prevention is left with thresholds and after-the-fact tracing: the first hurts new users, and the second cannot recover rewards already paid.

The real effect of each practice depends on campaign size and on the adversary, and whether a threshold is right is something to watch and adjust on a dashboard after launch.

Next step: gather four items first

  • The prize list, with a budget for each prize
  • Expected number of participants
  • Campaign period
  • Whether members must log in

Once these are ready, contact the engineering team with the campaign details and expected scale. For evaluating the development vendor itself, see how to choose a software development vendor.

(문의)

Start a project.

세 단계를 거치면 판단 근거가 되는 접근 방식과 예산 범위를 얻을 수 있습니다.

  1. 01첫 미팅 예약: 시간을 선택합니다. 완성된 사양서는 필요하지 않습니다.
  2. 0215~30분 온라인 상담: 접근 방향, 예산 범위, 일정 초안을 받습니다.
  3. 03자료를 검토한 뒤 약 3~5 영업일 안에 견적서 및 납품 사양 명세서를 제공합니다.
이메일
hello@polish-design.com.tw
우편 주소
대만 타이베이시 중산구 즈푸로 191-1호 3층 (104)

첫 미팅 예약

월요일~금요일 16:00–18:00온라인 30분

먼저 시간대를 선택한 뒤 Google 계정 또는 이메일로 확인합니다. 예약이 확정되면 캘린더에 추가할 수 있습니다.

시간 선택

또는 내용을 먼저 보내기

아직 시간을 정하지 않았다면 프로젝트 내용을 먼저 남겨 주세요. 담당자가 이메일로 회신합니다. 전송 후 이어서 예약할 수 있습니다.

추가 정보(선택): 서비스 항목, 현재 단계, 기존 링크, 알게 된 경로

입력한 정보는 이번 프로젝트 검토에만 사용합니다.

ISO/IEC 27001:2022 인증서

(Certificate)

ISO/IEC 27001:2022

인증 기관
ARES International Certification
인증 범위
인터랙티브 몰입형 3D 시스템의 개발 프로세스, 그리고 네트워크 및 인프라의 운영·관리.

인증 기관 웹사이트에서 확인