docs/adr/0056-anonymous-trademark-budget.md
Rama master · réplica de lectura del repositorio original.
# ADR-0056: Anonymous Trademark Budget Isolation
## Metadata
| Field | Value |
|-------|-------|
| **Status** | Accepted |
| **Date** | 2026-08-13 |
| **Authors** | AlessioBrillo |
| **Deciders** | AlessioBrillo |
| **Supersedes** | N/A |
| **Relates to** | ADR-0006, ADR-0030, ADR-0033, ADR-0043, ADR-0041 |
| **Project** | DOMINUS |
## Context
The trademark gate (ADR-0006) is mandatory: every candidate must pass the
USPTO/EUIPO check before any buy recommendation. On the public namespace
(ADR-0030) every anonymous valuation runs the same gate through
`AnonScoringService`, drawing from the **same** per-process USPTO/EUIPO rate
limit buckets as pipeline runs.
Two problems follow from sharing one budget:
1. **Starving the pipeline.** The public namespace is internet-facing and
per-IP rate limited (ADR-0043), but aggregated anonymous traffic is
unbounded. A valuation spike (a post going viral, a scraping wave) can
drain the shared USPTO/EUIPO tokens for minutes at a time, delaying the
trademark stage of pipeline runs — the revenue-critical path. In the
multi-process Cloud deployment (ADR-0033) each process holds its own
in-memory budget, so three processes can *multiply* anonymous demand by
the replica count.
2. **Fail-closed leakage onto the public surface.** The trademark gate fails
closed by design (ADR-0006): its deadline and circuit breakers produce an
`unverified` verdict and strip the buy signal. That is correct for the
pipeline, but on the public namespace an exhausted shared budget converts
a *capacity* problem into degraded *quality* for visitors — without any
signal telling the operator why.
The public namespace needs its own, bounded allowance for trademark checks:
anonymous traffic must be *contained* by design, not by luck, and the
operator must see when the containment engages.
## Decision Drivers
1. **Trademark gate is non-negotiable (ADR-0006)** — every candidate, public
or pipeline, keeps a mandatory trademark verdict; the public surface may
never bypass the gate.
2. **Pipeline integrity** — anonymous traffic must never delay or starve
pipeline trademark checks (shared budget = hidden coupling).
3. **Public UX + cost discipline** — bounded wait, bounded provider spend;
degradation must be graceful, visible to operators, and cheap (community
edition stays at €0).
4. **Zero-cost community parity** — with defaults off, the community edition
behaves byte-identically to today; enabling the budget costs nothing.
5. **Operator control** — the budget parameters (capacity, refill, acquire
deadline) must be configurable and observable via existing telemetry.
## Considered Options
### Option A: Dedicated fail-open budget gate (chosen)
A dedicated token bucket (`anon-trademark` Redis namespace in Cloud, in-memory
otherwise) guards trademark checks initiated by anonymous valuations. The
acquire is bounded by `ANON_TRADEMARK_ACQUIRE_TIMEOUT_MS`; on timeout (or
limiter error) the valuation **fails open** to an `unverified` verdict — the
provider is never touched and the buy signal is stripped (ADR-0006). Feature
disabled by default (`ANON_TRADEMARK_BUDGET_ENABLED=false`).
**Advantages:**
- Anonymous demand is isolated: no path from the public surface to the
pipeline's USPTO/EUIPO tokens, in any process topology.
- Provider cost is hard-bounded per interval (`TOKENS × replicas`); worst
case is 2 USPTO + 2 EUIPO checks per second on defaults.
- Fail-open keeps the public surface responsive — the price is an
`unverified` verdict, exactly the same output the gate already produces on
provider errors, so public semantics are unchanged, just more frequent
under load.
- Distributed namespace in Cloud shares one budget across api/worker/
scheduler replicas (ADR-0041 fair-share pattern), so a fleet cannot
multiply anonymous demand.
- Telemetry: `dominus_anon_trademark_{hits,blocked}_total` makes
containment observable; blocked = visitors got `unverified` solely due to
budget pressure.
**Disadvantages:**
- A new configuration surface (4 env vars) with a new default.
- Under sustained anonymous demand, visitors see `unverified` verdicts that
are capacity artifacts, not trademark reality — the operator must raise
the budget if they want public quality back.
- Fail-open is a deliberate softening of the gate's semantics for this one
surface; it must never extend to the pipeline.
**Cost Implications:** ~1 day (gate + service wiring + telemetry + docs). No
infra or API cost.
**Risk Assessment:** Low — feature is off by default; when on, the pipeline's
fail-closed trademark semantics (ADR-0006) are untouched by construction
(separate budget, separate code path).
---
### Option B: Shared budget with public priority queue
Anonymous valuations wait behind pipeline checks in the shared USPTO/EUIPO
buckets (priority queuing), no separate budget.
**Advantages:**
- Single budget to reason about; no new config.
**Disadvantages:**
- The shared budget is per-process; a scraping wave still stalls pipeline
checks until the wait timeout fires (and then *drains* provider calls on
the way out — worse).
- No cost containment: anonymous traffic can still spend unlimited provider
calls, just at lower priority.
- Queue-full rejections (RateLimiterQueueFullError) on the public surface
surface as hard errors instead of graceful degradation.
- No operator signal distinguishing "gate failing" from "budget pressure".
**Cost Implications:** ~0.5 day. No infra cost.
**Risk Assessment:** Medium-high — introduces a priority mechanism into the
shared limiter used by the pipeline; the isolation the problem actually needs
is not achieved.
---
### Option C: Fail-closed budget (deny → error)
Like Option A but a denied budget returns an HTTP error to the anonymous
caller instead of an `unverified` verdict.
**Advantages:**
- Simplest semantics: no budget, no valuation.
**Disadvantages:**
- Converts capacity pressure into hard failures on the public surface
(5xx storm under load — the failure mode public endpoints must avoid).
- The trademark gate's own behaviour on provider failure is `unverified`,
not error; Option C introduces an inconsistent failure mode for a
capacity problem.
- Worse UX and worse optics than a verdict that explains itself.
**Cost Implications:** ~0.5 day. No infra cost.
**Risk Assessment:** Medium — public availability degrades into error
responses exactly when traffic is highest.
## Decision
**Chosen option: Option A — dedicated fail-open budget gate.**
The public namespace gets its own trademark-check allowance with a bounded
acquire and fail-open semantics. This is the only option that achieves the
three non-negotiables at once: the pipeline's trademark capacity is isolated
by construction (no code path from anonymous traffic to the pipeline's
tokens), anonymous provider spend is hard-bounded, and the public surface
degrades into the same `unverified` verdict the gate already produces on
provider errors — no new failure mode.
The gate wraps a `RateLimiterLike` (in-memory `RateLimiter`, or
`RedisRateLimiter` on the `anon-trademark` namespace in Cloud) and exposes a
single `tryAcquire()` that never throws: grant → run the gate; denial →
`unverified`, provider untouched. Disabled by default so the community
edition ships byte-identical behaviour; operators who want public quality
under load raise `ANON_TRADEMARK_RATE_LIMIT_TOKENS` or disable the budget.
## Consequences
### Positive
- Anonymous valuations can never starve pipeline trademark checks, in any
process topology (shared Cloud namespace via Redis).
- Provider spend from the public surface is bounded by configuration
(2 tokens/s default); a traffic spike costs budget tokens, not money.
- Fail-open matches existing public semantics: an `unverified` verdict with
`suggestedBuyMax` stripped is already the ADR-0006 behaviour on provider
failure — visitors see the same quality degradation, not a new error.
- New Prometheus counters make containment visible: a jump in
`dominus_anon_trademark_blocked_total` is a capacity signal with a config
knob attached.
- ADR-0006 pipeline semantics untouched: the pipeline's trademark checks
keep the shared buckets, fail-closed, as before.
### Negative
- Visitors under sustained anonymous demand may receive `unverified`
verdicts that are capacity artifacts (mitigated by raising the budget).
- 4 new env vars and a new component (`AnonBudgetGate`) to learn.
- Fail-open is surface-specific by design; future maintainers must not
extend it to the pipeline (the code path separation enforces this: the
gate exists only inside `AnonScoringService`).
### Compliance and Security Implications
- None. No new data flows; no new attack surface — the budget adds a rate
limit, it removes none. Public endpoints keep their existing per-IP
limits (ADR-0043).
### Migration and Monitoring Plan
- Defaults off: existing deployments change nothing; enabling is a 1-line
env change (`ANON_TRADEMARK_BUDGET_ENABLED=true`).
- Metrics to watch after enabling: `dominus_anon_trademark_hits_total`
(rate of public gate checks), `dominus_anon_trademark_blocked_total`
(share failing open — keep near 0; raise
`ANON_TRADEMARK_RATE_LIMIT_TOKENS` if it grows), pipeline trademark stage
latency (should be unaffected, by design).
- Rollback: set `ANON_TRADEMARK_BUDGET_ENABLED=false` — restores prior
behaviour instantly.
### Validation
- Unit tests: gate grants/denies/fails open on queue-full, stall and limiter
error; service returns `unverified` without touching the provider on
denial; telemetry callback fires per attempt; config defaults/overrides;
factory builds in-memory/Redis/disabled variants.
- Full backend suite (2241 tests) green with defaults off — community
behaviour byte-identical.
- Timeline: 1 release cycle in Cloud before judging the default budget.
---
*This ADR was created following the MADR 4.0.0 standard. All DOMINUS ADRs should be consistent with the ADR series starting at `docs/adr/0001-project-architecture.md`.*