In November 2025 Anthropic disrupted a state-sponsored operation in which Claude Code performed 80–90% of the intrusion work autonomously across roughly thirty organisations — reconnaissance, exploitation, lateral movement, exfiltration (incident 1263). That one was stoppable because it ran on a commercial API and a vendor could kill the accounts. Frontier weights are a free download now, so the next one will not be. The enumeration step of an attack has become cheap enough to point at everything, forever.
Install
One file, no dependencies. Set PROOFWIRE_KEY in your environment, mount it after your body parsers, and you are done — detection, scoring and correlation all happen in the API.
curl -o proofwire.js https://layersecurity.jp/proofwire/express.jsconst proofwire = require("./proofwire")({ block: true });
app.use(proofwire.express());Drop block: true and it only reports. Not on Node? A Cloudflare Worker does the same thing in front of any backend — PHP, Rails, Java, a static bucket — without touching the app.
100,000 events per month, free. No card. The console shows your key, your usage and every session that has been confirmed.
Prefer the terminal? This creates the account and returns a password you can sign in with later.
curl -sX POST https://api.layersecurity.jp/v1/signup \
-H 'content-type: application/json' -d '{"email":"you@company.com"}'
{"key":"pw_…","password":"…","console":"https://layersecurity.jp/proofwire/app/"}What it looks like in production
Two sessions on the same app:
That key was never published, never linked, never in a robots.txt, never in a repository. It existed in exactly one place: inside a page we handed to this session seventeen seconds earlier. There is no path by which a real user, a real crawler or a real integration produces that request. It is a confession, and confessions do not have a false-positive rate.
Why the usual signals don’t work
| Signal | Why it fails |
|---|---|
| Rate / burst | A person hammering refresh fires fifteen requests in ten seconds. An agent pacing itself fires one every four. |
| User-agent, headless flags | One line of config. Every serious tool already lies about all of it. |
| “Looks automated” | Your customers’ own agents look automated. Blocking on this is how you lose accounts. |
| Honeypot paths, fake /admin | Advertised. A capable agent notices the surface doesn’t match the app and works around it — and now it knows you’re watching. |
| WAF signatures | Written against payloads people have already seen. A model composes a request nobody wrote a rule for. |
| Using a value only you could have given it | Not producible by a legitimate client, at all, ever. |
How it works
- Nothing is advertised. No breadcrumbs, no fake routes, no extra hostname, no banner. A scanner sees a surface byte-identical to your origin and gets your app’s real 404, not a synthesized one. There is nothing to notice and nothing to route around.
- The values ride inside responses you already send — shaped like AWS keys, live API keys, database URLs, placed in an HTML comment and a JSON field. They are derived from your key with HMAC, so we store none of them and no two customers ever share one.
- Use is the signal, not appearance. Nothing about being fast, scripted or headless can confirm a session. Only carrying one of those values can.
- Blocking is decided locally, with no round trip. The client already holds the values, so a request carrying one is refused on the spot — there is no window between the confession and the block.
- Behavioural signals — burst rate, path breadth, zero think time, 404 fuzzing — raise the score, but on their own they stop at
likelyand never block. We require breadth (10+ distinct paths) before even that, because a human hammering refresh trips everything else. - Fails open. API down, quota exhausted, config unreachable — your response goes out untouched. Telemetry leaves behind the response, batched. It is not in your critical path.
What it does not do
- It does not stop a patient human attacker who behaves like a human. That is the trade-off that buys you an alert stream with nothing to triage.
- It is not a WAF and does not replace one. With
blockoff it blocks nothing at all. - A blocked attacker learns they were caught. If you would rather watch, run in report mode — the detection is identical.
- Detection is only as good as where those values end up. If nothing credential-shaped ever reaches the attacker, there is nothing to confess with.
- The client rewrites
text/htmlandapplication/jsonbodies (one comment, one_kfield). If you sign or hash response bodies, scope it to the routes that matter. - Sessions are keyed on IP + user-agent. Behind a shared NAT that is coarse — though the confession itself does not depend on session identity being perfect.
- v0. The API surface will change. No external security review yet.
Any language
Two HTTP calls. That is the entire protocol.
# what to plant — cache it, refresh every few hours
GET https://api.layersecurity.jp/v1/config Authorization: Bearer pw_…
-> { "bait": [ { "id":"aws", "placement":"html-comment", "value":"AKIA…" }, … ] }
# what happened — batch up to 200, fire and forget
POST https://api.layersecurity.jp/v1/events Authorization: Bearer pw_…
{ "events": [ { "session":"e6dd", "path":"/api/keys", "status":403,
"dt":4, "bait":"AKIA…" } ] }
-> 202 { "alerts": [ { "level":"confirmed", "score":0.95,
"evidence":["decoy_hit x1","breadth=25_paths"] } ] }Set bait to the value you found in the inbound request, or null.
Pricing
| Free | 100,000 events / month, every feature. No card. |
| Past that | 429 with a reset date. Nothing breaks — the client keeps failing open. Mail us and we raise it. |
| What we store | Path, status, user-agent, a hashed session id, and whether one of those values appeared. No request bodies, no response bodies, no headers. |