Hosted API · free tier

The attacks are automated now. Block them in two lines.

Proofwire tells you which sessions are attacking your app — with certainty, not suspicion — and refuses them in the same request. Real users are never touched, because the signal it fires on is one they cannot produce.

100,000 events / month freeNo cardFails open by design
server.js
// 1. fetch the client
//    curl -o proofwire.js \
//      https://layersecurity.jp/proofwire/express.js

// 2. two lines (the key comes from PROOFWIRE_KEY)
const proofwire = require("./proofwire")({ block: true });

app.use(proofwire.express());

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.

And you cannot meet it by detecting “automation”. Half your legitimate traffic is automated, and the agents your own customers point at you are indistinguishable from the ones attacking you — same libraries, same headers, same speed.

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.js
const 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.

Create an account to get a key

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:

21:04:11 GET / 200 sess=4f2a think=1.4s 21:04:19 GET /pricing 200 sess=4f2a think=8.2s 21:04:41 GET /login 200 sess=4f2a think=22s └─ a person. nothing is ever emitted for this session. 21:07:02 GET / 200 sess=e6dd think=— 21:07:02 GET /.env 404 sess=e6dd think=8ms 21:07:02 GET /admin 404 sess=e6dd think=6ms 21:07:03 GET /api/v1/users 200 sess=e6dd think=4ms 21:07:03 GET /backup.sql 404 sess=e6dd think=5ms … 25 paths in 9 seconds ⚠ likely 0.60 breadth=25_paths fuzz_404=11 no_think_time(4ms) └─ suspicious. not actionable. we do not block on this. 21:07:19 GET /api/keys?k=AKIAC5192241… 403 sess=e6dd 🚨 confirmed 0.95 decoy_hit x1 + breadth=25_paths └─ refused. and every request from e6dd for the next 30 minutes.

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

SignalWhy it fails
Rate / burstA person hammering refresh fires fifteen requests in ten seconds. An agent pacing itself fires one every four.
User-agent, headless flagsOne 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 /adminAdvertised. A capable agent notices the surface doesn’t match the app and works around it — and now it knows you’re watching.
WAF signaturesWritten against payloads people have already seen. A model composes a request nobody wrote a rule for.
Using a value only you could have given itNot 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 likely and 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 block off 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/html and application/json bodies (one comment, one _k field). 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

Free100,000 events / month, every feature. No card.
Past that429 with a reset date. Nothing breaks — the client keeps failing open. Mail us and we raise it.
What we storePath, status, user-agent, a hashed session id, and whether one of those values appeared. No request bodies, no response bodies, no headers.
Built by Layer Security (Tokyo). Our main product is shadow-AI detection; Proofwire came out of asking what an attack surface should look like when the attacker is a program. Holes in the reasoning are very welcome — contact@layersecurity.jp.