From d141c29ceda6ef172fed5f23685ce3fd723c05e5 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Mon, 5 Oct 2026 14:42:37 +0800 Subject: [PATCH] docs(update): cut the upstream request down to the ask The first draft read as a design document for the CivitAI team: it explained how to change toPublicPaidAccessDto, cited discountedTerms, and described how we read prices from model pages today. Two of those do not belong in an issue. - no implementation guidance: they know their service, and withholding prices may be a deliberate product decision (the code comment says pricing belongs to the purchase flow), so the request has to argue the need rather than the diff - no description of our current page reading: it is our approach, it shifts the thread from the feature to our behaviour, and it invites an objection that has nothing to do with the ask The filed text is now 138 words: ask, why (the API gives the gate and the early access end date but not the price, which is the other half of "wait or pay now"), one credibility clause naming the integration, and one scope concession (public or authenticated-only). The evidence table and the write/read asymmetry stay in the plan as internal notes, explicitly marked as not for the issue. --- docs/plans/paid-model-price-tracking.md | 143 ++++++++++-------------- 1 file changed, 56 insertions(+), 87 deletions(-) diff --git a/docs/plans/paid-model-price-tracking.md b/docs/plans/paid-model-price-tracking.md index c44e9960..b1302095 100644 --- a/docs/plans/paid-model-price-tracking.md +++ b/docs/plans/paid-model-price-tracking.md @@ -840,103 +840,72 @@ where every row has exactly one subject. The only model-level fact worth conside opening the model; it is rare in practice (4 unexpired early access versions across 3 models in that library), so it is optional polish rather than part of the correction. -## 13. Upstream API request (draft) +## 13. Upstream API request -Target: a GitHub issue on `civitai/civitai`, titled with that repo's convention -(`[API Feature Request] ...`). No existing issue asks for this (searched `paidAccess`, "download -price", "buzz price API", and every open `[API Feature Request]`). +### 13.1 The request as filed (send this) -The ask is small on the CivitAI side, and that is the strongest part of the argument: the row the -public DTO is built from already carries the prices, and the sale resolution already exists. - -* `toPublicPaidAccessDto(row)` (`src/server/services/paid-access.service.ts:649`) receives a - `PaidAccessRow` that already has `terms: PaidAccessTerms` and `sales?: ModelVersionSaleWindow[]` - (`packages/civitai-buzz/src/paid-access.ts:132`), and returns only - `{ permanent, endsAt }` (`PublicPaidAccessDto`, line 643). -* `discountedTerms(terms, sales, now)` (line 426) already resolves what a buyer pays during a sale. -* The write path already accepts prices: `/api/v1/model-versions/early-access` validates - `updateModelVersionPaidAccessSchema`, whose `paidAccess.terms` is `modelVersionTermsSchema` - (`{ download, generation, acceptsBlueBuzz }`). - -So the reads withhold exactly what the writes accept, from a row that already has it. - ---- - -**Title:** `[API Feature Request] Expose the Buzz download price of paid / early-access versions` +**Title:** `[API Feature Request] Expose Buzz prices for paid / early-access model versions` **Body:** -### Ask +> **Ask:** include the Buzz price in the public `paidAccess` DTO - the download price, whether a +> sale is currently discounting it, and whether Blue Buzz is accepted. +> +> **Why:** the API already tells a client *that* a version is gated and *when* early access ends +> (`paidAccess: { permanent, endsAt }`), but not what it costs. In the Buzz economy that is the +> other half of the question users are asking: wait for it to become free, or pay now? A live sale +> or Blue Buzz support is often what decides it. +> +> With the price exposed, an integration like ComfyUI LoRA Manager can show "100 Buzz - free on +> Oct 11" on a version and tell users when a paid version becomes free, instead of sending them to +> the website to check. +> +> Public or authenticated-only - whichever you prefer. -Please include the gate's **terms** (the Buzz prices) in the public `paidAccess` DTO, so a client can -show what a paid or early-access version costs — not just that it is gated. +Filing notes: a new issue on `civitai/civitai`, using that repo's `[API Feature Request]` title +convention. No existing issue asks for this (searched `paidAccess`, "download price", +"buzz price API", and every open `[API Feature Request]`). -### What the API returns today +### 13.2 Internal notes (do NOT paste into the issue) -`GET /api/v1/models?ids=646411` → version `1249246`: +Kept here because it is what makes the request cheap to grant - but telling a maintainer how to +implement their own service reads as presumptuous, and volunteering how we read prices today is +our business, not theirs. Both were in the first draft and were cut. -```json -"availability": "Public", -"paidAccess": { "permanent": true, "endsAt": null } -``` +* `toPublicPaidAccessDto` (`src/server/services/paid-access.service.ts:649`) already receives a + `PaidAccessRow` carrying `terms: PaidAccessTerms` and `sales?: ModelVersionSaleWindow[]` + (`packages/civitai-buzz/src/paid-access.ts:132`) and returns only `{ permanent, endsAt }`. +* `discountedTerms(terms, sales, now)` (line 426) already resolves what a buyer pays during a sale. +* The write path already accepts the same terms: `/api/v1/model-versions/early-access` validates + `updateModelVersionPaidAccessSchema`, whose `paidAccess.terms` is `modelVersionTermsSchema`. +* So the reads withhold exactly what the writes accept, from a row that already holds it. -`GET /api/v1/model-versions/1249246` → the same `paidAccess`, plus `usageControl: "Download"` and -`licensingFee: null`. So a client can tell *that* a version is paywalled and *when* an early-access -window ends (`paidAccess.endsAt`), but never *how much* it costs. +Evidence gathered for our own records (also not for the issue): -For the same version, the model page's own payload carries: +| Check | Result | +| --- | --- | +| `GET /api/v1/models?ids=646411` | version `1249246` -> `paidAccess: {permanent: true, endsAt: null}`, no price | +| `GET /api/v1/model-versions/1249246` | same, plus `usageControl: "Download"`, `licensingFee: null` | +| Same version on the model page | `downloadPrice 100`, `generationPrice 50`, `acceptsBlueBuzz false` | +| `civitai.red` model pages | Cloudflare challenge (403 for any User-Agent; aiohttp and httpx alike) | +| `civitai.com` / `civitai.green`, mature models | 404 to anonymous visitors, so no readable price source | -```json -{ "downloadPrice": 100, "generationPrice": 50, "acceptsBlueBuzz": false, "sale": null } -``` +### 13.3 Why the request is written that way -### Why reading the page is not a substitute - -We currently parse that payload from the model page, anonymously. It does not work reliably: - -* `civitai.red` model pages answer non-browser HTTP clients with a Cloudflare challenge - (HTTP 403, "Just a moment...", for any User-Agent — tested with `aiohttp` and `httpx`). -* `civitai.com` and `civitai.green` return 404 for mature models to anonymous visitors. -* Therefore for mature models there is **no readable price source at all** for a third-party tool — - and those are the models where creators monetize most. - -### The change looks small on your side - -`toPublicPaidAccessDto` already receives a row that carries the terms: - -```ts -export type PaidAccessRow = { …, terms: PaidAccessTerms, sales?: ModelVersionSaleWindow[] }; -export type PublicPaidAccessDto = { permanent: boolean; endsAt: Date | null }; -export function toPublicPaidAccessDto(row) { - if (!row || !isPaidAccessActive(row)) return null; - return { permanent: row.timeframeDays == null, endsAt: row.endsAt }; // terms dropped here -} -``` - -`discountedTerms(terms, sales, now)` already computes what a buyer pays while a sale is live, and the -write path already accepts the same terms via `updateModelVersionPaidAccessSchema`. A field such as: - -```json -"paidAccess": { - "permanent": true, "endsAt": null, - "terms": { "download": { "price": 100 }, "generation": { "price": 50 }, "acceptsBlueBuzz": false }, - "sale": { "downloadPrice": 80, "endsAt": "2026-10-20T00:00:00.000Z" } -} -``` - -on `GET /api/v1/models` and `GET /api/v1/model-versions/{id}` would be enough. If exposing prices to -anonymous callers is undesirable, returning them only for authenticated requests -(`Authorization: Bearer `) is fine too — it would also cover mature models, which is where -the gap hurts. - -To be explicit: this is **read-only display pricing**. Nothing here touches the purchase flow, which -stays on the site; we only link to it. - -### Why it matters - -ComfyUI LoRA Manager is a local model manager with a Civitai integration. Its users' question in the -Buzz economy is *"do I wait for this to become free, or pay now?"* Today the tool can answer the -first half (early-access end dates come from `paidAccess.endsAt`, which works and is appreciated) but -not the second: the price, whether a sale is live, and whether Blue Buzz is accepted are exactly what -change that decision. Without a supported field, every third-party tool either scrapes an -unreliable page or gives up on mature models. +* **Lead with the ask and the need; stop.** A maintainer triages on the first lines; the rest is + only read if the ask survives. +* **No implementation guidance.** They know their service. A "here is your one-line change" + section invites either irritation or a correction, and it can be wrong: withholding prices may be + a deliberate product decision (the code comment says pricing belongs to the purchase flow), so the + request has to argue for the *need*, not the diff. +* **No mention of how we read prices today.** It is an internal approach, it shifts the thread from + the feature to our behaviour, and it invites a "please don't do that" that has nothing to do with + the ask. +* **One scope concession, one clause** ("public or authenticated-only"): it removes the most likely + objection without prescribing anything. +* **One precedent clause** ("you already expose whether a version is gated and when early access + ends"): it makes the price look like the natural companion of what is already public. +* **One credibility clause** (naming ComfyUI LoRA Manager as a real integration): it separates the + request from a one-off user wish. +* Target length: ~130 words. Longer drafts read as a specification, which is the failure mode this + section exists to prevent.