{"openapi":"3.0.0","info":{"title":"Permissionless Groups Backend","version":"0.1.0","description":"Sparse-Merkle-tree proof service for CRC score-groups: leaves are `(member → score)`, only the root is published on-chain, and clients fetch a proof here to mint.\n\n**Public — no authentication** (safe for any client, e.g. the SDK at mint time):\n- `GET /groups/{group}/proof/{address}` — membership proof against the published on-chain root\n- `GET /groups/{group}/root` — current published root\n- `GET /groups/{group}/mint-limits/{address}` — mint headroom\n- `GET /health/*` — liveness / readiness\n\n**Operator-only — require a bearer token; NOT intended for public callers:**\n- `POST /internal/groups/*` — score ingestion & group registration (token issued by the auth service)\n- `GET /internal/snapshot/*` — bulk membership+scores export, verified against a *separate, dedicated* key and additionally network-restricted; present only on publishing source nodes."},"servers":[{"url":"/score-groups"}],"components":{"schemas":{},"parameters":{}},"paths":{"/health/live":{"get":{"responses":{"200":{"description":"Service process is alive","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"service":{"type":"string"},"uptimeSec":{"type":"number"}},"required":["status","service","uptimeSec"]}}}}}}},"/health/ready":{"get":{"responses":{"200":{"description":"Service is ready to accept traffic — DB reachable","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"service":{"type":"string"},"uptimeSec":{"type":"number"}},"required":["status","service","uptimeSec"]}}}},"503":{"description":"Service is degraded — a required dependency is unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["degraded"]},"service":{"type":"string"},"error":{"type":"string"}},"required":["status","service","error"]}}}}}}},"/groups/{group}/proof/{address}":{"get":{"summary":"Fetch a Merkle proof for a user's score in a group","description":"Returns the leaf value, score, proof bytes, and the root for the given (group, user). Public — no authentication.\n\nThe backend is double-buffered: this endpoint reads from a snapshot that matches the root the SCORE_MINT_POLICY contract has stored ON-CHAIN RIGHT NOW. The live tree advances in the background as the bridge pushes scores; the snapshot only flips at each successful publisher mine. As a result, the returned `(value, proof, root)` triple verifies against the verifier contract immediately — no SDK-side wait/refetch logic required for the normal path.\n\n`publishStatus=pending` is only emitted during the bootstrap window before a group's first publisher mine. In that case, SDKs should retry after one `publishCadenceSeconds`. Otherwise, submit normally.","parameters":[{"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","description":"EVM address of the score group (case-insensitive).","example":"0x7CadB2E92295F3E4fA65D3d4E7265E2e05d7a783"},"required":true,"name":"group","in":"path"},{"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","description":"EVM address of the user the proof is generated for (case-insensitive).","example":"0x227642eBD3a801E7b44A5bb956c02C2d97Ca71F0"},"required":true,"name":"address","in":"path"}],"responses":{"200":{"description":"Score, leaf value, proof bytes, current root, and proof-validity hints (publish status + cadence).","content":{"application/json":{"schema":{"type":"object","properties":{"groupAddress":{"type":"string","description":"Group address from the request, lowercased.","example":"0x7cadb2e92295f3e4fa65d3d4e7265e2e05d7a783"},"userAddress":{"type":"string","description":"User address from the request, lowercased.","example":"0x227642ebd3a801e7b44a5bb956c02c2d97ca71f0"},"value":{"type":"string","description":"32-byte leaf value as 0x-hex (= the score encoded into a leaf-hash). Mostly informational; the SDK does not need to pass this on-chain — the score mint policy recomputes it from `scoreRaw`.","example":"0x00000000…00000051"},"scoreRaw":{"type":"string","description":"User's raw score (e.g. 0–100) as a decimal string. Pass this as the `score` value encoded for the score mint policy.","example":"81"},"proof":{"type":"string","description":"Sparse Merkle proof bytes encoding the path from leaf to root. Pass directly as the `proof` value encoded for the score mint policy.","example":"0xfc0000…"},"root":{"type":"string","description":"32-byte SMT root the proof verifies against. SDKs MAY cross-check this against the on-chain root (see `publishStatus`) before submitting; if `publishStatus=pending` the on-chain root is still the previous one and submitting now will revert.","example":"0x0316afc3ae3065de6e75d64a2c4320351c53cf0f…"},"depth":{"type":"number","description":"Tree depth used for proof encoding/verification (always 160 in production).","example":160},"publishStatus":{"type":"string","enum":["published","pending"],"description":"Whether the `root` in this response has been broadcast on-chain. `published`: this exact root has been broadcast (publisher loop fired). The score mint policy will accept this proof until a NEW `updateMerkleRoot` lands (see `nextHeadEarliestAt`). `pending`: the publisher loop has NOT yet broadcast this root. Submitting a tx now will REVERT — the on-chain root is still the previous one. SDKs should wait for the next publisher cycle and refetch the proof, or apply their own backoff.","example":"published"},"rootPublishedAt":{"type":"string","nullable":true,"description":"ISO-8601 timestamp when this exact `root` was broadcast on-chain. `null` when `publishStatus=pending` (broadcast hasn't happened yet). Note: this is the time of OUR broadcast, not the on-chain confirmation time. Allow ~1 block (~5 s on Gnosis) for tx mining before assuming the verifier has the new root.","example":"2026-05-06T04:30:25.000Z"},"lastHeadChangedAt":{"type":"string","nullable":true,"description":"ISO-8601 timestamp of the most recent published root for this group, regardless of whether it equals the proof's `root`. Equal to `rootPublishedAt` when `publishStatus=published`. When `publishStatus=pending`, this is the prior on-chain root's broadcast time — useful for telling the user 'on-chain head last changed N seconds ago'. `null` when this group has never had a root published yet.","example":"2026-05-06T04:30:25.000Z"},"nextHeadEarliestAt":{"type":"string","nullable":true,"description":"Soft upper bound (ISO-8601) on when the on-chain head MAY change next. Under the snapshot/double-buffer model, `/proof` is constructed against the snapshot of the live tree that matches the currently-on-chain root — so a proof returned with `publishStatus=published` is valid against the verifier RIGHT NOW. The on-chain head will not flip until the next epoch tick (`EPOCH_BLOCKS` cadence), so this field reports approximately one cadence into the future regardless of state. `publishStatus=pending` only fires in the bootstrap window before the very first publisher mine for a group; SDKs should retry after one cadence in that case. Treat as a SOFT bound: block intervals vary, the publisher may run slightly late.","example":"2026-05-06T04:35:25.000Z"},"publishCadenceSeconds":{"type":"number","description":"Configured publisher cadence in seconds (= `EPOCH_BLOCKS × CHAIN_BLOCK_TIME_SECONDS`). The publisher fires on blocks divisible by `EPOCH_BLOCKS`, so this is the maximum wall-clock gap between successive onchain root updates. Stable across requests until the operator reconfigures — SDKs may cache it.","example":300},"serverTime":{"type":"string","description":"ISO-8601 timestamp at which this response was generated. Use this (instead of the client's local clock) when computing remaining validity, to avoid client clock-skew bugs.","example":"2026-05-06T04:30:30.123Z"},"proofValidityModel":{"type":"string","enum":["single-root","ring-buffer"],"description":"How the verifier contract treats this proof's lifetime. `single-root`: verifier stores ONLY the latest root. The proof becomes invalid the moment a new `updateMerkleRoot` lands. The deployed score mint policy is single-root. `ring-buffer`: verifier stores the last N roots (e.g. ROOTS_KEPT=10). The proof remains valid for ~N publisher cycles after `rootPublishedAt`. **When the verifier migrates to a ring-buffer model, this enum value flips and SDKs SHOULD widen their TTL math accordingly** — `nextHeadEarliestAt` then describes the moment a new root publishes (not when this proof expires); expiry is `rootPublishedAt + N × publishCadenceSeconds`.","example":"single-root"}},"required":["groupAddress","userAddress","value","scoreRaw","proof","root","depth","publishStatus","rootPublishedAt","lastHeadChangedAt","nextHeadEarliestAt","publishCadenceSeconds","serverTime","proofValidityModel"]}}}},"404":{"description":"Group not registered.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/groups/{group}/root":{"get":{"parameters":[{"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"required":true,"name":"group","in":"path"}],"responses":{"200":{"description":"Published (on-chain) Merkle root for the group","content":{"application/json":{"schema":{"type":"object","properties":{"groupAddress":{"type":"string"},"root":{"type":"string","description":"The PUBLISHED (on-chain) Merkle root the verifier contract currently holds — the same root `/proof` verifies against. Advances only when a new root has actually landed on-chain, never the live (unpublished) tree."},"depth":{"type":"number"},"publishStatus":{"type":"string","enum":["published","pending"],"description":"`published`: `root` is on-chain. `pending`: bootstrap window (group registered, no publisher mine yet) — `root` is the empty-tree default."},"rootPublishedAt":{"type":"string","nullable":true,"description":"ISO-8601 time this root was broadcast on-chain; null while pending."}},"required":["groupAddress","root","depth","publishStatus","rootPublishedAt"]}}}},"404":{"description":"Group not registered","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Remote root source unavailable (transient; retry)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/groups/{group}/mint-limits/{address}":{"get":{"summary":"How much can a user mint/migrate against a score group","description":"Fuses the deployed `OffchainScoreBasedMintPolicy` getters + Hub reads + the score this backend owns into one answer. Public — no authentication.\n\n`migration` mirrors the router/migration branch of `beforeMintPolicy` for the user's OWN personal collateral (token id = uint256(uint160(user))): cap = historicalSupply + personalMinted − treasuryBalance. The policy lazily snapshots historical supply on the first migration, so before that the raw getter reads 0; `*Effective` fields predict the real limit by substituting live `Hub.totalSupply` (what the contract will snapshot). Prefer `leftToMintEffective`.\n\n`personalMint` is the score-gated personal-issuance ceiling: min(score,100)/100 × issuance.\n\nCAVEAT: these are CONTRACT-CAP numbers only. The amount actually routable via the Hub trust graph is frequently LOWER (trust-topology / live-supply path bound). Cross-check against `circlesV2_findPath` maxFlow before assuming `leftToMintEffective` is reachable in a single path.","parameters":[{"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","description":"EVM address of the score group (case-insensitive).","example":"0x7CadB2E92295F3E4fA65D3d4E7265E2e05d7a783"},"required":true,"name":"group","in":"path"},{"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$","description":"EVM address of the user whose mint limits are computed (case-insensitive).","example":"0x227642eBD3a801E7b44A5bb956c02C2d97Ca71F0"},"required":true,"name":"address","in":"path"}],"responses":{"200":{"description":"Migration + personal-mint limits for the (group, user) pair.","content":{"application/json":{"schema":{"type":"object","properties":{"groupAddress":{"type":"string","example":"0x7cadb2e92295f3e4fa65d3d4e7265e2e05d7a783"},"userAddress":{"type":"string","example":"0x227642ebd3a801e7b44a5bb956c02c2d97ca71f0"},"collateralTokenId":{"type":"string","description":"The user's personal CRC token id = uint256(uint160(user)), as a decimal string.","example":"196743003800004331574209137775474814545847546352"},"migration":{"type":"object","properties":{"historicalSupplyInitialized":{"type":"boolean","description":"Whether the policy has already snapshotted historical supply for this (group, collateral). When false, `*Effective` fields predict the limit using live `Hub.totalSupply` — the value the contract itself snapshots on the first migration. The `*Raw` fields would be 0 here (faithful to current storage, but misleading as a 'can I migrate' answer).","example":false},"historicalSupplyOnTodayRaw":{"type":"string","description":"`policy.getHistoricalSupplyOnToday(group, collateral)` verbatim (wei). 0 when uninitialized.","example":"0"},"mintedAmountOnToday":{"type":"string","description":"`policy.getMintedAmountOnToday(group, collateral)` (wei, demurrage-adjusted to today).","example":"142471689811225351435"},"alreadyInTreasury":{"type":"string","description":"Treasury holding of this collateral (wei) — `IScoreTreasury.balanceOfCollateral`, Hub.balanceOf fallback. This is the amount already consumed against the cap.","example":"142471689811225351435"},"maxTotalRaw":{"type":"string","description":"historicalSupplyOnTodayRaw + mintedAmountOnToday (wei). The literal on-chain cap.","example":"142471689811225351435"},"maxTotalEffective":{"type":"string","description":"Effective cap (wei): substitutes live totalSupply for the historical term when uninitialized.","example":"5423389882657548034515"},"maxTotalEffectiveCrc":{"type":"string","description":"maxTotalEffective formatted as CRC (18 dp).","example":"5423.389882657548034515"},"leftToMintRaw":{"type":"string","description":"max(0, maxTotalRaw - alreadyInTreasury) (wei). 0 before the first migration.","example":"0"},"leftToMintEffective":{"type":"string","description":"max(0, maxTotalEffective - alreadyInTreasury) (wei). The headline 'how much can this user migrate now' number.","example":"5280918192846322683080"},"leftToMintEffectiveCrc":{"type":"string","description":"leftToMintEffective formatted as CRC (18 dp).","example":"5280.91819284632268308"},"collateralLimitReached":{"type":"boolean","description":"True when alreadyInTreasury > maxTotalEffective — the contract would revert `CollateralLimitReached`.","example":false}},"required":["historicalSupplyInitialized","historicalSupplyOnTodayRaw","mintedAmountOnToday","alreadyInTreasury","maxTotalRaw","maxTotalEffective","maxTotalEffectiveCrc","leftToMintRaw","leftToMintEffective","leftToMintEffectiveCrc","collateralLimitReached"]},"personalMint":{"type":"object","properties":{"score":{"type":"string","description":"User's score from the published SMT snapshot, capped at MAX_SCORE (100).","example":"81"},"currentIssuance":{"type":"string","description":"`Hub.calculateIssuance(user)` currently-claimable amount (wei). NOTE: the contract uses the value snapshotted by `snapshotIssuance()` at mint time; this is the live estimate of the limit if the user snapshots now.","example":"11000000000000000000"},"scoreAdjustedIssuanceLimit":{"type":"string","description":"min(score,100)/100 × currentIssuance (wei) — the personal-issuance mint ceiling estimate.","example":"8910000000000000000"},"scoreAdjustedIssuanceLimitCrc":{"type":"string","description":"scoreAdjustedIssuanceLimit as CRC (18 dp).","example":"8.91"}},"required":["score","currentIssuance","scoreAdjustedIssuanceLimit","scoreAdjustedIssuanceLimitCrc"]},"serverTime":{"type":"string","example":"2026-05-19T12:00:00.000Z"}},"required":["groupAddress","userAddress","collateralTokenId","migration","personalMint","serverTime"]}}}},"404":{"description":"Group not registered in this backend.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"On-chain reads not configured (RPC_CHAIN_URL / SCORE_MINT_POLICY_CONTRACT_ADDRESS / HUB).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/groups/mint-limits/batch":{"post":{"summary":"Mint limits for many (group, user) cells in one round trip","description":"Batch variant of `GET /{group}/mint-limits/{address}`. The `mode` field on the body discriminates between three input shapes — pick the example below that matches your use case.\n\n### Modes\n\n**`userGroups`** — one user across N groups. The primary SDK pre-mint check; the response has one cell per group.\n```json\n{ \"mode\": \"userGroups\", \"user\": \"0x227642…71F0\", \"groups\": [\"0x7CadB2…a783\", \"0x6897dD…be3d\"] }\n```\n\n**`groupUsers`** — N users in one group. Ops/analytics; the response has one cell per user.\n```json\n{ \"mode\": \"groupUsers\", \"group\": \"0x7CadB2…a783\", \"users\": [\"0x227642…71F0\", \"0xb2FA2e…aDBe\"] }\n```\n\n**`matrix`** — cartesian product of `users` × `groups`. Response cell count = `users.length * groups.length` and that product must be <= 100. Useful for dashboards.\n```json\n{ \"mode\": \"matrix\", \"users\": [\"0x227642…71F0\", \"0xb2FA2e…aDBe\"], \"groups\": [\"0x7CadB2…a783\"] }\n```\n\n### Caps & dedup\n\n- Total response cells ≤ 100 (each axis ≤ 100, and `users × groups` ≤ 100 for matrix).\n- Duplicate addresses are de-duplicated case-insensitively before fan-out — the response cell count equals the unique-pair count, not the raw input.\n- Addresses are case-insensitive (lowercased in the response).\n\n### Error semantics\n\nPer-cell failures (group not registered in this backend, on-chain read reverted, indexer + fallback both failed) are returned as `{ ok: false, error: { code, message } }` items alongside successful cells — the HTTP status stays 200, so one bad cell never poisons the other 99. `503` only for whole-request misconfiguration (RPC env vars unset); `400` for body validation (empty arrays, > 100 cells, malformed addresses, payload > 64 KiB).\n\nPer-cell error codes: `group_not_found` | `group_lookup_failed` | `policy_revert` | `treasury_not_set` | `treasury_revert`.\n\n### Response cell shape\n\nEach `results[i]` is `{ ok: true, groupAddress, userAddress, collateralTokenId, migration, personalMint }` (success) or `{ ok: false, groupAddress, userAddress, error }` (failure). The success payload matches the single-call `GET /{group}/mint-limits/{address}` response 1:1 modulo the top-level `serverTime` (hoisted out of cells) and the `ok` discriminator.","requestBody":{"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"mode":{"type":"string","enum":["userGroups"]},"user":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"groups":{"type":"array","items":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"minItems":1,"maxItems":100}},"required":["mode","user","groups"],"description":"One user across N groups (primary SDK pre-mint check). One cell per group.","example":{"mode":"userGroups","user":"0x227642eBD3a801E7b44A5bb956c02C2d97Ca71F0","groups":["0x7CadB2E92295F3E4fA65D3d4E7265E2e05d7a783"]}},{"type":"object","properties":{"mode":{"type":"string","enum":["groupUsers"]},"group":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"users":{"type":"array","items":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"minItems":1,"maxItems":100}},"required":["mode","group","users"],"description":"N users in one group (ops/analytics). One cell per user.","example":{"mode":"groupUsers","group":"0x7CadB2E92295F3E4fA65D3d4E7265E2e05d7a783","users":["0x227642eBD3a801E7b44A5bb956c02C2d97Ca71F0","0xb2FA2eE7Ef39dc5DeA7A473B0014fAE76b3fAdBe"]}},{"type":"object","properties":{"mode":{"type":"string","enum":["matrix"]},"users":{"type":"array","items":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"minItems":1,"maxItems":100},"groups":{"type":"array","items":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"minItems":1,"maxItems":100}},"required":["mode","users","groups"],"description":"Cartesian product. `users.length * groups.length` must be <= 100 (enforced in handler — the per-axis caps above only bound zod work).","example":{"mode":"matrix","users":["0x227642eBD3a801E7b44A5bb956c02C2d97Ca71F0","0xb2FA2eE7Ef39dc5DeA7A473B0014fAE76b3fAdBe"],"groups":["0x7CadB2E92295F3E4fA65D3d4E7265E2e05d7a783"]}}]},"examples":{"userGroups":{"summary":"userGroups — one user, N groups (SDK pre-mint check)","description":"Returns one cell per group. The headline number per cell is `migration.leftToMintEffectiveCrc` (how much the user can migrate against that group right now).","value":{"mode":"userGroups","user":"0x227642eBD3a801E7b44A5bb956c02C2d97Ca71F0","groups":["0x7CadB2E92295F3E4fA65D3d4E7265E2e05d7a783"]}},"groupUsers":{"summary":"groupUsers — N users, one group (ops/analytics)","description":"Returns one cell per user. Useful for inspecting headroom per member of a group.","value":{"mode":"groupUsers","group":"0x7CadB2E92295F3E4fA65D3d4E7265E2e05d7a783","users":["0x227642eBD3a801E7b44A5bb956c02C2d97Ca71F0","0xb2FA2eE7Ef39dc5DeA7A473B0014fAE76b3fAdBe"]}},"matrix":{"summary":"matrix — users × groups cartesian product (dashboards)","description":"Returns `users.length * groups.length` cells. Product must be <= 100.","value":{"mode":"matrix","users":["0x227642eBD3a801E7b44A5bb956c02C2d97Ca71F0","0xb2FA2eE7Ef39dc5DeA7A473B0014fAE76b3fAdBe"],"groups":["0x7CadB2E92295F3E4fA65D3d4E7265E2e05d7a783"]}}}}}},"responses":{"200":{"description":"Per-cell results (mixed ok/error allowed).","content":{"application/json":{"schema":{"type":"object","properties":{"serverTime":{"type":"string","example":"2026-05-21T12:00:00.000Z"},"results":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"groupAddress":{"type":"string"},"userAddress":{"type":"string"},"collateralTokenId":{"type":"string"},"migration":{"type":"object","properties":{"historicalSupplyInitialized":{"type":"boolean","description":"Whether the policy has already snapshotted historical supply for this (group, collateral). When false, `*Effective` fields predict the limit using live `Hub.totalSupply` — the value the contract itself snapshots on the first migration. The `*Raw` fields would be 0 here (faithful to current storage, but misleading as a 'can I migrate' answer).","example":false},"historicalSupplyOnTodayRaw":{"type":"string","description":"`policy.getHistoricalSupplyOnToday(group, collateral)` verbatim (wei). 0 when uninitialized.","example":"0"},"mintedAmountOnToday":{"type":"string","description":"`policy.getMintedAmountOnToday(group, collateral)` (wei, demurrage-adjusted to today).","example":"142471689811225351435"},"alreadyInTreasury":{"type":"string","description":"Treasury holding of this collateral (wei) — `IScoreTreasury.balanceOfCollateral`, Hub.balanceOf fallback. This is the amount already consumed against the cap.","example":"142471689811225351435"},"maxTotalRaw":{"type":"string","description":"historicalSupplyOnTodayRaw + mintedAmountOnToday (wei). The literal on-chain cap.","example":"142471689811225351435"},"maxTotalEffective":{"type":"string","description":"Effective cap (wei): substitutes live totalSupply for the historical term when uninitialized.","example":"5423389882657548034515"},"maxTotalEffectiveCrc":{"type":"string","description":"maxTotalEffective formatted as CRC (18 dp).","example":"5423.389882657548034515"},"leftToMintRaw":{"type":"string","description":"max(0, maxTotalRaw - alreadyInTreasury) (wei). 0 before the first migration.","example":"0"},"leftToMintEffective":{"type":"string","description":"max(0, maxTotalEffective - alreadyInTreasury) (wei). The headline 'how much can this user migrate now' number.","example":"5280918192846322683080"},"leftToMintEffectiveCrc":{"type":"string","description":"leftToMintEffective formatted as CRC (18 dp).","example":"5280.91819284632268308"},"collateralLimitReached":{"type":"boolean","description":"True when alreadyInTreasury > maxTotalEffective — the contract would revert `CollateralLimitReached`.","example":false}},"required":["historicalSupplyInitialized","historicalSupplyOnTodayRaw","mintedAmountOnToday","alreadyInTreasury","maxTotalRaw","maxTotalEffective","maxTotalEffectiveCrc","leftToMintRaw","leftToMintEffective","leftToMintEffectiveCrc","collateralLimitReached"]},"personalMint":{"type":"object","properties":{"score":{"type":"string","description":"User's score from the published SMT snapshot, capped at MAX_SCORE (100).","example":"81"},"currentIssuance":{"type":"string","description":"`Hub.calculateIssuance(user)` currently-claimable amount (wei). NOTE: the contract uses the value snapshotted by `snapshotIssuance()` at mint time; this is the live estimate of the limit if the user snapshots now.","example":"11000000000000000000"},"scoreAdjustedIssuanceLimit":{"type":"string","description":"min(score,100)/100 × currentIssuance (wei) — the personal-issuance mint ceiling estimate.","example":"8910000000000000000"},"scoreAdjustedIssuanceLimitCrc":{"type":"string","description":"scoreAdjustedIssuanceLimit as CRC (18 dp).","example":"8.91"}},"required":["score","currentIssuance","scoreAdjustedIssuanceLimit","scoreAdjustedIssuanceLimitCrc"]}},"required":["ok","groupAddress","userAddress","collateralTokenId","migration","personalMint"]},{"type":"object","properties":{"ok":{"type":"boolean","enum":[false]},"groupAddress":{"type":"string"},"userAddress":{"type":"string"},"error":{"type":"object","properties":{"code":{"type":"string","description":"`group_not_found` | `group_lookup_failed` | `policy_revert` | `treasury_not_set` | `treasury_revert`","example":"policy_revert"},"message":{"type":"string"}},"required":["code","message"]}},"required":["ok","groupAddress","userAddress","error"]}]}}},"required":["serverTime","results"]}}}},"400":{"description":"Validation failed (empty arrays, > 100 cells, malformed addresses).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"On-chain reads not configured (RPC_CHAIN_URL / SCORE_MINT_POLICY_CONTRACT_ADDRESS / HUB).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/internal/groups":{"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"groupAddress":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"depth":{"type":"integer","minimum":8,"maximum":256},"remoteGroupId":{"type":"string","minLength":1,"maxLength":128}},"required":["groupAddress"]}}}},"responses":{"200":{"description":"Group registered (or already existed)","content":{"application/json":{"schema":{"type":"object","properties":{"groupAddress":{"type":"string"},"depth":{"type":"number"},"id":{"type":"number"},"remoteGroupId":{"type":"string","nullable":true}},"required":["groupAddress","depth","id","remoteGroupId"]}}}},"400":{"description":"Invalid request or address is not a Hub group","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Hub precheck unavailable (RPC unreachable)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/internal/groups/{group}/reset-changes-seq":{"post":{"parameters":[{"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"required":true,"name":"group","in":"path"}],"responses":{"200":{"description":"changes_seq cursor reset to 0; consumer will re-drain all scores from AA on next cycle","content":{"application/json":{"schema":{"type":"object","properties":{"groupAddress":{"type":"string"},"changesSeq":{"type":"number"}},"required":["groupAddress","changesSeq"]}}}},"404":{"description":"Group not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/internal/groups/{group}/score-overrides":{"post":{"parameters":[{"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"required":true,"name":"group","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"score":{"anyOf":[{"type":"string","pattern":"^\\d+$"},{"type":"integer","minimum":0}]}},"required":["address","score"]},"minItems":1,"maxItems":1000}}}},"responses":{"200":{"description":"Manual score overrides applied; evaluator pushes for these users are ignored","content":{"application/json":{"schema":{"type":"object","properties":{"groupAddress":{"type":"string"},"applied":{"type":"number"},"skippedOverrides":{"type":"number"},"root":{"type":"string"}},"required":["groupAddress","applied","root"]}}}}}}},"/internal/groups/{group}/score-overrides/{address}":{"delete":{"parameters":[{"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"required":true,"name":"group","in":"path"},{"schema":{"type":"string","pattern":"^0x[0-9a-fA-F]{40}$"},"required":true,"name":"address","in":"path"}],"responses":{"200":{"description":"Manual score override cleared for one user","content":{"application/json":{"schema":{"type":"object","properties":{"groupAddress":{"type":"string"},"userAddress":{"type":"string"},"cleared":{"type":"boolean"},"scoreStillActive":{"type":"boolean"},"scoreRaw":{"type":"string"}},"required":["groupAddress","userAddress","cleared","scoreStillActive","scoreRaw"]}}}}}}}}}