Marketing-copy generation for financial services, gated by a compliance linter the model can't talk its way past.
Every firm using AI for marketing copy hits the same wall: the model writes well, but "sounds great" and "can ship" are different standards. In financial services the gap is regulatory — guarantee language, promissory performance claims, and missing disclaimers aren't style problems, they're FINRA 2210 problems.
The common fix is to ask the model to check its own output. That's the wrong tool: a model reviewing a model gives you two opinions, not a gate. copy-check splits the job the way a real marketing team does:
- The model writes. From a structured brief, told the rules up front.
- A deterministic linter reviews. A declarative rule pack — banned-term rules and conditional-disclosure rules — enforced with regex, not judgment. Same copy in, same report out, every time.
- Violations loop back. When a draft fails, the model gets the specific violations (rule, snippet, requirement) and redrafts. Most copy passes in zero or one revision. Copy that still fails after the loop is returned marked failed — it does not ship quietly.
The rule pack and the generator's instructions render from the same source, so what the model is told to follow and what the gate enforces can never drift apart. And the compliance officer's control surface is a readable Python file of rules — not a prompt.
Real output from python -m copycheck demo, linting the included examples:
── copy-violating.md
✗ [specific-return-projection] (line 6) — "averaged 14%"
✗ [promissory-performance] (line 7) — "will grow"
✗ [guarantee-language] (line 8) — "guaranteed"
✗ [past-performance-disclaimer] (line 8) — "Returns"
Copy mentions performance/returns but carries no past-performance disclaimer.
⚠ [risk-balance] (line 1) — "Grow Your"
⚠ [unsubstantiated-superlative] (line 3) — "best"
⚠ [testimonial-flag] (line 9) — "our clients love"
4 blocking, 3 warning(s).
── copy-clean.md
No violations. Copy passes the rule pack.
Most of marketing-compliance review reduces to two patterns, and the rule pack (rules.py) has one type for each:
| Shape | Example |
|---|---|
| TermRule — this language may not appear | "guaranteed", "risk-free", "will outperform", "earn 12%" |
| DisclosureRule — if trigger appears, required language must too | mention returns → carry "past performance does not guarantee future results" |
Severity matters too: BLOCK stops shipping; WARN (superlatives, testimonials) routes to human review. The exit code reflects it, so the linter drops into CI or an approval workflow as-is.
My favorite bug this design surfaces: the standard disclaimer itself contains the word "guarantee." A naive banned-words list flags the disclaimer as a violation. The rule pack handles negated uses ("does not guarantee") — and there's a test pinning that behavior, because that's the kind of false positive that makes marketing teams turn the checker off.
Requires Python 3.10+. The linter and demo run offline; write needs an
Anthropic API key (ANTHROPIC_API_KEY, or a profile from ant auth login).
git clone https://github.com/spencerxsmith/copy-check
cd copy-check
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
python -m copycheck demo # offline
python -m copycheck lint your-draft.md # offline, exit 2 on blockers
python -m copycheck write examples/brief-retirement-webinar.json # generate → lint → revise
pytest # 13 tests, all offline- The rule pack is illustrative, not a 2210 implementation. It covers the themes (promissory language, projections, disclosures, balance), but a real deployment starts with your compliance team writing the rules that match your registrations and your regulator. This repo is the harness, not the law. Nothing here is legal advice.
- Regex has edges. "Fees as low as 1%" won't trip the projection rule; a creative enough promise can dodge a term list. Deterministic rules catch the categorical failures cheaply and consistently — pair them with human review for the judgment calls, in that order.
- Balance is a heuristic. The risk-balance rule checks that risk is mentioned, not that the copy is genuinely fair. That final read is a person's job; the linter's job is making sure the obvious failures never reach that person.
This is a sibling of cite-check (RAG with machine-verifiable citations), and the two share one design rule: never let the model grade its own homework. Generation is the model's job; verification belongs to deterministic code that a domain owner — a compliance officer, a supervising attorney — can read and change.
The pattern comes from a marketing-collateral builder I built for a global accounting-software company's partner-marketing teams. That system is private because the work belongs to the client; the generate-lint-revise loop is the part worth showing, rebuilt here with a fictional firm and synthetic examples.
Built by Spencer X Smith - I build AI systems for financial services and legal firms. More at spencerXsmith.com.