Why Per-Tool-Call Attestation Beats Post-Hoc Audit Logs
The CFPB's Circular 2022-03 on Responsible AI requires that any AI system making credit decisions produce "specific reasons" for each decision. But here's what regulators are finding: companies log everything, and when asked to explain a decision from three months ago, they can't. The logs show "agent called LLM, LLM returned score 0.42," but not why the LLM weighted income at 0.8 and debt-to-income at 0.2. Log files alone don't meet the standard.
Per-tool-call attestation fixes this. Each decision gets a cryptographic signature at the moment it's made. The signature proves what data went in, which tools were called, and what came out. When a regulator asks six months later, you don't reconstruct—you retrieve the attestation and show them exactly what happened.
This isn't hypothetical. Lending fintechs and bias-audit firms are already dealing with FCRA disclosures, ECOA Regulation B findings, and CRB adverse-action notices that require this level of proof.
The Post-Hoc Problem
Traditional AI audit logs capture state, not decisions. They log the happy path, not the reasoning.
What you log now (best case):
timestamp: 2025-11-15T14:32:17Z
decision_id: req_12345
input: {applicant_id: alice_12345, ...12 fields...}
output: {decision: "deny", score: 0.62}
That's it. You logged that a decision happened, not why.
What a regulator needs:
- Proof that data X was complete and accurate at the time of decision (not later, when you "found" the missing income statement)
- Proof that the agent weighed each input in a defensible way (income: +0.35, debt-to-income: +0.25, not +1.0 for debt and zero for income)
- Proof that the decision threshold (0.65? 0.70?) was applied consistently across all applicants
- A frozen snapshot: this exact configuration, these exact parameters, this exact input, produced this exact output—verifiable via cryptography, not memory
Between logging and regulator inquiry, your codebase changed three times. Your LLM updated twice. Your data pipeline added a new field. Your model tuned the weights. You're now explaining a decision made under a config that no longer exists.
The regulator doesn't care that your current system is fair. They care that the July decision was fair. And you can't prove it because you didn't capture the reasoning at the time.
CFPB Circular 2022-03 specifically calls this out:
"Institutions should maintain appropriate records demonstrating compliance with the ECOA, including: (1) documentation and testing that identifies the potential for algorithmic bias, (2) regular monitoring for performance across relevant demographic categories, and (3) the ability to explain decisions to applicants."
You cannot explain decisions from July to an applicant asking in January if you only have access to July's logs and January's code.
FCRA § 1681m (Adverse Action Disclosure) requires:
"In any case in which a consumer is denied credit or receives less favorable terms based in whole or in part on information obtained from an outside source...the user shall, within a reasonable period of time, disclose the fact of the denial...and the nature of the information which was the principal reason for such adverse action."
You have to disclose the "principal reason" from that decision. If your logs don't capture which factors actually drove the decision, you're guessing.
What Per-Tool-Call Attestation Actually Is
An attestation is a cryptographic proof attached to each decision. It includes:
Input Schema + Hash. The exact fields the agent received, hashed with RFC 8785 canonical JSON encoding. Proves the data wasn't tampered with.
Tool Calls + Parameters. Every external call (to a credit bureau, income verification service, fraud checker) is logged with the exact parameters sent and the exact response received.
Decision Path. Which business rule or scoring logic was applied. Not just "score = 0.72" but "score calculated as: income_weight × 0.8 + debt_to_income_weight × 0.2 + fraud_score_weight × 0.0 = 0.72."
Timestamp + Signer. Proof of when the decision was made and which configuration/LLM version signed it. EdDSA (Ed25519) signature—cryptographically strong, not reversible, not deniable.
Output Schema + Hash. The exact decision output, hashed for proof.
Packed into a standard envelope (JWT format or a custom binary format), signed, and stored immutably. When a regulator asks "explain this decision," you query your attestation ledger, retrieve the envelope, and show them the complete provenance.
How Regulators Read It
Regulator's expected inquiry: "Why was applicant Alice denied?"
Old way: You search logs for Alice's decision, find a summary entry, and say "we ran a scoring model and the score was below threshold." Regulator's next question: "Which features drove that score?" You go back to your team, dig through code, reconstruct the model configuration from that date, and guess.
AttestProto way: You retrieve Alice's attestation envelope. It contains:
{
"decision_id": "att_1a2b3c4d5e6f7g8h",
"timestamp": "2025-11-15T14:32:17Z",
"applicant_id": "alice_12345",
"input_hash": "8f9e7d6c5b4a3219...",
"input_fields": {
"income": 65000,
"debt_to_income_ratio": 0.38,
"credit_score": 680,
"employment_tenure_months": 24,
"fraud_signal": false
},
"tool_calls": [
{
"tool": "credit_bureau_check",
"call_id": "tb_xyz123",
"timestamp": "2025-11-15T14:32:09Z",
"request": { "ssn_last_4": "5678", "check_type": "hard" },
"response": { "score": 680, "inquiries_6mo": 2, "delinquencies": 0 },
"latency_ms": 142
},
{
"tool": "income_verify",
"call_id": "iv_abc456",
"timestamp": "2025-11-15T14:32:13Z",
"request": { "employer_id": "emp_789", "period": "last_2_years" },
"response": { "verified_income": 65000, "stability": "stable" }
}
],
"decision_logic": {
"algorithm": "linear_scoring_v2.1",
"weights": {
"credit_score": 0.35,
"income_stability": 0.30,
"debt_to_income": 0.25,
"fraud_signal": 0.10
},
"calculated_score": 0.62,
"threshold": 0.65,
"decision": "deny",
"principal_reason": "credit_score (0.62 * 0.35 = 0.217) and debt_to_income_ratio (0.38 on limit, contributes 0.10 to final) below threshold"
},
"output_hash": "3a2b1c0d9e8f7g6h...",
"decision_message": "Application denied. Primary factors: credit profile and debt obligations.",
"signature": "ab12cd34ef56gh78...",
"signer_key_id": "key_prod_v2.1_nov2025"
}
Now you show the regulator exactly:
- What data went in (hashed for privacy, but the applicant's own data is verifiable)
- Which external checks ran and what they returned
- The exact weights and thresholds
- The calculated score and the decision
- The cryptographic proof that this happened on this date with this configuration
The regulator can verify the signature independently. Your compliance team doesn't have to guess.
What This Saves a Compliance Team
Adverse-action notices are the biggest pain point.
Old way: Applicant is denied. Your system logs show a score. Your compliance team opens a ticket, calls engineering, waits 2–3 days for clarification, then manually drafts the adverse-action letter from the Regulation B template:
"Your application was denied based on: insufficient credit history, high debt-to-income ratio. You have the right to obtain your credit report..."
They're filling in blanks by memory and hope. They spend 30–45 minutes per notice. With volume, that's 2–3 FTE annually just on adverse-action paperwork.
AttestProto way: Applicant is denied. Compliance team runs:
attestproto adverse-action \
--decision-id att_1a2b3c4d5e6f7g8h \
--applicant-email alice@example.com \
--regulation fcra-ecoa
The CLI reads the attestation, extracts the principal reasons, formats them into FCRA-compliant disclosure language, generates the notice, and sends it. Time: 45 milliseconds.
Your template is version-controlled. Your disclosures are consistent. Your audit trail is immutable. A compliance officer can spot-check 20 notices per week instead of drowning in 80.
For a lending platform processing 1,000 applications per month, that's 1 FTE saved. At $80k salary + burden, that's $80k per year, recurring.
Self-Hosted, MIT — Your Data Never Leaves
The compliance objection is always the same: "Can't use SaaS for this. Attestations contain applicant PII."
AttestProto is self-hosted. Open source, MIT license. Your data lives in your infrastructure—Postgres, SQLite, DynamoDB, wherever you want.
You control the signing keys. You control backup and retention. Your DPO, CISO, and audit team can review the code. No mystery vendor, no data escrow, no SLA roulette.
The CLI is a single binary: ~12 MB, runs in your CI/CD, runs in your backend, runs offline. No API calls to our servers. No rate limits. No vendor lock-in.
(This changes the business model—we don't charge per attestation or per query. AttestProto is free. We'll offer premium managed hosting and compliance consulting as upcharges. But the core ledger is yours, MIT, and offline-first.)
Real-World Deployment Patterns
Lending fintechs: Attestations signed at the moment the underwriting agent produces a decision. Stored in Postgres alongside the application record. On adverse action, compliance CLI queries the attestation and auto-drafts the notice.
Bias-audit firms: Client uploads historical model decisions. AttestProto CLI retroactively attestates (if you saved the input/output) or flags gaps. Used for FCRA-defensibility audit and CRA (Credit Reporting Agency) compliance check.
EU SMBs under AI Act Annex III: Attestations satisfy the "documented decision-making process" requirement. Exported to auditors and regulators on demand. Self-hosted keeps data in EU region for GDPR.
HR systems: Job screening agent flags candidates as "advance to phone screen" or "reject." Each decision is attestated with the resume text, screened factors, and threshold applied. When a candidate sues alleging bias, you have cryptographic proof of what the agent saw and how it decided.
Integration: How It Fits Into Your Stack
AttestProto is not a replacement for your existing AI infrastructure. It's a lightweight sidecar.
Your agent (LLM + tools) produces a decision. Before you log it or return it to the user, you:
from attestproto import Attestor
attestor = Attestor(signing_key="key_prod_v2.1")
# Your agent just returned a decision
decision = agent.run(applicant_data)
# Attestproto wraps it
attestation = attestor.sign(
decision=decision,
input_data=applicant_data,
tool_calls=agent.tool_call_log, # captured from your agent
algorithm_version="linear_scoring_v2.1",
weights=model_weights,
threshold=decision_threshold
)
# Store the attestation
db.store_attestation(decision.id, attestation)
# Return the decision as normal
return decision
Latency: <5ms. CPU: negligible. You don't rewrite your stack—you add a checkpoint.
Storage: ~2–5 KB per attestation (JSON or compact binary). For 10,000 decisions/month, that's 20–50 MB/month. Negligible.
Getting Started
If you're processing credit decisions, you're under compliance scrutiny today. The regulators have moved past "show us your logs" to "prove you made defensible decisions." They're also looking at bias metrics (disparate impact on protected classes, unexplained performance gaps). Attestations let you defend both the decision and the process.
AttestProto is in v0.1.1. Lending-AI-specific features (adverse-action CLI, ECOA weight disclosure, Regulation B templates) shipped this month. The roadmap includes CRB integration (Consumer Financial Protection Bureau automated reporting) and Regulation E compliance packs.
See it in action: attestproto.com/demo — browser widget, runs in your browser, no signup required. Load a sample lending decision, inspect the attestation, verify the signature.
Read the spec: attestproto.com/docs/attestation-format — understand the schema, signature algorithm, and regulatory mapping.
Start self-hosting: github.com/lex-wired/attestproto — clone, follow the README, 10-minute setup. No external dependencies, runs in Docker or bare metal.
Contact us for: EU AI Act mapping, CRB integration, on-premise compliance audits, or managed signing infrastructure.
AttestProto is cryptographic decision attestation for regulated AI. Self-hosted, MIT licensed, built for lending, HR, and high-risk classification systems. When regulators ask "why," you show them proof.
Try it in your browser
Generates an Ed25519 keypair client-side and signs a sample attestation. Nothing leaves the page.
Run the demo →