Vouch AI
Credit That Learns: Adding Long-Term Memory to an Underwriting Engine Autonomous AI agents now execute tasks, hire other agents, and rent cloud infrastructure. To do that at scale, they need credit. Most credit models use fixed formulas. They calculate a score based on completed tasks, spend volume, and repayment track records. If an agent has completed dozens of tasks successfully, a formula gives it a generous credit limit. Formulas work well for individual histories. But they have a major blind spot: they cannot see fleet-wide or contextual patterns. If ten scraper agents belonging to the same operator start defaulting on data-scraping tasks because an upstream target changed its anti-bot rules, a formula treats each agent as an isolated applicant. The lender absorbs ten separate losses before the formula catches up. I built VouchAI to explore a different model: pairing a deterministic credit formula with persistent agent memory. For the memory layer, I integrated Hindsight (GitHub repo), a specialized engine that retains outcomes, reflects on patterns, and recalls relevant facts during loan origination. Here is what I built, the silent failure that humbled me, the real benchmark results, and the design limits you should know about. The Bug: When Tests Pass but Memory Saves Nothing Early in development, I thought the memory integration was working smoothly. I ran my test suite and every test passed. Then I checked the Hindsight bank. It was completely empty. Nothing had been saved. The bug was subtle. In backend/app/services/memory.py, I had written a defensive runner _run() that caught exceptions, retried once, and returned a fallback so transient network hiccups would never crash the lending API: python def run(label: str, action: Callable[[], _T], fallback: _T) -> tuple[_T, float]: for attempt in range(2): try: return action(), latency_ms except Exception as exc: logger.debug("%s attempt %d failed (%s)", label, attempt + 1, type(exc).name_) return fallback, latency_ms Inside retain_outcome(), I called the client like this: python _get_client().retain( bank_id=bank_id, content=content, context="loan outcome", tags=[principal_name, category], retain_async=retain_async, # The bug ) The real Python SDK for Hindsight does not accept retain_async on client.retain(). Calling it raised a TypeError. But because _run() caught every exception to be safe, the error was swallowed silently. It logged a debug message and returned False. My unit tests did not catch this because my mock client used **kwargs in its method signatures: python class FakeHindsight: def retain(self, **kwargs): self.calls.append(("retain", kwargs)) return object() The mock accepted any parameter I threw at it. Tests passed, green checks appeared everywhere, while production calls silently dropped every fact. I found the bug by checking the client's actual method signature: python def retain(self, bank_id: str, content: str, ..., tags: list[str] | None = None) -> RetainResponse: There was no retain_async parameter on single-item retain(). I removed the argument, updated the test assertions to check tags, and verified real retention with a smoke script. Architecture: How Memory Alters Credit Limits VouchAI does not let an LLM invent credit decisions out of thin air. The system anchors every decision to a mathematical formula and uses memory to adjust the limit. Tagged Retention and Strict Recall When a loan resolves as repaid or defaulted, VouchAI writes the outcome into Hindsight with tags for the principal fleet and the task category: python retained, _ = _run( "retain_outcome", lambda: bool( _get_client().retain( bank_id=bank_id, content=content, context="loan outcome", tags=[principal_name, category], ) ), False, ) When an agent requests a new loan, recall_context retrieves past facts matching that exact fleet and category: python response = _get_client().recall( bank_id=bank_id, query=query, budget="low", max_tokens=1200, tags=[principal_name, category], tags_match="all_strict", ) Setting tags_match="all_strict" was critical. Under "any", recall pulled in memories that matched only the principal or only the category, along with untagged facts. That added noise. Strict matching guarantees the underwriter only reviews facts directly relevant to both the fleet and the task. The Constrained Underwriter Prompt The recalled facts are sent to Groq running an LLM underwriter. The prompt explicitly prevents hallucinations and strictly restricts the model to evidence: python SYSTEM_PROMPT = ( "You are a cautious lending underwriter for autonomous AI agents. " "Use ONLY the MEMORY facts given; do not invent history. " "If memory is empty or irrelevant, return multiplier 1.0. " "Lower the multiplier sharply (0.0-0.3) when memory shows defaults by the " "same principal on the same category. A clean repayment history for the " "same agent may raise the multiplier up to 1.2. " 'Reply with ONLY a JSON object: ' '{"multiplier": number 0-1.2, "rationale": "max 2 sentences citing specific memory facts"}' ) If memory is empty, the multiplier is 1.0. The applicant receives exactly what the formula calculated. If memory shows repeated defaults for that fleet on that category, the multiplier drops toward 0.0, shrinking or denying the loan. The Identity Gate Before any math, memory calls, or LLM inferences run, applicants pass through an identity gate in backend/app/services/identity.py: python def check_identity(db: Session, agent: Agent) -> GateResult: reasons: list[str] = [] norm_agent_name = normalize_name(agent.name) # Rule 1: Lookalike cross-fleet collision other_agents = db.scalars(select(Agent).where(Agent.id != agent.id)).all() for other in other_agents: if other.principal_id != agent.principal_id: if normalize_name(other.name) == norm_agent_name: reasons.append( f"Lookalike identity conflict: normalized name '{norm_agent_name}' " f"matches agent '{other.name}' under principal '{other.principal.name}'" ) break # Rule 2: Anti-name washing own_records = task_count + resolved_loans_count if own_records < 5: if principal_defaults >= 2: reasons.append( f"Name washing risk: agent has only {own_records} record(s) (< 5 required) " f"while principal '{agent.principal.name}' has {principal_defaults} default(s)" ) return GateResult(passed=len(reasons) == 0, reasons=reasons) Rule 1 stops bad actors from naming their agent scraper07 to impersonate scraper-07 from another fleet. Rule 2 stops name washing: if an operator racks up two or more defaults, they cannot spin up a new agent with a clean slate to borrow immediately. Dual-Ledger Simulator Design To prove whether memory helps, I built an offline simulator in backend/app/services/simulator.py. Testing on live borrowers can introduce confounding variables. To isolate the impact of memory, the simulator generates a stream of random loan events once from a seed. It then plays that identical stream through two parallel ledgers: python off_ledgers = { agent.id: ModeLedger(agent.tasks, synthetic[agent.id]) for agent in agents } on_ledgers = { agent.id: ModeLedger(agent.tasks, synthetic[agent.id]) for agent in agents } Both modes see the same applicants, same loan amounts, and same underlying default rolls. The only difference is that Mode ON calls Hindsight and updates its multiplier, while Mode OFF relies strictly on the mathematical formula. Real Benchmark Results Here are the exact results from a 40-round simulation using seed 7 against real Hindsight and Groq APIs: Simulation setup: 40 rounds, seed 7, live Hindsight Cloud + Groq (openai/gpt-oss-120b). Total loss, memory OFF: $2,300.00 Total loss, memory ON: $700.00 Approvals ON vs OFF: 28 approvals (12 denials) for ON vs 40 approvals (0 denials) for OFF Net PnL: Memory OFF finished at - 1,872.00.MemoryONfinishedat−344.00. Memory cut total losses by nearly 70%. In early rounds, Memory ON approved loans and took two early hits. But as those default facts were retained, Hindsight reflected on the emerging pattern. When subsequent agents from that fleet applied for loans in that category, the underwriter dropped the multiplier to 0.0 and denied the requests. Mode OFF approved all 40 loans and took every default on the chin. Honest Limitations This project is an experimental prototype. If you are building on these ideas, keep these limitations in mind: The default pattern is synthetic. In simulator.py, I planted a specific test rule: NovaOps agents performing data-scraping default 75% of the time, while the baseline default rate is 6%. This tests whether memory can discover a cluster pattern faster than a formula. It does not reflect the messy reality of production borrower behavior. The exploration/exploitation trap. Once Memory ON decides that a fleet-category combination is toxic, it denies every future application. Because it never lends to them again, it can never learn if the fleet operator fixed their bugs. Without a deliberate exploration budget (e.g. micro-loans to test recovery), memory creates permanent blacklists. The identity gate is basic. The current identity check uses regex string normalization ([^a-z0-9]). It catches basic lookalikes and flagrant name-washing, but it is not cryptographic security. A production system requires cryptographic signatures tied to verified public keys or on-chain agent identities. Simulator vs Live Page isolation. The simulator focuses strictly on the formula-versus-memory comparison and does not run the identity gate. The identity gate runs on the Live Loan page (POST /loans/{agent_id}). Conclusion Formulas measure individual track records. Memory measures collective context. Adding semantic memory did not require replacing our credit math. The formula established the baseline limit, and Hindsight provided the historical context needed to adjust that limit when fleet-level risks emerged. For autonomous agent economies, credit systems will need both.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to