Skip to main content
Any endpoint that accepts user-authored public content - posts, status updates, comments, replies, reviews - is a target for LLM-powered content generation. The right integration runs at the content-submit handler, blocks bot sessions, and uses the visitor fingerprint to cap posting velocity across account rotations.

The threat

Public content surfaces have two automation shapes stacked on top of each other:
  • Scripted posting - headless browsers or direct API calls submitting content on a schedule. Headless browsers get the automation attribution (Playwright, Puppeteer, Selenium). Direct API calls skip your page, so they carry no Foil token and your handler rejects them.
  • LLM-written content - the script is still automated, but now the content itself is generated by a language model in the same pipeline. The browser looks real because a real (headless) Chromium is driving it; the text looks real because it came from a capable LLM. Foil classifies the session by how the browser is driven, not by who wrote the text, so a script that posts LLM-written text is automation. The ai-agent category applies to agents that operate a browser themselves.
The second one is where policy has to be deliberate. Automation is unambiguously bad on a posting surface - no real human posts through Puppeteer. AI-generated content is blurrier: a human writing a post with LLM assistance is probably fine, an LLM agent posting hundreds of posts under one account is not. Foil doesn’t read the text. It sees whether a script, an agent, or a human drove the submission, regardless of who wrote the words. Three tactics dominate abuse on these endpoints:
  • Spam - outbound links, crypto shilling, promotion of other accounts or products.
  • Astroturfing - coordinated posting to manufacture consensus or suppress criticism.
  • Engagement farming - AI-generated posts designed to get reactions, build account reputation, and later pivot to spam or resale.
All three share the same detection target: a submission that wasn’t driven by a human at a keyboard.

The flow

1

Start Foil on the composer surface

Wherever the user actually drafts content - the compose modal, the reply box, the review form.
2

Call getSession() at the submit click

This captures keystroke timing, paste patterns, and the full fingerprint in the sealed handoff.
3

Verify and inspect the actor label

Check decision.verdict and the actor label in attribution.bot.labels (the label whose kind is actor). You’ll treat automation and ai-agent differently from human, and you may want to allow verified agents and crawlers through read endpoints (see API abuse).
4

Apply a visitor-fingerprint velocity cap

Even a “human” verdict shouldn’t let one fingerprint publish 200 posts per hour. Rate-limit by visitor_fingerprint.id, not just account ID.
5

Consider shadow mode

For surfaces where silent rejection is unacceptable, score content but don’t block - feed the verdict into trust-and-safety tooling instead.

Client integration

If the script fails to load or getSession() fails, the page sends the post without a handoff, so a blocked script doesn’t leave the composer unresponsive. The server rejects a post without a valid token, as the next section shows. Keystroke and paste events are part of Foil’s behavioral signal set, and they fire strongest when the user is actually composing in your page. Mount the client on the composer, not just the top-level app shell.

Server verification

Decisioning policy by actor label

The top-level verdict tells you whether to block; the actor label in attribution.bot.labels tells you why and helps you build useful signal for trust and safety teams. A session can have no actor label, so treat a missing label as unknown. Persist the actor label alongside the content row (the examples store it as foilActor or foil_actor). A post that was created with a human verdict but whose decision.manipulation.verdict is high is a useful thing to surface to moderators without blocking the user outright. decision.manipulation is null when Foil has no manipulation assessment for the session, so check for that before you read verdict. The server SDKs don’t check a token’s age. Reject a token whose decision.evaluated_at is more than a few minutes old, so that an attacker can’t replay a token captured from one session. See Verify the token on your server for the check in each language.

Velocity caps that survive account rotation

Attackers rotate accounts - making a hundred accounts and posting from each one is cheaper than making one account and posting a hundred times. A per-account rate limit catches the second pattern and misses the first. Per-fingerprint caps catch both: the visitor_fingerprint.id persists across account creation on the same device, so a fingerprint that signed up three times in six hours is already suspicious by the time it tries to post.
Node.js
Pair with a per-account cap. The two limits solve different problems: per-account stops one user flooding your feed; per-fingerprint stops one attacker flooding it from many accounts.
The server SDKs type visitor_fingerprint as nullable, so the examples check for a missing ID. Foil resolves the visitor fingerprint before it issues a token, so a token from the browser SDK carries one. If your code does see a missing ID, fall back to IP-based limiting.

Shadow mode

Not every surface can silently reject content. A comments section on a news site where users expect their comment to appear might reasonably want to ship the post even on a bot verdict - but route it to a moderation queue, not to the public feed. Or score against a shadow threshold for 30 days before flipping to enforcement. Two patterns worth keeping separate:
  • Shadow scoring - verify the token, persist the verdict alongside the post, publish the post anyway. Used to baseline verdict distribution before you turn enforcement on. See Going to production.
  • Shadow ban - accept the post, publish it to the author only, suppress it from other feeds. Useful against low-grade spam where you want to waste the spammer’s time rather than tip them off that detection fired.
Whichever you pick, don’t mix them accidentally. A post that’s “shadow scored” should still appear publicly; a post that’s “shadow banned” should not.

What’s next

API abuse & scraping

The read-side counterpart: allow crawlers, block LLM scrapers.

Signup protection

Stop the account factory before the posts start.

Verdicts & scoring

How verdict, risk_score, and attribution fit together.

Going to production

Report-only and shadow-mode rollout plans.