Daily Login Streaks

A daily claim ladder that rewards consecutive visits — day 1 pays small, day 7 pays big. One endpoint. Pulse tracks the calendar, the grace rules, and the ladder position; you just call claim when the player shows up.

Claim

POST /v1/streaks/{streak_id}/claim

{"player_id": "your-player-ref", "idempotency_key": "uuid"}

-> {"status": "claimed",
    "streak": 4,                       // consecutive days, 1-based
    "rung": 4,                         // ladder position paying out
    "currency": "SC", "amount": 500,
    "grant": { "grant_id": "...", "signature": "...", ... },
    "rewards": [                        // everything this rung awarded
      {"type": "currency", "currency": "SC", "amount": 500, "label": "5 SC"},
      {"type": "raffle_tickets", "raffle_id": "weekly_draw",
       "count": 5, "label": "+5 Raffle Tickets"}   // earned only, NO grant
    ]}

Call it whenever the player logs in or opens their rewards page — the platform sorts out the rest:

Ladder amounts, timezone, and grace policy are configured with the Pulse team at onboarding — your integration is just the claim call.

Rendering a streak calendar

The claim response carries everything a "day 4 of 7" UI needs: current streak, the rung that just paid, and on repeat calls the next_claim_date. Most sites render a 7-day strip with checkmarks up to rung and the upcoming rewards greyed out.

Ticket-award days

A rung can also hand out raffletickets — e.g. "day 3 pays 3 SC anddrops you into the weekly draw." Configure it per day in Studio (the Tickets + Raffle columns on the streak ladder). It surfaces as a raffle_tickets row in the claim response's rewards[]:

Tickets are earned, not paid. A raffle_tickets reward carries no grant and never touches your wallet or outbox — Pulse credits its own ticket ledger. Only a raffle win mints a grant. So iterate rewards[]: settle currency rows through the outbox as usual, and render raffle_ticketsrows as a toast ("+5 Raffle Tickets") — do not credit them.

Collecting the reward

Paying claims append a signed grant to your outbox — verify, credit, ack, same as every feature. The response includes the grant inline for immediate UX. The rewards[] array is the full manifest of a rung: a currency row for the cash (and its grant), plus any raffle_tickets rows, which are ledger-only.

Endpoint summary

EndpointPurposeBillable
POST /v1/streaks/{id}/claimclaim today's rungyes (1 token; same-day repeats free)

Integrate with your coding agent

Building with Claude, Cursor, Copilot, or another AI agent? Paste this prompt — it contains the complete contract, the security rules, and an acceptance checklist, so your agent can implement the integration without reading these docs.

Integrate the Pulse Rewards "Daily Login Streaks" feature into this
codebase. Pulse is a gamification API for sweepstakes sites. Follow this
architecture exactly - it is a hard security contract.

## Non-negotiable rules
1. The Pulse API key (env PULSE_API_KEY, "prw_...") is backend-only:
   never in frontend bundles or browser requests.
2. All Pulse calls go frontend -> our backend -> Pulse.
3. Player balances live in OUR wallet. Pulse pays via signed grants we
   verify, apply idempotently, and acknowledge.
4. Every claim sends an idempotency_key (UUID per user action).

## Pulse API (base https://api.playwithpulse.com, header
## "Authorization: Bearer <PULSE_API_KEY>")
- POST /v1/streaks/{streak_id}/claim
    body {"player_id": str, "idempotency_key": str}
    Success -> {"status": "claimed", "streak": int, "rung": int,
                "currency": str, "amount": int, "grant": {...}?}
    Same local day -> {"status": "already_claimed", "streak": int,
                       "next_claim_date": "YYYY-MM-DD"} (an answer, not
    an error, and not billed). The day boundary uses the timezone
    configured for our site on Pulse's side; do NOT compute any streak
    or calendar logic locally - render what the API returns. Amounts
    are integers in our smallest currency unit (500 = 5.00 SC).
    A "claimed" response also carries rewards[], the full manifest of
    the rung: {type:"currency", currency, amount, label} rows (the cash,
    settled via the grant/outbox) and possibly {type:"raffle_tickets",
    raffle_id, count, label} rows. RAFFLE TICKETS ARE EARNED, NOT PAID:
    they carry NO grant, must NEVER hit the wallet or outbox, and only a
    raffle WIN later mints a grant. Iterate rewards[]: settle currency
    rows, render raffle_tickets rows as a toast only.
- GET /v1/outbox and POST /v1/outbox/{grant_id}/ack for settlement
    (shared with all Pulse features; reuse the outbox worker if one
    already exists in this codebase).

## Build these pieces
A. Backend endpoint POST /api/pulse/streak/claim (player must be
   authenticated): mint a UUID idempotency key, call Pulse claim with
   the player's stable id, return {status, streak, rung, amount_text,
   next_claim_date?} where amount_text formats amount/100 with the
   currency label.
B. Call it automatically once on login/session start AND expose it to a
   "claim today's reward" button. Handle both statuses: "claimed" shows
   a reward toast; "already_claimed" quietly updates the calendar UI.
C. A streak calendar UI: a 7-slot strip using the response - checkmarks
   for days 1..rung, today highlighted, remaining rungs greyed. (If the
   configured ladder is longer/shorter ask me; do not hardcode rewards -
   show amounts only for the day just claimed.)
D. Outbox settlement: identical to the Pulse wheel integration. If this
   codebase already has the Pulse outbox worker, reuse it unchanged -
   streak grants flow through the same pipe. Otherwise implement:
   poll GET /v1/outbox; verify HMAC-SHA256(key=PULSE_WEBHOOK_SECRET,
   msg=canonical JSON of the grant minus grant_id and signature, keys
   sorted, separators ("," ":")) against grant["signature"] in constant
   time; apply to the wallet idempotently by grant_id; then ack.

## Acceptance checklist
- [ ] Claiming twice in one (site-timezone) day: first returns
      "claimed", second "already_claimed"; exactly one wallet credit.
- [ ] Replaying the same idempotency key returns the identical body.
- [ ] raffle_tickets rewards create NO wallet credit and NO outbox
      grant - they are rendered as a toast only.
- [ ] The calendar renders purely from API responses (no local date
      math beyond display formatting).
- [ ] Tampered grant signatures are rejected, logged, never acked.
- [ ] PULSE_API_KEY appears nowhere in built frontend assets.
Ask me for: PULSE_API_KEY, PULSE_WEBHOOK_SECRET, our streak_id, the
player id mapping, and the wallet credit function/location.