How it works
A registry, a trust check, and a pure verdict function
Fromenance delivers communication provenance by matching a customer's submission against a registry of what your institution actually sent, using a checksummed verify code, a recipient HMAC, and a content fingerprint. No model sits on the verdict path, so the answer is a lookup your compliance team can stand behind, not a probability.
Trust model
Before we look at a single forwarded message we establish that it came through your infrastructure.
One TXT record anchors the tenant
_fromenance.yourdomain.com TXT "v=fromenance1; t=<slug>; k=<token>". We poll DNS every 60 seconds until it resolves, re-check daily, and suspend inbound for that domain if the record disappears. Your DNS footprint is one record.
The forwarding hop must authenticate
Mail to <slug>@verify.fromenance.com is processed only when it carries a DKIM signature that verifies for a domain you own, or an ARC chain whose last seal is by such a domain, and that domain's TXT token matches. We run DKIM, ARC, and SPF ourselves inside the Worker on the raw MIME.
Everything else is held, not answered
A message that fails any part of the trust check is stored for 24 hours as rejected inbound, visible to our operators for diagnosing onboarding problems, never processed, never replied to, then deleted.
Redirect, not forward
The rule on verify@yourdomain.com redirects to your tenant inbox so the customer stays the sender and the original headers survive. Guides for Google Workspace, Microsoft 365, Proofpoint, Mimecast, Cisco Secure Email. The setup wizard sends a test and refuses go live until the rule is right.
What a registration carries
The SDK computes everything sensitive locally. The API never receives a raw recipient address or a message body.
POST /v1/communications
{
"channel": "email",
"recipient_hash": "hmac-sha256:9f2c...",
"message_id": "<c1b2@northfieldbank.com>",
"from_address": "alerts@northfieldbank.com",
"sent_at": "2026-09-24T14:42:10Z",
"content_fingerprint": { "simhash": "0x9a4d...", "sha256": "e3b0..." },
"link_domains": ["northfieldbank.com"],
"template_id": "fraud-alert-v3",
"provider": "api"
}
200 OK
{ "id": "com_01J...", "verify_code": "KX73-PQ9G", "expires_at": "2026-12-23T14:42:10Z" }The response is a verify code bound to this registration and this recipient. You put it in the footer. Codes are unique per tenant, rotate per message, and expire with your retention window (default 90 days).
For ESPs that cannot hand us the rendered body, POST /v1/communications/reserveissues the code before send. The ESP's send event completes the registration. A reservation that is never completed within 24 hours expires and is reported as a coverage gap.
Every POST accepts an Idempotency-Key stored for 24 hours. Rate limits are 600 registrations per minute per key and 6,000 batch items per minute.
Extraction, in order of signal strength
Deterministic, in this order, on every submission from either entry point.
1. Verify code
Eight Crockford base32 symbols shown as XXXX-XXXX. The eighth symbol is a checksum, so the extractor only accepts codes that validate, including from OCR text. A code that resolves to a registration is the strongest signal.
2. Recipient
The customer's address from the redirect envelope, and the innermost To header of the quoted message when present. Both are hashed with your tenant HMAC and compared to the recipient_hash on the registration. We never see the address in clear on the API path.
3. Content fingerprint
The quoted body goes through the same normalization used at registration and becomes a 64 bit SimHash. A match is Hamming distance 6 or less, a tenant tunable threshold validated in the pilot.
4. Headers
Only present when the customer forwarded as an attachment or their client preserved them. When present, we verify DKIM for your sending domains against the original message.
5. Links and indicators
Every URL, domain, phone number, email address, and QR payload, with your own domains skipped. Stored on the submission, used now for Known fraud matching, clustered later.
Content normalization
Both sides compute the same fingerprint because both run the same code from packages/core. This is the pipeline.
- 01Strip HTML to text and drop tracking pixels
- 02Unwrap gateway links (Proofpoint v2 and v3, Microsoft Safe Links, Google redirects)
- 03Remove forward chrome: Forwarded message markers, From/Sent/To/Subject blocks, "wrote:" lines, quote markers, device signatures
- 04Remove our own footer block
- 05Cut at the signature delimiter
- 06Replace every URL with its bare registered domain
- 07Collapse whitespace and lowercase
- 08SimHash over word 3-gram shingles with FNV-1a 64, identical bits in Node, Bun, and Workers
The verdict function
decide() is pure: no I/O, no model, no randomness. It implements the matching rules table and is covered by a test matrix for every row, including replay.
- Known fraudfirst, when an indicator is on the fraud list or the fingerprint matches an analyst confirmed fraud sample.
- Verifiedwhen the code resolves and the recipient matches, unless both fingerprints are available and far, in which case the customer gets Not verified and the submission is flagged as a replay.
- Not verifiedwhen the code resolves but the recipient does not match. Flagged as a replay when the fingerprint is far or unavailable; a close fingerprint from the wrong mailbox is a forward from a second account, not an attack.
- Verifiedwith no code, when the nearest registration for that recipient inside the window is within the fingerprint threshold.
- Not verifiedotherwise, worded as "we did not send this" only in Authoritative Mode.
The pipeline
One Hono API on Cloudflare Workers fronts everything. Queues decouple the mail pipeline from the request path so a burst of forwards never slows registration. Neon Postgres is the system of record, D1 an edge mirror, R2 the raw store.
Step 1: Ingest
Resend delivers the raw MIME through a Svix signed webhook. We copy it to R2 under your tenant prefix, encrypted, and enqueue it.
Step 2: Match
Parse, authenticate, extract, look the code up in the edge mirror, compare the recipient HMAC, compute the fingerprint, run decide().
Step 3: Reply
Render your template for that verdict with the locked block and timestamp, send from your reply identity, emit webhooks, and queue the intel job.
Questions evaluators ask
- Why does a registry beat a classifier?
- A classifier estimates whether a message looks like phishing. A registry knows whether the institution sent this message to this person. The first is a probability that can be gamed by better writing; the second is a lookup that an attacker cannot insert themselves into.
- What survives a forward?
- The verify code almost always survives, which is why it is the primary signal and appears twice in the footer. The recipient survives when the rule redirects rather than forwards. The body survives well enough that Gmail, Outlook, Apple Mail, and Proofpoint rewritten forwards of the same message land within Hamming distance 6 of the original. Headers survive only in attachment forwards.
- What if an attacker copies our verify footer?
- They will, and it is useful. The copied code is bound to a registration and a recipient. Pasted into a different message it fails the fingerprint; sent to a different person it fails the recipient. The customer gets Not verified and we flag a replay attack with high priority, which means the lure was delivered straight to your fraud team by its target.
- Is there any machine learning in the verdict?
- No. Extraction and matching are regex, MIME parsing, HMAC comparison, and SimHash distance. The only model in the system is OCR for screenshots, and its output is treated as untrusted text that feeds the same deterministic extractors.
- How fast is it?
- Targets are under 10 seconds at p95 from inbound webhook to reply sent, under 3 seconds for a pasted message on the web page, and under 150 ms for the registration API. Code lookups are served from a D1 edge mirror with Neon as fallback.
- What does the customer see if we have not registered the message?
- Not verified, worded as "no registered communication matches this message. That does not by itself mean it is fraudulent, but treat it with caution." The reply never says a message is fraudulent unless an analyst has confirmed it, and never says "we did not send this" unless the tenant is in Authoritative Mode.
Run a 60 to 90 day pilot on one communication stream.
Fraud alerts or transaction alerts are the best first stream. You get a written report of verification volume, verdict distribution, campaigns discovered, and intelligence yield.