Loopcard — risk, legal, and ops challenge
Date: 18 September 2026
Role: adversarial risk / legal / ops (not counsel).
Product: Loopcard on https://cryptobook.space (preview). Code: /root/cryptobook-next/loopcard/.
This is not legal advice. Counsel reviews Terms, Privacy, AUP, and any public claim before paid launch or a new domain. Do not invent a US entity in the footer. VPS is in Germany.
Raw inputs: docs/08-legal-posture.md, docs/09-operations.md, challenges/legal.md, challenges/security.md. Product facts checked against loopcard/server.js, loopcard/cli/loopcard.js, loopcard/public/js/app.js, loopcard/lib/parse.js, and nginx cryptobook.conf.
0. What we actually ship (so the challenge is about this, not Slip)
| Piece | Fact |
|---|---|
| Parse | Browser FileReader or local CLI. No multipart upload. Express JSON body 64 kb. |
| Publish | POST /v1/cards stores amount, source (32), loop (280), kind (40), optional note (280), format, row_count, demo. |
| Share | Unauthenticated GET /c/:id + /c/:id.png. public_id = lc_ + 10 hex chars (5 random bytes ≈ 40 bits). |
| PNG | Homepage canvas download is local. Share PNG is server-side PIL spawn to /tmp. |
| Keys | No OAuth. No vendor login. No LLM keys. No Stripe at launch. |
| Process | PM2 name loopcard, one fork, 127.0.0.1:3100. nginx TLS on cryptobook.space. Postgres DB loopcard. |
| Rate | App-wide express-rate-limit 120/min/IP. nginx limit_req 100 r/s (legacy social zone), limit_conn 20. |
If a later feature logs into Anthropic/Cursor, holds API keys, scores people, or claims we cap spend, this document is void — that is a different product and a kill.
1. CFAA — we must not scrape vendor accounts
Kill line. Do not log into Anthropic, Cursor, OpenAI, GitHub, or any third party for the user. Do not scrape password-protected consoles. Do not call vendor APIs from 173.249.19.205 with stored user secrets. CSV/JSONL the user already exported, or labeled demo data. That is the BRIEF constraint and it is still the right one.
Why the Ninth Circuit does not give us a free pass. Amazon.com Services, LLC v. Perplexity AI, Inc., No. 26-1444 (9th Cir. Aug. 4, 2026) (Smith, J.; Tung, J.; Hinderaker, D.J., sitting by designation). Official PDF: cdn.ca9.uscourts.gov/datastore/opinions/2026/08/04/26-1444.pdf. Coverage still circulating mid-September: Cooley, 6 Aug 2026; Jones Day / Mondaq, 16 Sep 2026. Amazon asked the full court to rehear (Law360, 19 Aug 2026). The panel vacated a CFAA/CDAFA preliminary injunction because, on that record, Perplexity’s servers did not talk to Amazon; the user’s browser did. “However advanced the Assistant currently is, it is a tool, not a person for statutory purposes.” “It is the user who ‘accesses’ Amazon’s computers, with the help of the Assistant.”
The opinion says, at unusual length, what it is not doing: no new legal regime for agentic AI; no ruling on tort, ToS, copyright, or DMCA; facts with vendor-to-site TCP, more vendor control, or stored credentials used from the vendor’s network are left open. Distinguished from Facebook v. Power Ventures, 844 F.3d 1058 (9th Cir. 2016), where the defendant’s own systems caused messages on Facebook.
Loopcard today is on the safe side of that line only because we never enter the vendor computer:
- Docs tell the human how to click Export in a console they already use. That is user access, not our access.
- Parser runs in the tab or on
node cli/loopcard.js. CLI may walk~/.claude/projects/**/*.jsonlon disk the user owns, skip files >8 MB, and never sends the file.--publishis the only network call, aggregates only. - We do not wrap Admin API, Cursor usage API, or a “connect Claude” OAuth.
Drift that re-opens CFAA (instant KILL):
- “Paste
sk-ant-…and we pull your invoice.” - Hosted browser / Computer-Use that opens console.anthropic.com from this VPS.
- A cron that scrapes vendor HTML with a session cookie we stored.
- Marketing that we “sync your Anthropic bill.”
ToS, copyright, and tort survived Perplexity. Even a clean CFAA posture does not license ignoring vendor terms if we fetch. User-initiated export is the architecture; keep it.
Residual. A user who exports a coworker’s workspace CSV and publishes it is the actor under Perplexity. We still host the URL. That is §2, not CFAA against us — unless we built the scrape.
2. Publish URLs — other people’s bills, defamation, employment
/c/:id is public, unauthenticated, crawled (OG title is $AMOUNT on SOURCE). Anyone with the link sees amount, caption, source label. The PNG is cacheable 24 h. There is no account, no report button, no takedown mailbox on the page, and legal@cryptobook.space is documented as “after it exists.”
| Abuse | Why it is real on this product | Why it is not an automatic KILL |
|---|---|---|
| Posting a teammate’s bill | Manual fields + CSV drop have no “is this yours?” check. A lead can mugshot an IC’s overnight. | UGC; we did not create the number. Still a hosting/AUP problem. |
| Defamation | Manual amount + loop can name a person or a shop (“Acme burned $80k, fired the intern”). False bill + share URL is a statement we serve. |
We do not assert the number is true; footer says “not an invoice.” That is not immunity. |
| Employment / performance | A public $6,000 card used in a PIP, a hiring packet, or a “who wastes tokens” ranking. Annex III pt 4 and CA ADMT (opt-out 1 Jan 2027) attach to intended purpose. Ours is a screenshot, not a score. If we add leaderboards, badges, or “hire-worthiness,” it flips. |
Do not rank people. ToS: not a consumer report. |
| Doxxing via caption | loop/note are free text, 280 chars, HTML-escaped on the page, not escaped out of existence on OG/Twitter. |
Length cap only. |
Mitigations (keep / cheap).
- Confirm on Publish: “This URL is public. Do not publish someone else’s bill.” (not in UI today — gap).
- AUP: no others’ invoices, no false attribution, no HR scoring. Counsel drafts;
/legalcurrently has posture, not AUP. - Takedown: a mailbox you will actually read (Virtus contact until a Loopcard address exists). Document a 48-hour hide for
public_id. Schema has nohiddencolumn — gap. - Do not add search, sitemaps of
/c/, or a public gallery.robots.txtshould not invite a bill index. - Do not add “team spend leaderboard.” That is employment-adjacent on purpose.
Won’t fix (accepted). We will not KYC publishers. We will not watermark every PNG with a legal name. 40-bit IDs are unguessable enough for v1 obscurity, not a capability. Guessing is ~1e12; brute force of /c/ will 404, but a leaked URL is forever until we hide it.
3. Demo $6k — misrepresentation if unlabeled
The growth unit is a PNG of ~$6,000 / $1,263 / $480. Those are labeled re-creations of published 2026 reporting (Claude Code overnight; public Cursor-month; 30-team median). They are not access to anyone’s account and not our invoice.
Where the label actually is:
| Surface | Labeled? |
|---|---|
| Homepage trust line | Yes: “Demos are labeled re-creations…” |
| On-page note / Origin | Yes: Demo + “Published 2026 reporting” |
/legal, /docs |
Yes |
Share page {{NOTE}} if demo=true was published |
Yes, if they did not overwrite the note |
Downloaded PNG (drawPng in public/js/app.js) |
No. Amount, source, loop, kind, “virtus labs.” No “DEMO,” no “re-creation.” |
Server PNG (lib/render_card.py) |
No. Same fields. demo is in Postgres and never drawn. |
Homepage og:description |
“Someone’s Claude Code ran overnight. About $6,000. Here’s the card. Make yours.” — story-shaped, not a disclaimer. |
FTC § 5 cares about net impression, not the footer. An unlabeled $6,000 CLAUDE CODE card on X reads as a live invoice. If we, or a contractor, post that PNG without “re-creation / not a private invoice,” that is our misrepresentation. If a user republishes the demo as “my bill,” that is theirs — we still supplied the default.
Mitigations.
- Draw
DEMO · re-creation, not an invoiceon canvas and PIL whendemo=true. Cheap. Do it before any paid ads. - Copy/share text already mentions the site; keep “labeled public story” in the caption helper.
- Never claim the $6k run happened on Loopcard or to Virtus.
- Do not use fake testimonials or “used by {logo}.” (
docs/marketing/claims-we-will-not-make.md).
Won’t fix. We will not watermark user-export PNGs with “estimate” unless the parser path is JSONL list-rate (those totals are estimates: Sonnet $2/$10, Opus $5/$25, Haiku $1/$5 per MTok, Sep 2026). JSONL estimate vs vendor invoice mismatch is a support and FTC-substantiation issue if we call it “your bill” without “estimated.” Docs already say estimates. PNG still shows a hard dollar. Residual: keep the word estimated in JSONL loop headlines (parser already does).
4. GDPR / EU, 18+, US
Intended picture (docs/08-legal-posture.md): US-facing, 18+, honor-system “not from EU/EEA/UK” in v1, geo-block later if still required. No children’s product. No Annex III purpose (not hiring, credit, education admissions, health, LE, justice).
What is live: a trust line “US-oriented · 18+ · not legal advice.” No checkbox. No age gate. No geo-block. nginx does not distinguish Union IPs. The box is a German Contabo VPS (vmi3003269, 173.249.19.205). Processing in the EU of EU visitors’ IPs is a GDPR facts pattern even if copy says US-oriented.
| Data | Where | GDPR-ish? |
|---|---|---|
| nginx IP / UA | /var/log/nginx/cryptobook.access.log |
Yes, personal data if it identifies a person. Ordinary logs. No documented retention. |
| Theme | localStorage key cryptobook:themeMode |
Not a cookie; still disclose. |
| Published card | Postgres cards — amount + caption, public URL |
If the caption names a person, or the amount is reasonably identifiable with a handle, that is personal data we host and send to crawlers. |
| File bytes | Not on server (confirmed) | N/A for GDPR storage; local parse is the user’s device. |
| Accounts / email | None in v1 | Good. Do not add EU accounts without a real privacy program. |
EU AI Act. Intended purpose is a shareable bill PNG, not Annex III. Article 50 transparency (in force 2 Aug 2026) is about AI that interacts with people or marks synthetic media. Loopcard is not a chatbot; the demo is a static re-creation. Do not CE-mark, do not sell “AI Act compliant.” High-risk Annex III standalone duties deferred toward Dec 2027 by the 2026 Digital Omnibus — not a free pass to build employment scoring.
Children. COPPA Rule amendments: compliance deadline 22 Apr 2026. California 2026 kids/chatbot bills (AB 2246, SB 1119, etc.). A public playground with no age gate is the trap. Loopcard is not child-directed if we do not market to schools. Honor-system 18+ is weak. Do not add classroom SKUs.
US state privacy / ADMT. We do not make “significant decisions” about people. Users might. ToS prohibition + no scores. CCPA “business” thresholds are operational if we grow; no sale of personal information.
Mitigations.
- Honor-system checkbox before Publish (18+, not EU/EEA/UK, this bill is mine) — still missing.
- Privacy paragraph on
/legalalready describes FileReader vs Publish. Add log retention (e.g. nginx 14 days) when ops can actually rotate. - Geo-block at nginx if counsel says the German box + no DPA is worse than losing EU traffic. v1 documents this as forthcoming, not done.
- Contact path: virtusblockchainlabs.com until a mailbox exists. Do not print
root@.
Won’t fix. No verifiable age. No Schrems paper. No DPO. No DSAR portal. No cookie banner for theme localStorage. We will not pretend GDPR “compliant.” US-oriented is a market posture, not a legal opinion that EU law cannot apply.
5. Credential / CSV secrets — we do not upload files (residual)
Confirm: the file is not posted.
- Studio:
FileReader.readAsText;POST /v1/cardsJSON is amount/source/loop/kind/note/format/rows/demo (public/js/app.js). - CLI: reads disk; prints “Nothing was uploaded.
--publishsends amounts only.” (cli/loopcard.js). - Server:
express.json({ limit: "64kb" }). Nomultipart, no disk write of uploads. Schema has no file column. /legal: “No file bytes. No prompts. No API keys. No passwords.”
Residual risk if someone hits Publish “wrongly”:
- Secrets in the fields we do store.
sourceis sliced from a CSV model/product column (24 chars in parser, 32 on the server). If that column is actually a key, token, email, or internal project name, Publish puts it on a public URL and in a PNG. Parser does not stripsk-,sk-ant-,Bearer,ghp_. - Manual caption. User can type a password or a customer name into
loop/note. Publish sends it. That is user error; we still host it. - CLI walk of
~/.claude/projects. JSONL often contains prompt bodies. The parser only sumsusagetokens and builds a headline from totals — it does not currently copy prompt text intoloop. A parser bug, a “helpful” dump ofmessage.content, or a future--upload-bodieswould become a prompt lake. Security challenge: do not implement--upload-bodiesin v1. - Wrong file, right click. User drops
.envor a key CSV. Parse may fail (good) or may sum a numeric column and publish a nonsense amount with a leakedsource. No client-side secret scan. - nginx
client_max_body_size 100Mis leftover from social video. App JSON is 64 kb, so a 100 MB POST dies in Node, not nginx. Residual: a mistaken proxy or a future upload route would inherit 100 MB. Lower it for/v1/when ops touches the vhost. - Debug logs.
console.error(e)on save/render. Do not logreq.bodyin full. Architecture (refuse file bytes) is the control; logging config is not.
Mitigations.
- Keep refusing file bytes. Never add
FormDatafor “better parse on the server.” - Client + server reject
source/loop/notematchingsk-,sk-ant-,ghp_,github_pat_,Bearer,AKIA. Cheap regex. Not a vault product. - CLI: require a path arg before walking
~/.claude(already the documented rule: do not walk huge JSONL without a path). Publish should be opt-in twice if the file looks like a transcript. - Docs: “Publish does not upload the file. It does publish the caption. Do not put keys in the source box.”
Won’t fix. We will not run a DLP product on 280-character captions. We will not hold keys “encrypted at rest” — that was the security kill for the gateway. Local JSONL with prompts stays on the user’s disk; we cannot un-read it.
6. Brand — CryptoBook leftover, expired-cert history
| Fact | Risk |
|---|---|
Preview host is cryptobook.space |
Crypto-social leftover. ~65 unique IPs and ~1,500 nginx hits in 7 days to 18 Sep 2026, mostly old social APIs. Conversion tax, not a PLG channel. H1 is Loopcard (good). Do not put CryptoBook in the H1. |
cryptobook.com A = 217.160.223.94 (IONOS Apache) |
Not us. Advertising .com from this VPS is false until DNS changes. |
| TLS outage | Let’s Encrypt for cryptobook.space expired 2026-04-11. Not an app outage; certbot/webroot. Renewed 18 Sep 2026; leaf until 2026-12-17. Academy had the same class of failure (expired 2026-04-19). Catch-all location ~ /\. without ACME exclude will break HTTP-01 again. |
Theme key cryptobook:themeMode |
Harmless leftover; still says CryptoBook in DevTools. |
| CSP on the vhost | Still allows Google/Facebook script origins from the social app. Unused by Loopcard. Residual XSS surface + mixed-product story. |
| Footer / legal | Virtus Blockchain Labs. Do not invent Delaware. |
Mitigations. Buy loopcard.ai / .app / getloopcard.com and move the A record. Keep ACME location ^~ /.well-known/acme-challenge/ on port 80 before return 301, and on 443 before hidden-file deny. certbot renew --no-random-sleep-on-renew for interactive work. Calendar the 17 Dec 2026 expiry. Strip social CSP when ops can do it without breaking a leftover /rtc if LiveKit stays.
Won’t fix. We will not litigate the word “CryptoBook” off old indexes this month. Expired-cert history is already in the public Web. A new name is the fix; a blog post is not.
7. Ops — VPS, nginx, PM2, single process
Box. vmi3003269, Ubuntu 24.04, 173.249.19.205, Germany. Apps historically run as root via PM2. SSH: PermitRootLogin yes, password auth, empty authorized_keys, no fail2ban (operator policy: do not lock out root-password sessions in this ticket). Postgres/Redis/Mongo localhost. No HSM, no tenant KMS, no SOC 2. Assume attacker gets root or the Node app (challenges/security.md).
Loopcard process. ecosystem.config.cjs: instances: 1, exec_mode: fork, bind 127.0.0.1:3100. One Node event loop. PNG render spawns python3 lib/render_card.py per /c/:id.png, writes /tmp/lc_*.png, unlinks on stream close. A hung Python or a /tmp fill is an outage for all cards, healthz, and static-via-Node.
nginx. Public 80/443 only. /v1/ and /c/ and /healthz proxy to loopcard. Static root /var/www/loopcard. Old social still in the same vhost (cryptobook_backend :3000, LiveKit /rtc /twirp). TURN 3478 and RTC UDP 50000–60000 remain open for the retired video product. Do not open 3100 on WAN.
Single-process residuals (won’t fully fix on this VPS):
- Deploy/restart = brief 502 on
/v1and/c. - One leak / one tight loop / one unbounded
renderPngqueue takes the product down. - Same box as TokensMarketCap, Virtus, LiveKit. A Loopcard PNG flood is a neighbor-noise problem, not an isolated SaaS.
- Disk is plenty (~6% on 18 Sep 2026);
/tmpPNGs plus a spawn bomb are the real resource.
Mitigations. Keep bind localhost. Keep JSON 64 kb. Cap concurrent PIL spawns (not implemented — gap). PM2 max_restarts. Do not put LLM keys on this machine. Do not reuse LiveKit for Loopcard. When touching nginx: never deny ACME; lower client_max_body_size on /v1/. Health: curl -sS https://cryptobook.space/healthz.
Won’t fix. No second region. No SOC 2. No non-root PM2 in this document’s scope. No “100% secure.” SSH/UFW hardening is a separate operator ticket (keys, then disable passwords, fail2ban). This product must stay boring enough that a VPS breach is a bad week (public amounts + IPs), not “we drained 4,000 Anthropic keys.”
8. Abuse of POST /v1/cards (spam PNGs)
Unauthenticated publish is the growth mechanic. It is also a free image CDN and a graffiti wall.
| Vector | Mechanics | Harm |
|---|---|---|
| Card spam | 120 JSON POSTs/min/IP → 120 public URLs | Noise, defamation mill, SEO junk on /c/lc_* |
| PNG DoS | Each GET /c/:id.png spawns Python + PIL 1080² |
CPU, /tmp, PM2 stall. nginx conn limit 20 helps; Express limiter is shared with POST (120/min) and is not spawn-aware. |
| Cache | Cache-Control: public, max-age=86400 on PNG |
Good for us; also good for a viral defamatory card. |
| Huge amounts | Server allows 0 … 1e9 |
$1,000,000,000 meme cards. Harmless except brand. |
| Bot flood | No captcha, no API key | Scriptable. Social crawlers are allowed in $bad_bot (needed for OG). |
Mitigations.
- Keep 120/min; add a stricter limit on
POST /v1/cards(e.g. 10/min) and onGET /c/:id.png(e.g. 30/min) — gap. - Cap in-flight
renderPng(semaphore 2). Reuse files bypublic_idinstead of re-rendering every miss after unlink — gap. - Drop cards with
demo=truefrom any future sitemap. Do not index/c/. - Hide/delete path for abuse (see §2).
- Amount display is already rounded; still allow large numbers (won’t KYC spend).
Won’t fix. No Cloudflare WAF in this stack. No account-to-publish requirement at $0 launch (that would kill the 60-second path). Captcha can wait until we see actual flood. 40-bit IDs stay.
9. Insurance — we are not a cost-cap; users may think we are
Homepage “What this is not”: Not a cost cap. “Your vendor still stops the runaway. We show the tab.” Docs/FAQ: Cursor spend limits and Claude usage caps stop the loop; we will not claim we stop it. Community playbook already has the objection reply.
Why users will still get it wrong.
- The H1 is “The screenshot after a runaway agent.” Job-to-be-done reads as control, not mugshot.
- A shareable
$6,000card feels like documentation for insurance, expense reimbursement, or “I told my boss.” It is a PNG. It is not a policy, not a SOC 2, not a fiduciary cap, not proof the vendor will refund. - JSONL totals are list-rate estimates. Someone will file an expense against our number. We must not imply invoice-grade accuracy.
- Future Pro “90-day history” could be mistaken for an audit log. It is not.
Mitigations. Keep the “not a cost cap” tile. /legal: not an invoice, not insurance, not a spend limiter. Support macro: “Set the cap in Cursor/Claude. Then screenshot.” Never sell “we would have saved you $6k.” Never add a kill-switch we do not have.
Won’t fix. We cannot stop a buyer from believing a pink card is coverage. We will not buy E&O / cyber that treats us as a cost-control product — we are not one. If copy drifts to “stop the loop,” that is an FTC problem and a product lie.
10. Scoreboard
| Topic | Verdict | One-line |
|---|---|---|
| CFAA / vendor scrape | SURVIVE if we never log in | Perplexity is user-not-developer on user-browser facts. Hosted scrape = KILL |
| Publish URLs / defamation / others’ bills | FIX | Public UGC; need confirm + hide; no people-ranking |
| Demo $6k unlabeled PNG | FIX | Page is labeled; PNG is not. Draw DEMO on demo renders |
| GDPR / EU / 18+ / US | FIX (operational) | Honor-system only; German VPS; no geo-block. Not Annex III by purpose |
| File upload / secrets | SURVIVE with residual | Files not uploaded (confirmed). Caption/source can still leak |
| Brand / expired cert | FIX | New domain; ACME hygiene; Dec 2026 expiry |
| Ops single process | Accept | One PM2 fork on a shared VPS. Isolation is “don’t hold keys” |
/v1/cards spam PNGs |
FIX | Tighter POST/PNG limits + spawn cap |
| Insurance / cost-cap confusion | FIX copy | We show the tab. Vendors cap. Not coverage |
Kill if we add: vendor OAuth or Admin API; key custody; prepaid LLM credits; tokens; employment/credit scores; kids/classroom; “SOC 2 / AI Act / we cap your spend / we are your insurance”; server-side file ingest.
11. Mitigations vs residual “won’t fix”
Do (cheap, product law)
- Never scrape or OAuth into vendor billing. Export-in-browser only.
- Stamp DEMO · re-creation on demo PNGs (canvas + PIL).
- Publish confirm: public URL, my bill, 18+, not EU (honor-system).
- Secret regex on
source/loop/note; keep 64 kb JSON; no multipart. - Stricter rate limit on
POST /v1/cardsand PNG render; cap concurrent Python. hidden_at(or delete) for takedown; mailbox you read.- Keep “not a cost cap / not an invoice / not legal advice / not a certification” on
/,/legal,/docs. - ACME exclude + calendar cert expiry; do not advertise
cryptobook.com. - JSONL headlines stay estimated. Do not call them invoices.
- No gallery, no people leaderboard, no
--upload-bodies.
Won’t fix (accepted residual)
- German VPS, no geo-block, no age proof, no DPO, no DSAR portal.
- Unauthenticated publish (virality).
- 40-bit public IDs; leaked URL is public until hidden.
- Single PM2 process, apps as root, SSH posture unchanged, shared box with TMC/LiveKit.
- No SOC 2, no HSM, no second region, no WAF.
- CryptoBook DNS/history until a new domain is bought and indexed.
- Caption DLP beyond cheap key prefixes.
- Users who treat a PNG as insurance, as an expense source of truth, or as HR evidence.
- User-side CFAA if they scrape a vendor; we are not their counsel.
- Perplexity rehearing / other circuits; ToS/copyright/tort still exist.
12. Counsel checklist (not done)
- [ ] Entity and governing law (do not print fake Delaware)
- [ ] Terms, Privacy, AUP (others’ bills, no scrape, no HR scoring, not an invoice)
- [ ] Whether EU geo-block is required given DE hosting
- [ ] Takedown / abuse mailbox
- [ ] Demo-PNG labeling before paid distribution
- [ ] Cookie/theme storage disclosure
- [ ] Stripe later: SaaS fee only, never stored value
Nothing here is a statement that Loopcard is “compliant.” It is a challenger’s map of landmines, the patches that keep the current architecture off the kill list, and the residuals we are choosing not to close on a $0, 1-VPS preview.
Not legal advice. Sources fetched or re-checked 18 September 2026: 9th Cir. 26-1444 (4 Aug 2026); Cooley 6 Aug 2026; Jones Day/Mondaq 16 Sep 2026; product and nginx as deployed that day.