# FanZero pact rules (v3)

The contract between `core/fanzero.js`, the HTTP/MCP APIs and the web app. Tests live in
`test/rules.test.js` (fake clock) and `test/e2e.test.js`.

A creator finds a game that fits their audience, chooses a pact and proposes one or more dated
actions. The studio approves the schedule once. Revenue sharing starts on the earliest proposed
action date and ends 28 days later. There is no pitch, social-account connection, proof submission
or second studio confirmation in this flow.

## Scheduled pacts

1. **The offer.** A studio offers a share of the game's net revenue (`share_pct`, above 0 and at
   most 50) for 28 days. Existing offer fields `action`, `requirement` and `min_audience` remain
   available as information about the offer. They add no account-creation requirements or social
   verification. Creators may propose one or more supported action types.
2. **The request.** The creator sends `actions: [{ action, scheduledAt }]`. Supported action types:
   `livestream`, `video`, `short`, `post`, `playtest`. There must be 1–20 actions. Each date is a
   present or future instant when submitted, expressed as epoch milliseconds or an ISO time with
   `Z` or a UTC offset, such as `2026-10-20T18:00Z` or `2026-10-20T20:00+02:00`. Dates without a time,
   zoneless times, epoch seconds and impossible calendar dates are rejected. The core stores dates
   as epoch milliseconds. The order of actions does not determine the start.
3. **The window is fixed at request time.** `startsAt = min(actions[].scheduledAt)` and
   `endsAt = startsAt + 28 * 86400000`. The interval is `[startsAt, endsAt)`: start inclusive, end
   exclusive. Every proposed action must be inside it. Both dates are returned before approval so
   the creator and studio see the same proposed contract.
4. **One studio approval.** The internal pending status remains `pitched` for API compatibility;
   it means a request awaiting approval. Approval sets `acceptedAt` and chooses:
   - `accepted` if the first action is still in the future;
   - `active` if the window has already begun;
   - `completed` if the window has already ended.

   Approval never moves `startsAt`, `endsAt` or any action date. A slow studio approval does not
   grant a fresh 28-day period. No `deliverBy`, delivery deadline, proof or confirmation is created.
5. **Automatic dates.** On the first API call at or after `startsAt`, `sweep()` moves an approved
   schedule from `accepted` to `active`, logging the start at its scheduled instant. At `endsAt`,
   it moves `active` to `completed`, with `completedAt = endsAt`. If the first call arrives after
   both dates, both transitions are materialized once with their original timestamps.
6. **One holder per game.** An approved future schedule reserves its game until `endsAt`; an active
   schedule holds it during the window. Other creators can submit requests while it is reserved.
   The studio cannot approve another while there is a holder (`game_taken`, HTTP 409). Once it is
   free, it may select any waiting request; requests are not a queue. A game is free only when it is
   listed, has an open pact, and has no holder.
7. **Windows never overlap, including history.** Approval checks the proposed interval against
   approved and completed scheduled windows, legacy confirmed windows, and legacy proof awaiting
   review. An overlap fails with `window_overlap` (HTTP 409), leaving the request pending. Exactly
   adjacent windows are allowed. A pending, declined or withdrawn schedule preview reserves no
   revenue. A wholly past, non-overlapping request may be approved as completed while another
   creator holds a current or future window; it creates no current holder. These checks prevent
   late approvals from assigning the same sale to two creators.
8. **Changing your mind.** The creator can withdraw a pending request or an approved schedule before
   it starts. The studio can decline a pending request. An approved agreement cannot be declined
   through the pending-request endpoint. An active or completed window cannot be withdrawn.
   Closing or replacing an offer declines requests still pending on it, without altering approved
   schedules. Offers sent unchanged through `update_game` keep their ids and pending requests.
9. **Recorded actions.** Actions and dates remain on the pact as the agreed plan. FanZero does not
   require proof, track content completion or ask the studio to confirm it again. Legacy delivery
   endpoints reject scheduled v3 claims with `bad_state`.
10. **Revenue.** The share applies to the game's net revenue during the agreed window across its
    stores (Steam, itch.io, the studio's own site, etc.). The game can already be on sale. FanZero
    records the agreement; payment happens outside the platform.
11. **Creator account.** A handle is enough for account creation. Platform, audience, niche, bio and
    channel link are optional; an omitted or unknown platform becomes `other`. There is one pending
    request or current pact per creator per game, across all the game's offers.
12. **Store link.** `storeUrl` is optional, HTTPS only, at most 300 characters. MCP uses `store_url`;
    the HTTP API also accepts `storeUrl`. An empty string or `null` removes it.

Validation errors: `bad_actions` (count/type/shape), `bad_time` (date format/calendar), `too_early`
(past proposed action), `action_outside_window` (an action at or after the 28-day end), `dupe`
(existing pending/current request for this creator and game), `closed` (offer/game not open),
`game_taken` and `window_overlap`. Validation completes before a new claim is stored.

## Data and API contract

- `claimPact(creator, pactId, { actions })` creates a v3 claim. `pitch` is ignored for that claim.
  `channelUrl` may be omitted; if sent, it must be one HTTP(S) URL, at most 300 characters.
- A v3 claim stores `rulesVersion: 3`, `actions: [{ action, scheduledAt: epochMs }]`, `startsAt`,
  `endsAt`, `status`, and (after approval) `acceptedAt`. No social connection is required.
- `claimView` returns the stored window preview even while pending. Legacy proof fields remain
  nullable for API compatibility; v3 always has `liveMin: null`. The actions in views are copies.
- The v3 `holder`/`heldBy` view adds `rulesVersion: 3`, `startsAt` and `actions` to the existing shape
  `{ claimId, handle, status, acceptedAt, deliverBy, liveAt, endsAt, freeAt }`.
  `freeAt = endsAt`; `deliverBy` and `liveAt` are null. A legacy holder's shape stays unchanged.
- `pactView` remains `{ id, gameId, action, actionLabel, emoji, sharePct, durationDays: 28,
  requirement, minAudience, open, createdAt }`. `closePact` also returns the number `passed`.
- `gamePublic` returns `storeUrl`, `holder`, `free`, `waiting` (pending requests) and `streamers`
  (active and completed claims). The internal `streamers` field is retained for compatibility.
- `meta` provides `windowDays: 28` and `maxClaimActions: 20`. `deliverDays: 30` remains solely for
  legacy clients; it does not apply to v3 schedules.
- HTTP `POST /api/pacts/:id/claim` receives `{ actions }`; HTTP
  `POST /api/claims/:id/respond` receives `{ decision: 'accept' | 'decline', note }` for v3.
- MCP `respond_to_claim` approves or declines requests. Its human approval requirement still
  applies to committing a revenue-share agreement. `list_my_games` exposes scheduled actions and
  the fixed window; `confirm_delivery` is available only for legacy proof-based claims.
- `POST /api/logout { side }` signs out the selected `dev`, `streamer` or `curator` session only;
  omitting the side signs out every side.

## Compatibility with existing agreements (v2 and older)

Requests from older clients without an `actions` field retain the previous v2 lifecycle. Existing
agreements are never converted into scheduled claims, and their dates are never rewritten.

- `pitched -> accepted -> delivered -> active -> completed`, with side exits `declined`,
  `withdrawn` and `expired`.
- Acceptance sets `deliverBy = acceptedAt + 30 days`. Missing that deadline expires an accepted
  legacy claim. This deadline and pre-v2 backfill apply only when `rulesVersion !== 3`.
- Marking delivered needs a valid HTTP(S) `proofUrl` and a valid `liveAt` (default now).
  `max(acceptedAt - 1 hour, previous window end) <= liveAt <= min(now + 10 minutes, deliverBy)`.
  Errors remain `missing_proof`, `bad_time`, `in_future`, `too_early`, `too_late`.
- The studio confirms delivery once. `startsAt = liveAt`, `endsAt = liveAt + 28 days`; delayed
  confirmation never moves the dates. Past windows go directly to `completed`.
- Proof still in review holds the game only while `liveAt + 28 days > now`; after that it remains
  decidable but frees the game. Its historical interval is still protected from overlapping v3
  approval while awaiting review.
- `send_back` clears proof and `liveAt`. Only the first send-back extends the deadline to at least
  `now + 3 days`; a later send-back after the deadline expires the claim. Send-back cannot create a
  second holder. Declining delivered proof creates no revenue window.
- Pre-v2 confirmed windows keep their original end dates. Old delivered claims lacking `liveAt`
  use `deliveredAt`. Old accepted claims lacking `deliverBy` get a legacy deadline and `preV2: true`;
  those pre-v2 slot claims retain their existing overlap terms.
- Legacy endpoints remain `POST /api/claims/:id/delivered { proofUrl, liveAt, note }` and the MCP
  `confirm_delivery` tool. Legacy `holder` fields and response shapes remain compatible.
