Skip to main content
You can connect your own game to Nudj while keeping points, achievements and rewards in Nudj. Planet Nudj uses this pattern for a browser game in which you walk around a small planet, visit feature stations, race, cross stepping stones and find treasure. The live loyalty widget appears in the character’s phone. This guide records the implementation and tests from 9 September 2026. It is a worked example, including its deployment limits, rather than a game SDK.

How the game is built

The website uses Next.js and React for the page and phone overlay, with Three.js for the world, spherical movement, collision geometry and animation. The scene reports actions to a same-origin Next.js route. That route validates progression, saves it and delivers signed events to Nudj. Keep responsibilities explicit: Do not award points locally and again through Nudj for the same activity. Native quiz completion and check-ins remain native actions. Custom game events cover activities such as walking, racing and finding treasure.

Keep the three tokens separate

The game needs no Manager or MCP bearer token at runtime. Use administrative credentials separately when configuring your community, achievements and rewards.

Establish one player identity

For an existing product, use your authenticated account ID. Planet Nudj instead creates an opaque planet-<UUID> on the server and keeps it in an encrypted, HttpOnly, Secure, SameSite=Lax cookie. It never accepts a browser-supplied member ID, email or points balance. Cookie deletion starts a new demo player; this is not cross-device account recovery. Create a dedicated signing key in your organisation’s API configuration and store it in your server environment. This Node.js example signs the widget identity with the same standard-library mechanism used by the game:
Use kid in the header and sub for the external ID. The current embed identity flow accepts a maximum lifetime of 600 seconds; this game uses 300. A one-hour legacy API Link example is not suitable for the widget identity flow.

Obtain the native Integration API session

The widget identity JWT is not an Integration API member access token. The game’s server adapter establishes the native session through API Link:
  1. Request /api/link on the configured Nudj user-app origin, with userToken, clientId and callbackPath query parameters.
  2. Preserve Set-Cookie values in an isolated jar for this player. Follow a bounded number of redirects, checking every destination against the configured origin.
  3. Request /api/auth/session on that origin using the same jar. Read user.id and user.accessToken without logging the response.
  4. Call Integration /me?communityId=... with the native member token. Verify id, externalUserId and organisationId against your expected player and organisation before accepting the session.
Build the link with URLSearchParams so the nested callback is encoded correctly:
/api/link and /api/auth/session are user-app routes, not Integration API routes. This is the server bootstrap used by this example, not a dedicated OAuth token-exchange endpoint. See API Link authentication for the wider sign-in flow. The demo encrypts native credentials with AES-256-GCM inside its HttpOnly cookie. The session endpoint returns only public member fields and a fresh widget JWT. When the native token expires, bootstrap again using the same external ID. Coalesce the game and phone’s first session request; otherwise simultaneous first loads can create different players. The demo also serialises bootstrap across tabs with the browser’s Web Locks API where available.

Put the official widget inside the game

Use the official loader’s inline mode. Give its container a real width and height, and create the container before loading the script:
Planet Nudj uses https://derek-demos.nudj.cx/embed.js. Use the user origin configured for your organisation. The parent game’s origin must be allowlisted in the community embed settings. Inline mode creates one iframe in the container, with no floating action button or overlay panel. Do not create a floating widget and reparent its injected #nudj-embed-fab or #nudj-embed-panel elements into your game. In React, initialise after the container ref is mounted, retain the instance while the phone is closed, and call window.nudj("destroy") on disposal. The game obtains a valid session before appending the loader, because a failed userJwt callback can otherwise lead to guest recovery. It displays a retry state when session bootstrap fails. The supported public views are:
There is no public challenge-ID or action-ID navigation command in the version tested. Admin preview panel commands are not a merchant API. Keep task hints outside the iframe rather than trying to change its cross-origin DOM. If you listen for nudj-widget-ready or nudj-widget-close, validate both event.origin and event.source === iframe.contentWindow. Do not treat a message from another frame as authentication or achievement proof.

Match the widget’s gift count

The header’s gift badge counts claimable, unclaimed achievements. It is not an inbox unread count or a count of owned rewards. The widget posts the same value to its parent as { type: "nudj-unread-count", count } when it changes. Reuse your existing message listener and current iframe reference:
This is display data, never authority to grant a reward. Register the listener before loading the iframe and remove it on disposal. Accept updates down to zero after claims. Before the initial authenticated count read completes, its value is unknown; a read failure must not manufacture a zero. The world launcher loads its initial count through the existing authenticated GET /api/planet-nudj/game?notifications=1. This keeps the widget iframe lazy: an eager iframe can stop at cookie choice before it emits a count. The equivalent native read used by the server is GET /api/v2/integration/achievements?communityId=<id>&status=live&limit=50&skip=0 with the existing member headers. Count edges where activeTier.userState.isClaimable is true and isClaimed is false. That is the widget header’s current query and predicate. Its 50-item page covers Planet Nudj’s 14 regular achievements; larger catalogues need pagination and a matching widget fix before promising an accurate total. Refresh on initial load, phone close and confirmed native progress, with no interval polling. The native count lookup is read-only; the reused game GET retains its existing deferred outbox delivery. Abort obsolete requests and use a version counter so an older read cannot overwrite a newer iframe message. The live test at website commit 8807a9d9 reused an isolated player’s existing First wander qualification. The closed launcher showed one before the iframe opened. After Required-only consent, the native gift badge also showed one. An actual pointer claim succeeded; closing the phone refreshed the count to zero and hid the launcher badge. No member or game-store reset was used. The exact result, live log and screenshots are in output/planet-audit/launcher-notifications. Fixture checks at 1440 and 390 pixels cover invalid counts, wrong origins/frames, 99+ display, zero/unknown states, accessibility and the single lazy iframe. These are live-preview results, not verification of a production website deployment. The loader documents nudj:unread_changed on document and nudj("on", "unread_changed", callback), but the deployed loader suppresses these in inline mode. Emission currently depends on a closed panel and an existing floating badge, tracked as NJ-2574. The validated iframe message above is the supported protocol path available to an inline host today.

Open Lite views as the same player

The passport annex opens four native Lite views from the character’s phone. Mono shows the existing welcome challenge on its own. Journey, Stamp card and Calendar show the community’s challenges. These views reuse native progress; changing the layout must not create another completion or reward.
Planet Nudj phone showing the passport annex, its Mono format card and a button to open the native view

The passport annex links to four native Lite views. Each opens in a new tab as the game's current player; looking at another format awards nothing.

The game uses a same-origin handoff instead of relying on whichever Nudj account the browser last used:
The route accepts only mono, journey, stamp or calendar. It reuses the existing game-session handler to verify or renew the native member, obtains a fresh five-minute identity JWT, and returns a 303 through API Link. Its fixed callbacks are /lite/mono/{welcomeChallengeId} and /lite/{journey|stamp|calendar}/{communitySlug} on the configured Nudj user host. Copy the renewed game cookie onto that response and set Cache-Control: private, no-store and Referrer-Policy: no-referrer. Never log the redirect’s Location header: it contains the short-lived identity proof. The implementation added 36 route lines and a 41-line focused check on 10 September 2026. Run node scripts/check-planet-lite.cjs in the website checkout. Live tests used four separate browser contexts with only the game’s cookie and no Nudj cookies. Every format opened as the same member, retaining ten points, thirty XP and one completed welcome challenge. A separate test clicked all four links from the phone and confirmed the same identity and an empty referrer. Invalid formats returned 400; cross-site requests returned 403. The four Lite variants were already enabled for this community. No challenge duplication or enabledForSingleView update was needed. Native presentation defects remain tracked: Stamp’s hard-coded Pampers cash graphic, Mono’s completed attempt and badge counters, and Journey’s mobile heading overlap. Their presence does not mean the underlying member progress was lost or awarded twice.

Validate gameplay before sending an event

The game posts a small envelope to its own server:
The server rejects cross-site requests, bodies over 2 KB, unknown action fields, out-of-order sequences, unsupported IDs and caller-supplied award amounts. It checks movement against server time and shared world geometry. Six ordered landings qualify island arrival; digging at the island qualifies a separate treasure milestone. Guided accessibility input uses the same native rewards. State and its event outbox commit in one SQLite transaction. Send native HTTP requests after committing, then mark delivered events. The browser serialises actions, coalesces unsent movement samples and retries the same sequence and payload if a response is lost. Network delays can compress the interval between two valid actions arriving at the server. For a transition that arrives before its minimum duration, the game returns 425 and retryAfterMs without consuming its sequence. Retry the same envelope after that delay. Invalid order or an expired run still returns 409. These are the example game’s response conventions, not Nudj API status rules.

Validate each puzzle input

The six camp games became interactive puzzles in website commit 1bf3b9c4. Each normal game uses the objects in the 3D world. Guided mode renders a playable graphical version of the same mechanism and uses the same server rules. For example, the game sends a cable rotation through its existing endpoint:
In lib/planet-nudj/game.ts, lines 144–168 at that commit, the server checks the active mechanism, its allowed controls, normal-mode proximity and minimum input interval before applying the move. quest-puzzles.ts evaluates pipe connections, the note sequence, weight torque or symbol alignment. Only a solved mechanism advances progress. The old quest-step action is rejected at a puzzle stage, so it cannot bypass the controls. The existing outbox, stable nonce and native achievement claim still own the once-only reward.
Actual mobile 3D relay with six rotatable cable tiles and Back to world control

The 390-pixel 3D relay test tapped the cable objects. Physical targets were unobstructed, and Back to world preserved the active puzzle.

Version migration restarts incomplete older runs at the new definition. Completed native rewards and their event records remain intact. Definition versions are two for cloud, pyramid, lab, relay and tower, and three for Wisp. A completed cosmetic preview still needs an explicit new playthrough before its first native award; pending or delivered reward events prevent another rewarded replay. The saved results contain six normal 3D runs, six graphical guided runs and one additional mobile 3D relay run. Each earned ten points and ten XP through a native claim, survived reload and rejected a duplicate claim with 400. The normal tests clicked the objects; Nib also used held climb input. Mobile checks used browser-emulated touch, not a physical handset. Raw logs, emitted result JSON and screenshots are in output/planet-audit/puzzle-games. These results concern the preview connected to Nudj, not a production website deployment.

Sign and submit the native event proof

After validation, reuse the server-side sign function from the identity example:
integrationBase ends in /api/v2/integration. Keep it in trusted server configuration. The token’s expiresAt is milliseconds; JWT iat and exp are seconds. The nonce stays stable for this member, event and milestone across retries and reloads. Generating a new nonce for each request defeats duplicate protection. Never return signed event proofs to the game client. Configure a native achievement to listen to the exact custom event name. For the island example, use planet.island.arrived with custom category and subcategory. For native quiz completion, use challenge_challenge-completion and filter eventSourceId to the actual quiz challenge ID.

Register and test custom-event schemas

Configure the achievement separately

A schema validates an event’s payload. An achievement defines the qualification and reward. Create the achievement through POST /achievements on the Admin base with explicit queries and a matching tier criterion:
Then call POST /achievements/{id}/distribution:
The distribution endpoint creates and links its event ID. Read the achievement back and verify the exact event name, matching alias, open period, draft state and persisted ten-point/ten-XP distribution. For an existing achievement, updates use POST /achievements/{id}, unlike event-schema updates, which use PATCH. The MCP convenience mapper still rewrote planet.draft.approved to custom_custom in this test, tracked as NJ-2534. Explicit Admin queries preserved the correct name. Do not infer success from a create response without read-back. On 10 September, the owner approved publishing ready content. The lab, relay and tower achievements were then changed to live with status-only Admin updates. Read-back confirmed their exact queries, artwork and ten-point/ten-XP distributions were preserved. The API added null defaults to optional tier detail fields; this did not change the rules or rewards. Publish the native achievement before enabling its game mapping. Both the game queue and signer must reject disabled rewards. An ID alone does not activate a draft. Earlier preview completions must not silently become reward events: offer an explicit new playthrough for the live reward, and continue rejecting replays once its native event has been recorded. The publication checks, before the physical-puzzle redesign, passed normal and guided runs for all three activated camps with their real ordered steps, native ten-point/ten-XP claims, reload and duplicate-claim rejection. An existing player who had finished the lab preview received nothing on load or reload after activation. Choosing “Play again to earn the reward” started at step zero; completing all six steps then qualified and paid once. No member or game-store reset was used. The raw test records and screenshots are saved under output/planet-audit/activated-camps in the website checkout.

Register the payload schema

The Admin API can create and publish the same event schemas shown under Organisation settings → Event schemas. Schema setup is organisation-scoped; keep the administrative credential on your server or in protected tooling. The game continues to use its member token and signed event proof.

Find your schemas in the admin

Select your organisation, then open Organisation settings → Developer → Event Schemas. For this demo, select Derek Demos and open the schema list. The URL ending in /event-schemas/new opens an empty creation form. It does not list definitions you have already created.
Live Nudj admin list showing published walking, island-arrival and treasure schemas with valid and invalid event counts

Derek Demos has three published game-event schemas. The activity column counts valid and invalid incoming events over the last 24 hours.

These screenshots show the live demo in dark mode on 10 September 2026. Activity counts change as players explore the game.

Create and publish through the API

Create a draft for the exact event name your game emits:
The create response returns its ID and state: "draft". Use that ID to publish with PATCH /event-schemas/{id} and body { "state": "published" } after testing. The event name cannot be changed through that partial update. Do not define a property called name: it is reserved for the event name and is removed before property validation. Unknown additional payload properties are currently allowed. Open a schema and expand a property to inspect its type and constraints. The island event requires an integer landings value of exactly six, so both its minimum and maximum are six. assisted is a required boolean: both true and false qualify, covering guided and manual play.
Published planet.island.arrived schema with landings expanded, showing minimum six, maximum six, Integer only and Required enabled

The island schema's landings property is required, integer-only and limited to exactly six. The remaining required properties are nonce, schemaVersion and assisted.

Test a payload and inspect real events

Send that body to /event-schemas/validate. Valid results have valid: true, schemaExists: true and no issues. A string such as "yes" for assisted or five landings returns field issues. Dry-run validation writes no event. Planet Nudj now has published definitions for planet.walk.milestone, planet.island.arrived and planet.treasure.found. They drive First wander, Against the current and X marks your spot through the existing exact-name achievement queries. No second set of achievements or duplicate rewards is needed. The game payload needs no new schema ID; lookup uses its event name within the organisation. All three also require nonce, a string of 1–240 characters, and schemaVersion, an integer equal to one. These checks validate the payload shape. Your game server must still verify that the player actually performed the activity. In the admin, Test a payload → Validate performs the same dry run. The Live events panel shows stored ingress. Expand a row to compare its received payload with its validation result. Use Valid or Invalid to filter it, or pass isValid=true or isValid=false to GET /event-schemas/{id}/events.
Expanded valid island-arrival game event alongside a successful dry-run validation with landings six and assisted true

A real guided game run produced a valid island-arrival event. The separate dry-run panel confirms that six landings and a boolean assisted value match the schema; that dry run stores nothing.

Invalid island event showing landings must be at least six and assisted must be a boolean, with matching dry-run field errors

The deliberately invalid test event has five landings and assisted set to the string yes. Both the stored event and the dry run identify the two offending fields.

Actual keyboard movement and a fresh guided island run produced events with the correct validation.schemaId and validation.isValid: true in the live schema streams. Their native achievement claims paid 20/20 for walking and a combined 30/30 plus the key for island arrival and treasure. Missing, out-of-range and wrong-type payloads were rejected in the test endpoint; actual signed invalid submissions were retained as invalid diagnostic rows and awarded nothing. There is an open ingress error-contract defect, tracked internally as NJ-2556: a published-schema rejection returns 500 from /integration/events/claim, and retrying its already rejected nonce returns 200 even though the stored event remains invalid. The admin screenshot’s HTTP 400 help text does not describe this signed-claim failure correctly. Check native validation and achievement state when diagnosing this case. Do not credit rewards or manufacture a new nonce to turn a rejected payload into a successful milestone. This differs from replaying an originally valid event, which is an intended successful no-op.

Read progress and claim native rewards

All Integration calls below use the native member headers shown above: Event processing is asynchronous. A successful event POST does not mean the achievement is already claimable. Poll with a bounded delay and read:
After claiming, re-read the native member balance and ownership. Repeating an already-completed event claim returned 200 without another payout in testing. Repeating an achievement claim returned 400; reconcile its native claimed state before deciding whether a retry succeeded. Do not swallow every 400 as success. Badges and ordinary rewards are separate collections. The Secret island key is a badge and appears in /me/badges. /me/rewards intentionally excludes it. The Planet explorer pass is an ordinary reward asset. Follow totalCount and edges pagination, and check member, community and expiry when using ownership to unlock gameplay. Creating a reward does not place it in the shop; it needs a shop allocation.

Implementation size and checks

The website’s 1bf3b9c4 commit contains 868 physical lines across the eleven selected integration, puzzle-rule and puzzle-interface files below, measured on 10 September 2026 by counting newlines in git show output. This includes blank lines and comments. It excludes the 3D scene and mechanism meshes, other game files, tests, artwork and community configuration; it is not the total game size. Measuring the commit keeps concurrent uncommitted UI changes out of this snapshot. Run the focused checks in the website repository:
Live standard and mobile guided island runs proved actual game input, all six landings, digging, two native achievement completions, a combined claim delta of 30 points and 30 XP, and key ownership. Reload preserved member and balance. Repeat claims, another member’s unearned claims, forged input and cross-site requests were rejected. Those runs claimed through the Integration API; they do not by themselves prove the widget’s claim buttons. The original cloud, pyramid and six-step Wisp quests also have six recorded normal/guided passes: each qualified, claimed 10 points and 10 XP, survived reload and rejected a duplicate claim. A fresh production read confirms all six exact achievement records remain complete and claimed for those amounts. Their screenshot index is retrospective because the original runs did not save standalone result JSON; the fresh database read is stored separately. The new lab, relay and tower rewards were subsequently published after owner approval; their activation checks are separate from these earlier original-quest runs. An independent live phone check confirmed that the iframe and game session resolved the same native member, with one inline iframe and no floating launcher. Keep UI claims, keyboard/touch paths and other activities in the test record as they are checked; a passing island run is not proof of every game path.

Limits and integration pitfalls

The example uses Node 22’s built-in SQLite in WAL mode on one persistent preview host. Use shared transactional storage for multiple application instances, or deliberately provision one persistent host. An ephemeral serverless filesystem does not provide this persistence guarantee. Movement validation bounds browser telemetry using geometry and server time. A scripted client that follows those constraints can still play. This is not an authoritative multiplayer physics server, and it must not prove real purchases or other high-value commerce. Keep sample orders and receipt demonstrations out of genuine purchase events.

Open issues found by the main-journey test

These failures remain separate from the passing island and race tests. Do not use the transient widget display as proof of persisted state. Inline check-in scope. A real widget check-in wrote the correct community ID in payload.communityId and eventSourceId, but omitted top-level communityId. The widget briefly displayed one day; a native /me/streaks read still showed zero, and the community-scoped achievement remained locked. The shared event logger took community scope from a navigation cookie that was absent in this inline session. A source fix is being prepared to pass the known target explicitly. For the example’s first-check-in achievement, a temporary query selects the exact community as eventSourceId, keeps native category/subcategory/name and preserves member and organisation scope, without requiring the missing top-level field. Changing this query did not recalculate existing saved progress on read. It also does not repair the durable streak or an event carrying a different, incorrect top-level community ID. Do not generate a replacement nonce to force a payout. Challenge points and XP. The native welcome challenge completed and awarded 20 XP plus its collectible, but zero points despite a configured 20-point reward. With fixed points distribution disabled, challenge points are multiplied by the player’s action-XP ratio. In this example the action explicitly awards zero XP, while the ratio denominator uses the organisation’s 50-XP action default. Completion bonus XP does not correct that ratio. Validate actual awarded points for your configured XP policy; do not change organisation-wide defaults to repair a single demo. The welcome, quiz, check-in, pass and factory journey is not yet verified end to end. These native issues need resolution and another live run before release.

Record a game check-in through a custom event

The older integration used a custom event named check-in. A historical migration converted it to native community / check-in. Current POST /integration/events/claim emits custom / custom and preserves the exact payload.name; adding a category parameter cannot turn it into a native event. You can still configure a streak to accept your game’s own check-in event:
Preserve the full existing trigger set when updating it. The server sends the signed custom event only after the player’s explicit check-in action. This example uses a check-in-YYYY-MM-DD milestone in the verified organisation’s Europe/Berlin timezone, not the player’s browser timezone. The signed payload contains the same day and timezone, and the signer rejects mismatches. Cap proof expiry at organisation midnight. Native event time is its ingestion time, so an old check-in must not be replayed tomorrow as if it happened today. The example retains expired outbox entries with a reason and asks the player to check in again. Verify native /me/streaks after delivery, including repeated clicks, native and custom events on one day, and date boundaries. The live game API path produced a properly scoped event, a durable one-day streak and a claimable first-check-in achievement. Same-day repetition left one delivered daily event. Claiming through the widget handler added 30 points and 30 XP, and reload preserved them. The repeatable browser check now covers the actual game button, cookie consent, scaled pointer claim, native read-back and reload for a fresh player. The same test player’s native quiz was completed through all three widget questions. Invoking the real widget claim handler produced a200 Integration achievement claim and increased points by20 and XP by20; native state showed claimed. The later repeatable phone test also verified the actual pointer path. The inline loader scales its iframe; map pointer coordinates through the frame rectangle and its internal viewport after scroll and animation settle. The API streak date read also exposed a timezone defect: a check-in at 22:21 UTC was already September 10 in the configured Europe/Berlin timezone, but recentCompletedDates reported September 9. The single and batch helpers use server-local day comparisons and UTC date slicing. This is tracked for repair; keep the game calendar aligned to the organisation rather than copying the incorrect API timeline date.

Native game results and reward configuration

The native memory game exposed a zero-value default error. With three pairs worth ten score each, timeBonus: 0 and a 30–30 reward band, a completed round scored 73 because the game replaced zero with a multiplier of one. Native read-back confirmed completion with zero points and XP, since 73 missed the configured band. Treat a saved game configuration and its rendered result as separate checks. For the example, explicitly enabling a one-point-per-second bonus and assigning a 30–120 completed-round band preserved the intended ten-point/ten-XP reward. A fresh live test then completed the nested game, submitted score 117 and received ten points and ten XP. The source correction for explicit zero remains pending release; a configuration workaround does not prove that source bug is fixed. When replacing game rewards through MCP, supply both the score conversion and the distribution bands in the same assignment, then read them back:
This operation replaces the reward configuration. Sending only bands cleared the existing conversion; a conversion-only nested update cleared the bands. A top-level scoreConversion on games.update was ignored by its mapper. Use the supported full assignment shape while that contract is being clarified.

Referrals, birthdays and boosts

Referrals require an accepted code and qualification

Referral setup has separate rewards for the referrer and the invited player, plus a backing achievement defining what the invited player must do. In Planet Nudj, each side receives ten points and ten XP after one native challenge completion. Merely opening a link or accepting its code does not earn that award. Use the native member token for these Integration API calls: Two isolated game players verified the native flow. After code acceptance, the invited player completed the welcome challenge through the real phone. Each side received ten referral points and ten XP. The code recorded one use with conditionsMet: true; repeated acceptance returned 200 without another payout. The welcome challenge’s separate zero-point defect remained visible in this test. Two integration gaps remain. New code creation succeeds, but the widget does not refresh its code query until reload, tracked as NJ-2558. The game’s incoming referralCode URL parameter also needs a server handoff. The tested acceptance API infers the community from the code and cannot enforce the caller’s expected community before writing. NJ-2559 adds that check. Do not put an Admin bearer in the game or rely on a check after acceptance to repair a wrong-community write.

Add giveaway entries to a referral reward

Giveaway prize supply and entry supply are separate. The Christmas AirPods 5 draft uses one prize and an unlimited entry pool. The relevant Admin reward fields are:
Create the complete reward through POST /api/v2/admin/rewards and read it back before attaching it. At launch, the proposed referral package adds an allocation with allocationType: "entries", the giveaway’s rewardId and amountToDistribute: 1 to the referrer side. Preserve the existing points and XP fields. One newly qualified friend should produce one entry; a share-button click alone does not establish a referral. This AirPods reward remains draft with no allocation attached. Its proposed Christmas Day 2026 draw is not a public launch announcement. Participant eligibility, real identity checks, isolated entry-award tests, the game arrival handoff and physical prize fulfilment still need completing before launch. Draft status does not schedule the automatic giveaway job. The MCP reward_giveaways tool currently hard-codes unlimited prize supply and omits draw-mode controls, tracked as NJ-2571. Use the supported Admin API fields to configure one prize. Reward tags and campaigns accept existing entity IDs, not new text labels; invalid labels currently cause an ObjectId HTTP500, NJ-2570. Omitting optional tags allowed the draft to be created successfully.

Birthdays follow the real calendar

The native question-birthday action writes birthDay, birthMonth and birthYear to the member profile. An isolated player entered 31 December 2000 through the phone. /me and a reload returned the same date; the widget marked the activity completed and disabled its button. Points and XP did not increase. Planet Nudj’s birthday reward is configured for twenty points and twenty XP on the birthday, with zero advance days, a one-day membership minimum and the date locked after entry. This proves date capture and no early award. It does not prove a future scheduled payout; keep that outcome pending until its real date.

Verify boost expiry with another earned reward

The live one-hour points and XP boosts doubled a ten-point/ten-XP treasure claim to twenty of each. After both effects actually expired, the same player walked 19.7 world units and qualified for First wander. Its eventual claim paid the base twenty points and twenty XP, and native communityData[].effects was empty. The first post-expiry claim returned 500. Native read-back showed no award and an unclaimed achievement, then an identical retry succeeded once. Better Stack recorded a MongoDB write conflict during points distribution, tracked as NJ-2560. The points and XP helpers launch expired-effect cleanup outside the reward transaction, a suspected race that still needs repair. An expired entry remaining in a read response is not itself proof of an active multiplier: cleanup is lazy.

Distinguish visible tier progress from persisted promotion

The configured tiers use lifetime points earned: New neighbour at zero, Explorer at 80 and Maker at 140. A real guided cloud quest and phone claim increased the main player’s lifetime points from 130 to 140. The widget showed Maker with 80 spendable points. The player then bought another 60-point pass through the phone’s quantity and confirmation screens. Available points fell to 20, while lifetime points stayed at 140, XP stayed at 200 and Maker remained. Reload preserved the result and both purchased pass assets. A duplicate quest claim returned 400.
Actual Planet Nudj widget header showing Maker tier and 20 spendable points after a reward purchase

After an actual 60-point purchase, the live widget still shows Maker at 140 lifetime points, with 20 points left to spend.

The database read before that purchase confirmed 80 available points, 140 lifetime points and 200 XP, but no loyaltyTierState. The widget derives the visible label from lifetime earnings. This is the stored-promotion defect tracked as NJ-2554. These demo tiers have 1x multipliers and no entry reward configured; this run does not prove promotion history or an entry payout.

Purchase and factory proof

The same player completed the welcome and quiz, checked in, then earned an optional island achievement. With 70 legitimately earned points, the player bought the 60-point explorer pass through the native shop. Available points fell to 10 while lifetime points stayed 70 and XP stayed 120. /me/rewards returned the pass asset for that member and community. The server then confirmed all factory prerequisites. The player entered an idea and ran the reduced-motion factory through its real timed game endpoint. The native Programme maker achievement completed, and its pointer claim added 20 points and 20 XP. Reload preserved the pass, factory milestone and balances. This proves the downstream purchase and factory contract. The advertised main-only welcome/quiz earnings still require the challenge-XP repair described above; optional earnings do not remove that release hold. The same normal-factory check has a strict --main-only mode. It skips optional Curious mind and First wander claims, then checks every native balance:
The pre-release run stopped at welcome with zero available points, zero lifetime points and 20 XP. It exited with failure before any optional claim. This is current evidence for NJ-2532. It does not prove the later stages; rerun after the repair is released. The script reports mainOnlyFundingVerified: true only after the whole sequence, reload and duplicate protection pass. Default mixed-funding mode always reports false for that field. Run the repeatable phone and native-game flow in the website repository:
It creates an isolated test member and checks actual pointer input, same-member widget identity, check-in/claim persistence, duplicate rejection, no floating launcher, no extra phone header, and the native memory reward. It does not claim coverage of every station or future calendar outcome.

Award discoveries after completing a lesson

Secret seeker needs three different discoveries. The team, analytics and integration lessons each record planet.discovery.completed after the player finishes an activity: Await delivery to Nudj before showing “Discovery saved to Nudj.” If delivery fails, keep the activity’s retry button available. Opening a lesson does not qualify. Art, receipt examples, Lite formats and disabled draft quests do not emit these discovery events.
The right-key cabinet phone lesson after completing the role puzzle, showing Discovery saved to Nudj

Completing the three-job role puzzle records one discovery. The saved message appears after Nudj acknowledges delivery; it does not change anyone's actual permissions.

For normal walking, retain the server’s station-proximity check. The accessible guided path records a server-side lesson start and requires the same lesson ID and at least 900 ms before completion. It preserves the player’s physical position. Both paths use the same signed-event and nonce contract described above, so repeating a completed lesson creates no additional native event. The guided player’s three completions produced exactly three scoped native events. A real pointer claim in the phone paid 20 points and 20 XP. Reload preserved the reward; repeating the integrations lesson kept the event count at three, and a duplicate achievement claim returned 400. The same run verified fresh cookie consent, an unobstructed Back button and no extra phone header or floating launcher.
Native Secret seeker reward confirmation in the game phone showing Claimed and plus 20 points

The actual Secret seeker claim in the embedded widget. Native member read-back confirmed both 20 points and 20 XP.

Run the live guided check from the website repository:
Normal walking separately passed all three lesson completions, native event delivery and a real phone claim for 20 points and 20 XP. Reload retained the three discoveries and reward; a duplicate claim returned 400 with unchanged balances. The normal race and factory checks also passed with native claims and duplicate protection. The factory used optional walking earnings; the advertised main-only funding still waits for the challenge-points repair.