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:Keep the three tokens separate
Establish one player identity
For an existing product, use your authenticated account ID. Planet Nudj instead creates an opaqueplanet-<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:
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:- Request
/api/linkon the configured Nudj user-app origin, withuserToken,clientIdandcallbackPathquery parameters. - Preserve Set-Cookie values in an isolated jar for this player. Follow a bounded number of redirects, checking every destination against the configured origin.
- Request
/api/auth/sessionon that origin using the same jar. Readuser.idanduser.accessTokenwithout logging the response. - Call Integration
/me?communityId=...with the native member token. Verifyid,externalUserIdandorganisationIdagainst your expected player and organisation before accepting the session.
/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: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:
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:
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.
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.
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.
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: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 commit1bf3b9c4.
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.
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.
- 3D relay on mobile
- Graphical guided relay

The 390-pixel 3D relay test tapped the cable objects. Physical targets were unobstructed, and Back to world preserved the active puzzle.
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-sidesign 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 throughPOST /achievements on the Admin
base with explicit queries and a matching tier criterion:
POST /achievements/{id}/distribution:
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.

Derek Demos has three published game-event schemas. The activity column counts valid and invalid incoming events over the last 24 hours.
Create and publish through the API
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.

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
/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.
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.

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.

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.
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: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’s1bf3b9c4 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.
Limits and integration pitfalls
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 inpayload.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 namedcheck-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:
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:
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: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: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 nativequestion-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 nativecommunityData[].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 returned400.

After an actual 60-point purchase, the live widget still shows Maker at 140 lifetime points, with 20 points left to spend.
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:
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:
Award discoveries after completing a lesson
Secret seeker needs three different discoveries. The team, analytics and integration lessons each recordplanet.discovery.completed after the player
finishes an activity:

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.
400. The same run verified
fresh cookie consent, an unobstructed Back button and no extra phone header or
floating launcher.

The actual Secret seeker claim in the embedded widget. Native member read-back confirmed both 20 points and 20 XP.
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.


