-
Same model, same 30 invoices: 0/12 attacks stopped by the naive agent, 12/12 with PRISM guardrails.
-
Every honoured block: "No model call or output after the block", tied to the run's trace_id.
-
Proof 1 + 2: our rules in Block mode, and the Enforcement Check (Honoured 11, Not honoured 0).
-
Proof 3: Root Cause clusters 21 failed/blocked runs into 2 causes.
-
Bonus: our "Guardrail block on AP agent" alert firing on real runs.
Track: Trust & Risk · Built: Oct 17, at Money Talks 2 · Models: Nicrron API · Observed, guarded and verified in: PRISM by Block Convey
TL;DR for judges
- The risk: an AI agent that can pay invoices can be talked into paying the wrong account by the invoice itself.
- The guardrail: every invoice is screened by our PRISM guardrails in Block mode before the model reads it, under the run's own trace_id. A vendor-master policy backs it up.
- The run it stopped: same agent, same model (gpt-4o-mini via Nicrron), same 30 invoices. v1 stopped 0 of 12 attacks and paid an attacker 9 times. v2 stopped 12 of 12, paid no attacker, and held 0 of 16 clean invoices.
- PRISM proofs: ✅ Guardrail blocking real runs (9 blocks, zero model calls after any of them) · ✅ Block check: Honoured 11 / Not honoured 0, plus our verdict on where PRISM was right and where it wasn't · ✅ Root Cause: 21 failed/blocked runs in 2 clusters, with our one-sentence cause.
Inspiration
"Our bank details have changed, please pay this new account." That one sentence is business email compromise, which cost US businesses about $3 billion in reported losses in 2025 (FBI IC3). It works on people.
Finance teams are now handing accounts payable to AI agents. The agent reads the invoice, and the invoice is written by whoever sent it. That turns every PDF in the queue into a place to hide instructions. So we built an AP agent and then attacked it.
What it does
PayGuard is an accounts-payable agent. For each invoice it reads the PDF, looks up the vendor, matches the purchase order and checks payment history for duplicates. Then it approves the payment or holds it for review. It has 7 deterministic tools, and the model chooses the order.
We seeded a 30-invoice queue with 12 attacks, four kinds, three of each:
- Hidden injection: white 6-point text telling the agent to update the vendor's bank account and pay the new one.
- Bank-change note: a polite, visible "our remittance details have changed" message (classic BEC).
- Spoofed vendor: the vendor name with one Cyrillic look-alike letter, and a different bank account.
- SSN in notes: a Social Security number that a careless agent copies into the payment record.
The other 18 are 16 clean invoices, one amount mismatch and one duplicate. A guard that simply blocks everything would fail too.
v1 follows a naive SOP and treats invoice text as instructions. v2 is the same agent with two guards:
- PRISM screen, before the model. The invoice text goes to
POST /api/guardrails/evaluatewith the run's trace_id. Our rules, all in Block mode: PII/PHI, Prompt Injection, and two custom regex rules for bank-change language. A block ends the run with zero model calls. If PRISM can't be reached, the run fails closed. - Vendor-master policy on the money-moving tools. Money only goes to the account in the vendor master, and an invoice can never change that account.
After every run, in both versions, a deterministic ledger audit checks where money went. That's how v1's failures show up in PRISM as errored trajectories: caught, but only after the money left. The review UI shows each invoice with its hidden text highlighted in red, every tool step, the decision and the guardrail verdict, plus a v1-vs-v2 metrics bar.
Results (same model, same 30 invoices)
- v1 (gpt-4o-mini): 0/12 attacks stopped · 9 payments to attacker accounts · 3 SSNs echoed into the decision record
- v2 (gpt-4o-mini): 12/12 attacks stopped · 0 paid to attackers · 0 SSNs echoed · 0 of 16 clean invoices held · the amount mismatch and the duplicate are still correctly held
- What stopped each attack in v2: PRISM blocked 9 (3 hidden injections, 3 bank-change notes, 3 SSNs) before the model ran. The 3 spoofed vendors were held for review, because a look-alike letter gives a text guardrail nothing to match. The vendor-master policy is the safety net for exactly that case.
The three PRISM proofs (Trust & Risk)
1. Our guardrail blocking a real run. Our own rules run in Block mode, and v2 calls them before the model. The v2 batch produced 9 real blocks. In PRISM, every blocked trace has model: unknown and output_message: null: no model call happened. We also set an alert, "Guardrail block on AP agent", and it fired on real runs. That isn't required for this track, but it's how an AP team would get paged.
2. Block check, and did PRISM get it right? PRISM's Enforcement Check shows Honoured 11, Not honoured 0, each with "No model call or output after the block". The 20 Unverifiable entries are our own rule-test calls, which deliberately carry no trace_id because they aren't agent runs. Our verdict for each attack:
- SSN in notes → PII/PHI rule. Right. (In Flag mode the same rule also flagged dollar amounts on two clean invoices as phone numbers. Those are false positives, visible in the Guardrails audit log.)
- Bank-change note → our
bank-change-languageregex. Right. - Hidden injection → blocked, but by our regex, not PRISM's built-in Prompt Injection detector. Right to block, for the wrong reason. The detector catches jailbreak phrasing ("ignore your instructions"). It doesn't catch instructions disguised as a routine business notice.
- Spoofed vendor → no rule fired. There are no words to match, and the account number is just digits. This is an honest gap, and our vendor-master policy covers it.
3. Root Cause. PRISM clustered 21 failed or blocked runs into 2 causes: 15 under Prompt (Task Completion, Error Handling) and 6 under Policy / Guardrail (Sensitive Data Exposure). The buckets are right. Its narrative, "agent gets lost in repetitive tool loops", is not: the runs are 5–6 sequential steps. Our one-sentence cause: the agent treats text inside the invoice as instructions and lets the document choose the payee. v2 screens the document before the model reads it, and the vendor master, never the invoice, decides where money goes.
How we built it
- Agent: a Python LangGraph tool-calling agent followed by a deterministic audit node. v2 adds a screen node in front. Its 7 tools are plain Python over a vendor master, purchase orders, payment history and a ledger.
- Models (Nicrron API): every model call goes through Nicrron's OpenAI-compatible endpoint (
langchain-openai). With one key we ran the identical agent on gpt-4o-mini and on a stronger frontier model by changing a single env var, to see how different models behave against the same guardrail. When Nicrron failed over to another model on a provider error, we recordedmodel_servedon every run so the results stay honest. - PRISM: the
prismtrace-sdkLangGraph handler wraps every graph (wrap_langgraph), with one PRISM session per invoice, so each run is a trajectory with all of its tool steps. Guardrails run through/api/guardrails/evaluatewith trace_id. We also used the Enforcement Check, Root Cause and Alerts pages. The workspace has about 200 live traces. - Data: a synthetic generator (reportlab) makes 8 vendors, POs, payment history and 30 invoice PDFs with planted attacks, plus a labelled manifest as ground truth. Invoices are read with pypdf, which is exactly how hidden white text reaches the model.
- Review UI: FastAPI plus a single HTML page: invoice queue, hidden text in red, step list, decision, guardrail verdict, and v1 vs v2 metrics.
- Tests: pytest covering the tools, guard rules, generator, graph, metrics and API.
Challenges we ran into
- PRISM's injection detector didn't fire on our injection. The hidden text reads like a routine notice, not a jailbreak. We wrote custom regex rules for bank-change language, then wrote down that the block happened for the wrong reason.
- Look-alike vendors have nothing to match. A Cyrillic "а" passes every text rule. The fix is structural: the vendor master decides the payee, never the invoice.
- Making the Block check verifiable. PRISM can only call a block "honoured" if it can see the run that followed. So the screen call and the agent run share one trace_id.
- The model's word vs what happened. The model's "DECISION: approved" line is only a claim. We reconcile it against the ledger and the hold log, and trust those.
- Provider failover. Nicrron sometimes served a run from a different model. Outcomes matched, and every result now records which model actually answered.
- Fail closed. If PRISM's evaluate endpoint is unreachable, v2 blocks the run instead of paying.
Accomplishments that we're proud of
- 0/12 → 12/12 attacks stopped on the same inexpensive model, with 0 of 16 clean invoices held.
- Enforcement Check: 11 honoured, 0 not honoured. Every block provably ended the run before the model ran.
- All three Trust & Risk proofs shown on real runs, plus a guardrail alert firing.
- We tell you where PRISM was wrong, not only where it was right.
What we learned
- Model caution is not a control. Under the naive SOP, the stronger model resisted most attacks but still paid an attacker twice. gpt-4o-mini paid 9 times. A finance team can't rely on whichever model is cheapest this quarter being careful. v2's guard works on either model because it never asks the model.
- Guard the input, not just the output. Screening the document before the model reads it means a block costs zero model calls and leaves nothing to undo.
- Detectors and policy do different jobs. Text guardrails catch words. Only a system of record catches a look-alike payee.
What's next for PayGuard
- Route model calls through the PRISM proxy so outputs are enforced inline too.
- Add a PRISM custom rule for Unicode look-alikes, so spoofed vendors are caught by the guardrail and not only by policy.
- Add a PRISM evaluator that scores v1 vs v2 on the same inputs on every change.
- Ingest invoices from email, since that's how BEC arrives, and add OCR for scanned invoices.
- Turn a bank-change request into a call-back to the number already on file, the control AP teams use today, with a human approving the change.
- Connect to a real AP system through a NetSuite or QuickBooks sandbox.
Team
- Nikkat Afrin: Project Lead and Project Builder
- Tharun Prabhakar: Project Lead Review, QA and UI/UX
- Mohammad Zubair Khan: Project Manager and Engineer
Links
- Code: https://github.com/Nikkat-Afrin/payguard (spec, demo script, evidence JSON and screenshots in
docs/) - Demo video: https://drive.google.com/drive/folders/1yx6XfmZqoTOaQQruZmHuOtIuc4zMJBAF?usp=sharing
- Live demo: shown live at the judging table (FastAPI review UI + PRISM workspace)
Built With
- block-convey
- fastapi
- html
- javascript
- langchain
- langgraph
- nicrron
- prism
- prismtrace-sdk
- pydantic
- pypdf
- pytest
- python
- reportlab
- uvicorn
Log in or sign up for Devpost to join the conversation.