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.
Separate business journeys
Separate identities as well as communities
Derek Demos haspointsConfig.useOrgLevelPoints: true. With that setting, one
native member shares spendable points across communities. Root XP and some
aggregate progress reads also span communities. You cannot isolate eight
wallets just by creating eight communities or adding segmentation tags.
Use a stable server-owned visitor × business identity and provision its native
member when that journey is first selected. Omit a shared real email from these
demo identity assertions: native API Link resolves by email as well as external
ID, so reusing an email can resolve different persona IDs to the same account.
Verified account linking needs its own migration and duplicate-reward checks.
Keep the existing organisation signing key on the server. It is organisation
scoped; a separate key per shop would change revocation granularity, not create
a tenant boundary. Leave the organisation points setting and legacy player
identity unchanged. This design separates normal demo journeys; it does not
turn communities into independent security tenants.
A read-only production check found anonymous-login and
anonymous-account-conversion already enabled for Derek Demos. No flag changed.
Integration GET /feature-flags with empty input reads organisation flags;
ff() has no community selector, and community visibility has no override for
these two flags. Deprecated embedConfig.mode affects snippet templates, not
runtime authentication. These are prerequisites only; they do not prove the
guest, account-linking or durable-progress paths tracked in NJ-2584.
The server allowlist maps a business key to its community, challenges, rewards
and event names. Never accept arbitrary browser member IDs, community IDs,
reward IDs or award amounts. Pin pending requests and event retries to the
member and business that created them. Key client caches by business, and clear
the visible balance while the replacement widget identity is unresolved.
This excerpt uses actual draft IDs from the website’s
lib/planet-nudj/persona-content.json. It is part of the server allowlist, not a
complete manifest or permission to enable this journey:
Keep the earning and spending contracts explicit
Each business has two learning challenges with one required multiple-choice action each, six puzzle achievements with matching collectible badges, three shop rewards, a disabled daily streak and a selected draft weekly XP leaderboard. The daily question is repeatable; it is not an automatically rotating question bank. Community creation also seeds two unused draft leaderboards.200, application/pdf
and bytes matching the local SHA-256 hashes. Stable delivery remains pending.
A native wallet entitlement does not make a public static PDF private; choose
public distribution or implement a verified entitlement check before delivery.
Creating a shop reward does not allocate it to the shop. Use
reward_assets.add_to_shop, then read Admin GET /rewards/{rewardId} and verify
the actual allocations[] ID, isShop: true, price, supply and amount. For owned
rewards, paginate Integration GET /me/rewards?communityId=...&limit=100&skip=...
and match member, organisation, community, reward and allocation. Do not infer
wallet ownership from the global earned-reward count.
Reuse exact events and prove the current day
The brand cues change; the six validated physical inputs and event names stay the same. A branded pipe puzzle still uses pipe turns and launch controls.POST /achievements with an explicit
community-scoped query and matching count criterion. The existing
NJ-2534 mapper defect
can replace a supplied custom name with custom_custom; verify the raw
GET /achievements/{id} result. This is the query/criterion excerpt for the
relay achievement, with the other required create fields omitted:
planet.check-in proof, the server must verify the
selected persona’s native daily-challenge completion for the current
Europe/Berlin day. Reuse one nonce for that member, business and day across
retries. Integration GET /challenges/{id} exposes userState.completionsInPeriod
and attempt history, but no completedAt or lastCompletedAt field. Do not
invent a completion timestamp from rounded completionTime. Midnight,
stale-cache and duplicate-proof acceptance tests remain required; the existing
streak timeline timezone defect is tracked separately as NJ-2543.
Read back seeded rules, rendered sections and artwork
Creating a draft community seeded a live referral backing rule paying ten points to each side. All eight new referrer/referee distributions were set to zero and read back. Its hidden lifecycle remains live because the supported referral API has no lifecycle write. Hiding a panel is not disabling a rule. NJ-2583 tracks this gap; the original Planet referral campaign was untouched. Use implemented panel sectionsearn, spend, achievements, streak and
leaderboard. The accepted IDs badgeShowcase and featuredRewards currently
map to null renderers, so these drafts leave them disabled. pointsForSpend
means earning from purchases, not the shop.
NJ-2585
tracks the misleading successful configuration response.
All 48 business-specific badges are complete. Their palettes and shop cues are
visible at 96 pixels, and each retains its puzzle’s object. The final transparent
URLs are attached to all 48 achievements and 48 collectible rewards. Raw
read-backs preserve event queries, criteria, distributions and draft status.

Actual 96-pixel checks of the six Morrow Roast variants on light and dark backgrounds. These are the generated badge assets, not a screenshot of a published wallet.

The same six mechanics in Market Lane's pink palette, with basket cues and transparent object gaps.
image.ingest made the
sources durable, then image.edit with edit_type: "background_removal"
produced RGBA cutouts. Check interior holes as well as corners. The coffee
ring retained checkerboard pixels after removal, and a second pass worsened it.
Both outputs were rejected. A targeted edit made its centre a solid cream
medallion with a bean cue, followed by verified outer removal.
NJ-2586
records the quality failure. Tool success alone does not establish usable alpha.
Keep release checks separate from content completion
identityBinding: {memberId, requestId} and an
SDK identity_ready event carrying the resolved {memberId, communityId, requestId}.
The DOM event is nudj:identity_ready; failure uses identity_failed and
nudj:identity_failed. Match the expected values after the SDK validates origin,
frame and generation. Current frame-ready messages and accepted input JWTs do
not prove which member the iframe resolved. Recheck the deployed contract before
using these new events; draft source and passing unit tests are not release proof.
The website’s scripts/check-planet-persona-content.cjs validates the manifest
and local artefacts. scripts/check-planet-persona-native.cjs audits live Admin
read-backs without awarding points or publishing content. Keep their results
separate from the remaining native payout, wallet debit/reload, duplicate,
A-to-B-to-A, parallel-tab, Berlin-day and original-progress acceptance tests.
Award tiered time and walking badges
You can use native achievement tiers for increasing gameplay milestones. Planet Nudj has two four-tier achievements, each awarding one collectible badge per tier with zero points and zero XP:
The four generated time badges at 96 pixels on light and dark backgrounds. This is asset verification, not a published widget screenshot.

The four generated walking badges at 96 pixels on light and dark backgrounds. The game measures virtual distance only.
Register and validate the event schemas
Create an organisation schema with AdminPOST /event-schemas:
PATCH /event-schemas/{id} and {"state":"published"}.
Create the distance schema the same way with event name
planet.distance.milestone, property distance, minimum 100 and maximum 1200.
These routes worked against Derek Demos; no UI fallback was needed.
You can validate a payload without recording an event or awarding anything:
Count reached milestones once
Emit one signed custom event at each legitimately reached threshold, using the existing event proof flow. Retain a stable nonce per member, community, metric and threshold on retries. When a sample crosses multiple thresholds, emit every missing reached milestone in order. Do not add cumulative values together. Create one community-scoped event query for each metric. For time:Attach a badge to each tier
The raw Admin endpoint supportstierNumber; the MCP reward_allocations
tool currently does not expose it. The gap is tracked in
NJ-2598.
Omitting this field targets tier 1.
supply
for shared unlimited badges. Read all four achievement tiers back after
assignment: four requests without tierNumber can repeatedly change only the
first tier.
For member claims, read the actual active tier and pass its number to
Integration POST /achievements/{achievementId}/claim. The Integration source
schema exposes tiers[].userState and activeTier.userState; verify those
fields against the deployed response rather than assuming a single-tier shape.
If all four tiers complete before any claim, highestCompletedTier is already
4 while activeTier correctly starts at 1. Claim and refresh the active tier
until the remaining completed tiers are collected.
Current verification covers two original-community live achievements, eight
live badge rewards, all four separate distributions and images on each, zero
points/XP, exact custom event queries and the published schemas. All eight
artwork URLs returned HTTP 200. An isolated test driver then accumulated 600
active seconds and 1,440.329 accepted in-game metres over ten real minutes,
using 1,200 game API requests with no shortened clock or prefilled counters.
Independent production reads confirmed all eight milestone events exactly
once, each with its correct published schema and valid payload. This proves
real-clock game-server delivery; it is not a human browser play-through.
The same member then claimed all eight badges through the actual game cards,
with time and walking tiers each advancing 1 through 4. Each duplicate attempt
returned game HTTP 409; native points and XP stayed at zero before and after
every claim. This verifies the game claim action, not a repair of the separate
native-widget claim defect below. A configuration read-back or schema dry run
alone is not a gameplay test.

Actual 390 × 844 reduced-motion game card before successfully claiming the fourth walking badge. This is the game's native-backed claim action, not the official widget or a WebGL walking test.
Native widget claim limitation
The current widget source omitstierNumber in its achievement hook, claim
modal and home-section mutations. The API defaults that omission to tier 1.
Its home section also hides claimed achievements by ID alone, which can hide
later claimable tiers. This source-traced defect is tracked in
NJ-2599.
Until the native fix ships, the game must claim the actual eligible tier
through its server Integration API action; the official widget can still show
the real native badges. Do not describe this workaround as a fixed native
widget claim flow.


