# Best practices

> Patterns for a Hatched integration that scales — designing the economy, sending events safely, handling webhooks reliably, and staying multi-tenant clean.

Source: https://docs.hatched.live/docs/guides/best-practices

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](/docs/guides/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](/docs/concepts/config-versions).
- **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](/docs/guides/send-events).

## One identity space per audience

`userId` is *your* identifier, opaque to Hatched. If you run multiple
[audiences](/docs/concepts/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](/docs/concepts/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](/docs/guides/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](/docs/reference/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](/docs/concepts/compositing-and-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](/docs/guides/configure-rules) — where you tune the economy.
- [Send events](/docs/guides/send-events) — the event contract in detail.
- [Handle webhooks](/docs/guides/handle-webhooks) — verification and retries.
- [Troubleshooting](/docs/guides/troubleshooting) — when something's already broken.
