Carlos Sanoja
All work

COUNTERSIGN: an agent fleet that cannot sign or pay

September 2026DevNetwork API + Cloud + AI Hackathon 2026

Business email compromise often starts with an invoice from a lookalike domain claiming the vendor's bank account changed, and the usual control is a person looking at a PDF. COUNTERSIGN is a seven-stage agent pipeline that does the whole verification file in about forty seconds and then stops: signing and payment are capabilities that no agent in the fleet holds, and the gate fails closed on anything undeclared. On six labelled invoices it returned the right verdict 6/6 and was refused a signature 12/12 times. On a 20-invoice soak corpus (16 legitimate, 4 fraudulent) it detected 8/8 fraudulent runs but still flagged 9/32 legitimate runs, a 28.1% false-positive rate that I report as too high to deploy. I built it on 1 and 2 September 2026, and I do not know how it was judged.

1Problem

An invoice arrives from a domain one character away from the real vendor's and says the bank account has changed. The control that should stop it is a person in accounts payable reading a PDF for about four minutes. The project README cites the FBI IC3 2025 report for $3,046,598,558 in reported losses across 24,768 complaints, with a $123,005 median, and Ardent Partners (2025) for $12.88 to $19.83 per manually processed invoice.

The failure is not carelessness. The check is not reproducible: nobody has time to find the vendor's real domain, the bank-detail change gets confirmed over the channel the attacker already controls, and afterwards nothing records what was checked. I wanted a system that does the checking work consistently and leaves a trace, without being a system that can be talked into moving money.

2Approach

The pipeline has seven stages. Ingest parses the invoice deterministically with Nutrient DWS; the model only maps spans that already exist onto named fields, each field keeps the page and box it came from as fractions of the page, and a value it cannot anchor comes back absent rather than invented. Identity uses SerpApi to ask whether the counterparty is the same legal entity or a namesake, whether coverage is adverse, and whether the address is a business or a mail drop. Domain uses name.com to check 40 confusable variants across eight attack classes against the live registry. Risk fuses the evidence into a verdict where every claim must cite an evidence span; a verdict that cites nothing is rejected and retried. Generate produces an out-of-band bank verification document with Foxit PDF Services, Deliver prepares a Foxit eSign envelope in draft and stops, and Persist writes the vendor record and an append-only audit log to Xano.

The central claim is checkable in code. In fleet/capabilities.py, signature.execute and payment.release are declared as human-only capabilities and are absent from the set the agents hold. They are declared on purpose, so that an attempt resolves to a named capability and is denied on the record instead of being inexpressible and therefore unauditable. A tool that is not in the mapping resolves to no capability and is denied, so the gate fails closed. tests/test_boundary.py fails if anyone grants an agent the power to sign, and tests/test_adversarial.py tries to break the boundary.

Four of the seven agents call Gemini 3.5 Flash or Flash-Lite on Vertex AI. Three deliberately do not: the domain sentinel is deterministic so its signal survives an audit, the envelope preparer is plumbing, and the injection screener stays outside a model because asking a model whether a document is manipulating a model puts the judgement inside the blast radius. IBANs, BICs, tax identifiers and mailboxes are masked before any prompt is built, so no account identifier enters a model context. The capability ledger, permission gate, injection armor, review queue and evidence-span verification come from my earlier quanta-gradesync project; the README states this is a new domain on that base, not a fork.

3Architecture

Seven agent stages inside a capability boundary; signing and payment sit outside it Agent boundary: nine capabilities held IngestNutrient IdentitySerpApi Domainname.com RiskGemini + rules GenerateFoxit PDF DeliverFoxit eSign PersistXano Outside the boundary: no agent holds these signature.executeperson only payment.releaseperson only Deliver leaves the envelope in draft;a denial is written to the audit log
The seven stages run inside the agent boundary. The two human-only capabilities are declared but held by no agent. Based on fleet/capabilities.py, where nine capabilities are in the agent set.

The envelope preparer requests the signature on every run and is refused every time. The refusal is an audit row, for example seq=5 envelope-preparer foxit_execute_signature deny, rather than a silent omission.

4Results

Labelled benchmark

MetricResultMeasured on
Verdict accuracy, including 2 negatives6/66 labelled invoices, live APIs
Same invoice twice, same verdict6/62 runs per invoice
Citations matching collected evidence35/35All verdict citations
Providers cited in verdicts3/3name.com, Nutrient, SerpApi
Signature denied12/12Every benchmark run
Probes denied through the real gate4/4Probe raises if the gate lets it through
Human-only powers denied16/16Every agent against every reserved power
Median latency40.4 sBenchmark runs

Measurement: six invoices from one template, two runs each, live providers, known official domain held fixed. This measures reproducibility and the signal logic. It is not a claim about accuracy on real mail. A fabricated source was also rejected, and a legitimately granted call was still allowed, which rules out a gate that passes by denying everything.

Soak test

MetricResultMeasured on
Detection of fraudulent runs8/84 fraudulent cases, 2 passes
False positives (flagged, legitimate)9/32 (28.1%)16 legitimate cases, 2 passes
Would block a real payment2/32 (6.2%)Same legitimate runs
Verdict stability across passes19/2020 cases
Signature denied40/40All soak runs
Latency p50 / p9526.3 s / 42.5 s40 runs

Measurement: demo/soak, 20-invoice corpus, 40 runs over 2 passes, five concurrent. The corpus is 80% legitimate because fraud is rare and a balanced set would misstate the problem. The generation and delivery stages were closed in the soak, so latency excludes them. These are my own in-repo measurements, not an external evaluation.

False-positive trajectory

Stage of the fixFalse-positive rate
First soak80.7%
After the first round of fixes50.0%
After the second round28.1%

Measurement: the three figures are quoted from the README, which gives the trajectory but not the per-round corpus or run counts. Only the final 28.1% (9/32) is backed by the report file in the repository.

One live run

In the worked case name.com is the impersonated vendor and narne.com the fictional fraudulent sender, using an rn for m substitution. The run checked 24 names, found 8 confusables already registered, scored the invoice high at 1.00 on 3 signals from 17 evidence items, left Foxit envelope 35674834 in draft, wrote 10 audit rows to Xano, and was refused the signature. Nothing was mocked. I state registry facts only and make no claim about whoever holds that domain.

5What did not work

The false-positive rate is still 28.1%

After two rounds of fixes, 9 of 32 legitimate runs were flagged and 2 of 32 would have blocked a real payment. All five flagged legitimate cases are listed in the soak report, and the blocked one is a legitimate sibling TLD. A domain registry can say a variant is taken, not whether name.net belongs to the same company as name.com. I left this named rather than tuning it away, and I would not deploy the control as it stands.

The soak found defects the benchmark did not

The benchmark passed 6/6 while two real bugs sat underneath. The vendor baseline was written from whatever invoice arrived first, so a supplier whose first invoice was fraudulent would have had the attacker's account enrolled as its baseline. And billing subdomains were compared to the official domain by string equality, so every legitimate invoice from invoices.name.com carried the vendor's own confusable surface as a finding against it. The README ties the drop from 80.7% to 28.1% to fixing defects like these; it does not break the drop down per defect.

Verdicts moved between runs

One case, a clean legitimate name.com invoice, returned review on one pass (score 0.35) and clear on the next (score 0) with the same single signal. That is the 19/20 stability figure. Four times during the build, a fact a rule could settle had been given to a model and came out differently between runs, or not at all: the sender domain, the bank-detail change, the IBAN, and the sender-versus-official comparison. Settled signals now travel with the evidence and take precedence over the model's draft.

Providers and integrations

In the soak, 23 stage executions were degraded or failed, with 4 SerpApi faults and 1 Vertex fault recorded, and every run still produced a verdict. Doctavian is integrated but was never configured: its OAuth consent screen was in testing mode and rejected my Workspace account with 403 access_denied, so Foxit PDF Services covers generation. Foxit's envelope fetcher also could not download from every public host; raw.githubusercontent.com worked while w3.org and africau.edu failed with a generic download error.

Open evaluation limits

The benchmark is six invoices from one template, accuracy is measured given a known official domain, and the domain sweep reports registered or available, never who owns it. I do not know the hackathon result.

6Lessons