Best practices
Patterns for a Hatched integration that scales — designing the economy, sending events safely, handling webhooks reliably, and staying multi-tenant clean.
The other guides show you how to call each piece. This one is the set of decisions that keep an integration healthy once real users are on it.
Design the economy so it rewards engagement, not grinding
Coins, tokens, and skills are knobs you tune in the dashboard — the failure mode is making the most-repeated action the most rewarding one, which trains users to spam it.
- Reward outcomes, not raw volume.
lesson_completedwith a passing score beatsbutton_clicked. If an event is cheap to trigger, give it a small reward or none. - Cap the repeatable stuff. Use per-event-type daily caps on coin rules so the tenth repetition of the same action doesn't pay like the first. See Configure rules.
- Price the marketplace against earn rate. A user earning ~50 coins/day should be a few days away from the cheapest desirable item, not minutes and not months. Re-check pricing whenever you change coin rules — both live on the same config version.
- Keep skills few. More than ~8 skills crowds the widget and dilutes each one's meaning. Pick the dimensions a user would actually recognise.
- Use streaks for habit, badges for milestones. A streak says "you showed up again"; a badge says "you did the thing". Don't award a badge for something that happens daily — that's a streak.
Send events safely
POST /events is the hot path. Two rules:
- Always pass a stable, meaningful
eventId. It's the idempotency key — resending the sameeventIdis a guaranteed no-op. Derive it from your own domain (lesson_42:user_7,order_8891), never fromDate.now()or a fresh UUID on retry, or you'll double-count on every network blip. - Don't block your product on Hatched. Send events from a queue / background
job, not inline in the request that the user is waiting on. A slow or failed
events.sendshould never degrade your own UX. The SDK already retries transient failures with backoff; if it ultimately throws, log it and move on — theeventIdmakes a later replay safe.
Unknown type values are fine — Hatched stores them as custom counters and
never drops them — so you can ship new event types before configuring rules for
them. See Send events.
One identity space per audience
userId is your identifier, opaque to Hatched. If you run multiple
audiences (kids vs. adults, free vs. paid), make
sure a given userId means the same person everywhere — don't recycle ids
across audiences, and don't let two of your tenants collide in one Hatched
customer unless you actually want a shared economy. Each buddy is pinned to one
audience for its lifetime; pick the audience at egg creation.
Pick the right credential for where the code runs
- Server code → secret key (
hatch_live_*/hatch_test_*) in an env var. The SDK throws if you instantiate it in a browser; don't work around that. - Browser code that needs to track events or browse the marketplace → mint a
short-lived, scoped widget session token on your server
(
hatched.widgetSessions.create({ buddyId, userId, scopes, ttlSeconds })) and hand the token to the client. - Browser code that only reads buddy state → a publishable key
(
hatch_pk_*) is enough.
Never ship a secret key in a bundle, a NEXT_PUBLIC_* var, or a mobile app.
Full decision tree: Auth model.
Handle webhooks like a payment provider would
Webhooks are at-least-once, so treat them the way you'd treat Stripe events:
- Verify the HMAC signature against the raw body before parsing JSON. See Handle webhooks.
- Dedupe on
deliveryId. Persist processed ids; a repeat delivery is a no-op. - Return
2xxfast. Do the heavy work asynchronously — a slow handler gets retried and looks like a failure. - Make handlers idempotent. Combined with dedup, replays (manual or automatic) are safe.
Payload shapes: Webhook payloads.
Wait on operations, don't poll
Image-producing calls — hatch, evolve, equip — return an operationId. Use
hatched.operations.wait(operationId) (it long-polls efficiently) instead of
calling operations.get in a setInterval. The stage transition and ledger
writes are already committed atomically by the time the operation completes; the
operation is only telling you whether the visual is also done. Check
buddy.appearance.status for that — see Compositing & stages.
Log the request id
Every API response and webhook payload carries a requestId (X-Request-Id).
Log it next to your own correlation id. When something goes wrong, that single
value lets us trace the request end to end — it's the fastest path to a fix.
Related
- Configure rules — where you tune the economy.
- Send events — the event contract in detail.
- Handle webhooks — verification and retries.
- Troubleshooting — when something's already broken.