HatchedDocs
Guides

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_completed with a passing score beats button_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 same eventId is a guaranteed no-op. Derive it from your own domain (lesson_42:user_7, order_8891), never from Date.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.send should never degrade your own UX. The SDK already retries transient failures with backoff; if it ultimately throws, log it and move on — the eventId makes 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:

  1. Verify the HMAC signature against the raw body before parsing JSON. See Handle webhooks.
  2. Dedupe on deliveryId. Persist processed ids; a repeat delivery is a no-op.
  3. Return 2xx fast. Do the heavy work asynchronously — a slow handler gets retried and looks like a failure.
  4. 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.