
Reputo
Risk-aware lending with auto top-ups, OCCR(credit risk), and Self-verified identities
About
Risk-Aware Lending with Agentic Auto Top-Ups (Sepolia + Amoy + Alfajores) TL;DR A non-custodial lending protocol on Ethereum Sepolia with a built-in Repay Buffer and a personalized credit score (OCCR). A lightweight agent on Polygon Amoy pays a tiny HTTP 402 fee to authorize automated “top-up” repayments (no swaps, no bridging). Self Protocol on Celo Alfajores enforces “one human → one credit line.” Clean mono-repo, fully instrumented demo, and tidy commits. What problem are we solving? Liquidation risk is scary for retail users, especially in volatile markets. Cross-chain automation is flaky: bridging assets/swaps introduce slippage, MEV, and operational risk. One-size-fits-all LTV ignores user behavior and on-chain reputation. Sybil abuse can sabotage risk controls and incentive design. Our answer: Keep user funds on the lending chain (Sepolia), in a Repay Buffer owned by the lending pool. Make automation pay-per-action via a 402 fee on Polygon Amoy—no value transfer cross-chain. Personalize collateral limits with OCCR (On-Chain Credit Risk) scoring. Gate borrowing with Self Protocol verification on Celo for unique humans. High-level flow (happy path) Verify identity (Celo Alfajores): User completes Self verification → our backend/allowlist attests → IdentityVerifier.setVerified(user, true) on Sepolia (or Merkle proof). Deposit collateral (Sepolia): User deposits cWETH; pool tracks collateralBalance. Borrow (Sepolia): Pool computes personalized LTV using OCCR → user borrows tUSDC. Pre-fund Repay Buffer (Sepolia): User sends some tUSDC to their Repay Buffer (repayBuffer[user]). Turn on Auto Top-Up (Frontend): Set HF threshold (e.g., 1.10). An Agent monitors HF. Market dips: HF falls below threshold. Agent calls /api/topup → receives 402 challenge (pay small fee on Polygon Amoy). Agent pays 402 on Amoy: Sends 0.5 USDC (or MATIC) to payTo with a session memo → submits proofTxHash back to /api/topup. Server verifies + repays from Buffer: Server confirms Amoy payment, re-reads HF on Sepolia, computes needed repayment, and calls repayFromBuffer(user, amount). → HF recovers without swaps/bridges; user funds never left Sepolia. Why this design? No-swap safety: Eliminates slippage, MEV, and bridge risk. All principal/interest stays on Sepolia. Agentic but opt-in: Paying the 402 fee proves user/agent intent for each automated action. Personalized risk: OCCR adjusts LTVs using observable on-chain behavior, not just static tiers. Sybil resistance: Self enforces one human per credit line for sane risk controls. Architecture (three chains, clear roles) Ethereum Sepolia (Core Protocol) LendingPool.sol — deposits/borrows/repays/liquidations + Repay Buffer (depositBuffer, withdrawBuffer, repayFromBuffer). OCCRScore.sol — maintains scoreMicro[user] in [0..1e6] and emits CreditScoreUpdated. IdentityVerifier.sol — isVerified(address) gate for borrowing; supports admin attest or Merkle allowlist. TestToken.sol — simple 18-decimals tokens for collateral (cWETH) and debt (tUSDC). Polygon Amoy (x402 lane) Agent pays a small on-chain fee (USDC/MATIC) to payTo. No user funds bridged. Fee → permission to trigger automation. Celo Alfajores (Identity) Self Protocol verification → backend attests or Merkleizes verified addresses for IdentityVerifier. Repo map (mono-repo): /contracts # Foundry/Hardhat (Sepolia) /frontend # Next.js 15 (App Router, TS) + /api/topup (402 endpoint) /agents # Node/TS agent that watches HF & pays 402 /indexer # Lightweight event indexer + REST (Token API for scores/positions) Core protocol details (Sepolia) LendingPool (essentials) Config: baseLTVbps, liqThresholdBps, liquidationBonusBps, price1e18. State: collateralBalance[user], debtBalance[user], repayBuffer[user]. Views: valueOfCollateral(user) userMaxBorrowable(user) isUnderwater(user) getHealthFactor(user) = (value * liqThresholdBps/10000) / max(debt, 1) Actions: deposit(amount) borrow(amount) → requires IdentityVerifier.isVerified(msg.sender) repay(amount) liquidate(user, repayAmount) Buffer ops: depositBuffer(amount), withdrawBuffer(amount), repayFromBuffer(user, amount) (Optional) repayOnBehalf(user, amount) No-swap rule: All repayFromBuffer sources are the user’s buffer inside the pool on Sepolia. OCCRScore (personalized LTV) scoreMicro[user] ∈ [0..1e6] where 0 = best. Weights (example): Historical 35%, Current 25%, Utilization 15%, Txn 15%, New 10%. Hooks: onBorrow, onRepay, onLiquidation adjust subscores. Personalized LTV: risk = scoreMicro / 1e6 personalizedLTVbps = baseLTVbps * (1 - risk) Emit CreditScoreUpdated(user, scoreMicro) Intuition: Historical: repayments vs. liquidations over time. Current: recent behavior, e.g., days since last repay, active HF. Utilization: debt / (debt + free capacity) or variant. Txn: on-chain activity breadth/frequency (anti-dust behavior). New: cold-start penalty that fades as data accrues. IdentityVerifier isVerified(address) → bool. Path A (on-chain-lite): backend verifies Self proof and calls setVerified(user, true) (owner-only). Path B (Merkle): set merkleRoot; user calls prove(proof) to set verifiedHuman[user] = true. x402 Agentic loop (Amoy + Frontend API) /api/topup (Next.js API route) Request: { user, minHF, proofTxHash? } If no proof: return 402 challenge JSON { "payTo": "0x...amoy", "amount": "500000", "token": "USDC", "chain": "polygon-amoy", "session": "<uuid>" } If proofTxHash: Verify on Amoy that fee amount token was sent to payTo (and memo session if used). Read user HF on Sepolia. Compute needed = targetHF - currentHF → estimate minimal repay. Call repayFromBuffer(user, min(needed, repayBuffer[user])). Return { ok: true, repaid: "<amount>" }. Agent (Node/TS) Monitors HF via Sepolia RPC. When HF < threshold: POST /api/topup → get 402. Pay Amoy fee (USDC/MATIC). POST /api/topup with proofTxHash. Logs: “Repaid X from buffer; HF now Y.” Key point: The fee is the only cross-chain value. Collateral and debt never move chains. Indexer + Token API (off-chain analytics surface) Lightweight service (ethers.js listeners + sqlite/json) that ingests events: Deposit, Borrow, Repay, Liquidate, BufferDeposit, BufferWithdraw, BufferRepay, CreditScoreUpdated. Maintains per-user state (positions, counts, tx stats) and recomputes OCCR off-chain for comparison/tuning. REST endpoints: GET /api/scores/:address → { scoreMicro, factors, updatedAt } GET /api/users/:address/position → { collateral, debt, hf, buffer, history } Frontend: shows On-chain OCCR vs Indexed OCCR (beta) + mini-charts (score & HF over time). Security & trust model (transparent for judges) Non-custodial: Users control deposits; Repay Buffer is inside the pool and withdrawable unless used. No bridging risk: We never move principal/interest off Sepolia. x402 fee is low-risk: Only a small fee on Amoy; failing to pay just means no automation that cycle. Identity trust hop: For MVP, backend attests Self verification (or Merkle). We document the path to direct on-chain verification post-hackathon. Upgradability / Admins: Minimal, clearly documented (e.g., owner price control for demo). Observability: Event logs for every state change; indexer provides time-series for audits. User experience (what the judge sees) Connect wallet → Not Verified (borrow locked). Click Verify with Self → returns Verified; borrow unlocked. Deposit cWETH → Borrow tUSDC (LTV personalized by OCCR). Deposit Repay Buffer (e.g., 50 tUSDC). Toggle Auto Top-Up; set threshold HF = 1.10. Admin simulates price drop → HF dips. Agent pays x402 on Amoy → server repays from Buffer → HF recovers. OCCR updates (on-chain & indexed). User withdraws leftover buffer, repays, then withdraws collateral. What’s innovative here? Agentic safety net without swaps: automation that never touches principal across chains. Behavior-aware credit: OCCR turns on-chain traces into live LTV personalization. Human-bound credit lines: Self verification ensures one unique borrower per line. Composable lanes: Three chains, each doing what they’re best at (credit core, cheap fee signal, identity). Tech stack (concise) Contracts: Solidity (Foundry/Hardhat), Sepolia deployments. Frontend: Next.js 15 (App Router, TS), wagmi/ethers, /api/topup. Agent: Node/TS script (Amoy RPC, simple loop). Indexer: Node/TS service with ethers listeners + sqlite/json. Infra: Alchemy RPCs; test tokens; minimal admin price control for demos. CI/dev-ex: Env templates, scripts, first-commit scaffolds, architecture diagram (PNG/SVG). Example math (readable for judges) HF: HF = (collateralValue * liqThresholdBps / 10000) / max(debt, 1) Personalized LTV: risk = scoreMicro / 1e6 personalizedLTVbps = baseLTVbps * (1 - risk) Example: baseLTV = 70%, scoreMicro = 200k → risk=0.2 → LTV = 56%. Roadmap after hackathon On-chain Self proof verification or cross-chain attestation. Price oracles (Pyth/Chainlink) + multi-asset markets. Smarter OCCR (ML-assisted factors, anomaly detection) and “Good-Borrower” rewards. Optional DEX integration as separate module (while keeping Buffer-first design). Substreams migration for scalable indexing.
How it's made
High-Level Architecture Ethereum Sepolia (core protocol): LendingPool.sol, OCCRScore.sol, IdentityVerifier.sol, TestToken.sol Polygon Amoy (x402 lane): Agent pays a tiny on-chain fee (USDC/MATIC) to unlock a privileged backend action Celo Alfajores (Self identity): Unique-human verification feeding back into Sepolia via attestation/Merkle Frontend (Next.js 15): App Router, TypeScript, minimal admin panel; API route /api/topup implements HTTP 402 Agents (Node/TS): Poll health factor (HF), handle 402 challenge, pay fee on Amoy, trigger repay-from-buffer Indexer (Node/TS + ethers + sqlite/json): Listen to events → recompute OCCR off-chain → expose Token API Repo layout (monorepo): /contracts /frontend /agents /indexer README.md Track 1 — Self Protocol (Celo Alfajores): Unique Human → One Credit Line Why Self helped Self gives us Sybil-resistant borrowing without doxxing users. For a lending protocol that personalizes LTV via OCCR, “one human → one line” keeps incentives sane and curbs gaming. What we actually built Two pragmatic integration paths so the demo never blocks on heavy cryptography: On-chain-lite (chosen for MVP): User completes Self verification on Celo Alfajores Our backend verifies success via Self’s SDK/API Backend attests the EVM address by calling IdentityVerifier.setVerified(user, true) on Sepolia (owner-only) LendingPool.borrow() gates on identity.isVerified(msg.sender) Merkle allowlist (fallback): Collect verified addresses off-chain → compute merkleRoot IdentityVerifier.setMerkleRoot(root) on Sepolia Users prove inclusion once via prove(bytes32[] proof) → we set verifiedHuman[user] = true Both satisfy “one human → one verified account” for the hackathon. We document the path to direct on-chain Self proof verification post-hackathon (no trust hop). Tech details & glue Contracts: IdentityVerifier.sol is a thin gate with isVerified(address) + setVerified or setMerkleRoot/prove Backend: Tiny verifier service (Node/TS) that never stores PII; it only flips setVerified or builds the Merkle tree Frontend: “Verify with Self” button → opens Self flow; on success, UI shows ✅ Verified and unlocks “Borrow” Security notes: setVerified is owner-only, wired to a deployer/admin key held just for the demo Clear audit trail via events: UserVerified(user) We rate-limit backend attest calls and log the Self session ID → EVM address mapping (hashed) What’s hacky (but effective): The admin-attest shortcut trades zero-knowledge purity for a faster demo loop. It’s documented and easily swappable for a full on-chain verification later. Track 2 — Polygon x402 (Amoy): Pay-Per-Action Automation Without Swaps Why x402 helped We wanted agentic safety without moving principal cross-chain. The 402 fee is a cheap signal of intent that lets a bot do something privileged (trigger a repay-from-buffer) without touching user funds on Amoy. What we actually built HTTP 402 challenge at POST /api/topup: Request: { user, minHF } If no proof: returns 402 JSON with { payTo, amount, token, chain, session } If proofTxHash: server verifies fee payment on Amoy, then calls repayFromBuffer(user, amount) on Sepolia Agent (Node/TS): Polls HF on Sepolia via RPC If HF < threshold: calls /api/topup → gets 402 Pays USDC/MATIC on Amoy to payTo with memo = session Calls /api/topup again with proofTxHash Server confirms payment and triggers repayFromBuffer Logs: “Repaid X from buffer; HF now Y” No-swap rule: All repayments use the Repay Buffer on Sepolia. Nothing bridges or swaps on Polygon. Tech details & glue Amoy USDC/MATIC via Alchemy RPC; agent signs with AGENT_PRIVATE_KEY Fee verification: We check to == payTo, amount >= X, token == expected Optionally parse tx input/logs for a session memo to bind proof ↔ request Basic reorg guard: wait N confirmations (configurable) Server math: Reads HF_current from LendingPool Computes needed to reach minHF (conservative estimate) Executes repayFromBuffer(user, min(needed, repayBuffer[user])) Observability: Events emitted on BufferRepay, Repay, CreditScoreUpdated API returns { ok: true, repaid } for UI toast + logs What’s hacky (but notable): Using a session UUID as a tx memo on Polygon gives us a near-stateless binding between a 402 challenge and an on-chain payment proof. We accept either native MATIC or ERC-20 USDC as “fee tokens” using a small token registry on the server to validate decimals/amounts. Core Contracts (Sepolia) — Nitty-Gritty LendingPool.sol State: collateralBalance, debtBalance, repayBuffer Config: baseLTVbps, liqThresholdBps, liquidationBonusBps, price1e18 Views: valueOfCollateral(), userMaxBorrowable(), getHealthFactor(), isUnderwater() Actions: deposit, borrow (requires verified human), repay, liquidate Buffer: depositBuffer, withdrawBuffer, repayFromBuffer, optional repayOnBehalf Events: Deposit, Borrow, Repay, Liquidate, BufferDeposit, BufferWithdraw, BufferRepay Demo-only admin: price setter for price-drop simulations (owner-only) OCCRScore.sol scoreMicro ∈ [0..1e6] (0 best); emits CreditScoreUpdated Weights (example): 35% Historical, 25% Current, 15% Util, 15% Txn, 10% New Hooks: onBorrow/onRepay/onLiquidation adjust factors Personalized LTV: baseLTV * (1 - scoreMicro/1e6) IdentityVerifier.sol isVerified(address) gate for borrow setVerified(address,bool) (owner-only) and/or setMerkleRoot / prove Emits UserVerified Implementation notes: CEI pattern (Checks-Effects-Interactions) Minimal reentrancy surface; no external calls in state-mutating functions except ERC-20 transfers Gas-savvy math: precalc liqThresholdWAD / price1e18, use unchecked where safe, emit compact events Frontend & API (Next.js 15) App Router + TS, wagmi/ethers for chain calls Env-driven config: RPCs, contract addresses, fee token/amount, indexer URL UI blocks: Position card (collateral, debt, HF) OCCR card (on-chain vs Indexed OCCR (beta)) Repay Buffer panel (approve/deposit/withdraw) Auto Top-Up toggle + threshold Identity page: “Verify with Self” → shows ✅ and unblocks Borrow /pages/api/topup.ts implements the 402 handshake and fee proof verification Agent (Node/TS) Reads env: AMOY_RPC_URL, AGENT_PRIVATE_KEY, FRONTEND_TOPUP_URL, BORROWER_ADDR Loop: Fetch HF (Sepolia RPC → getHealthFactor) If below threshold: POST /api/topup Pay x402 fee on Amoy (USDC or MATIC) POST /api/topup with proofTxHash Log outcomes (and surface errors: insufficient buffer, fee mismatch, session expired) Reliability bits: Backoff & jitter to avoid hammering endpoints Confirmations before accepting a fee proof (N configurable) Idempotency key: we include the session; server short-circuits duplicate proofs Indexer & Token API Listeners: ethers.js on Sepolia for pool/score events Store: sqlite or json with a simple DAO layer Recompute OCCR off-chain to compare with on-chain (helps tuning) Endpoints: GET /api/scores/:address → { scoreMicro, factors, updatedAt } GET /api/users/:address/position → { collateral, debt, hf, buffer, history } Frontend: shows on-chain vs indexed values + tiny sparkline charts for score/HF history What’s hacky (but smart for a hackathon): We double-compute credit score (on-chain & off-chain). It’s extra work, but it gives us debugging levers and ML-ready signals later. Tooling, Testing & DevEx Contracts: Foundry/Hardhat (unit tests for deposit→borrow→repay, buffer repay, liquidation math, score updates) Frontend/API: Vitest + Supertest for /api/topup; MSW for RPC stubs where needed Agent: End-to-end against Amoy test token (USDC) and MATIC; retries + confirmation depth tests Lint/Type-safety: eslint, typescript strict, no-explicit-any DX niceties: .env.example for each package, first-run check:rpc script that prints latest block heights on Sepolia/Amoy/Alfajores Docs: README with contract addresses, envs, scripts, and an SVG architecture diagram Partner Tech — Concrete Benefits Self Protocol (Celo) Unique human constraint unlocks safer personalized LTV and fairer incentives Smooth dev ergonomics with SDK/API; low-friction MVP via admin-attest → can evolve to full on-chain proofs Polygon x402 (Amoy) Automation as a service: tiny on-chain fee becomes a clean, auditable trigger No cross-chain asset risk: we never bridge principal—only send a fee signal Cheap & fast UX for the agent loop Edge Cases & Safeguards Fee paid, but HF already recovered: server re-reads HF and no-ops (returns {ok:true, repaid:"0"}); we plan refunds/credits later Insufficient buffer: server returns an actionable error; UI highlights “Top up your Repay Buffer” Session replay attempts: session UUID bound to single use; we also check timestamp and amount ≥ quoted Chain reorgs on Amoy: wait N confirmations before accepting proof Price oracle in demo: owner-only setter for price drops; post-hackathon we’ll add Pyth/Chainlink What’s uniquely ours Repay Buffer first: a clean, no-swap automation surface that’s hard to mess up in live demos OCCR everywhere: same formula on-chain and off-chain, making the system tunable and explainable Three-chain composition with minimal trust: credit core (Sepolia), fee signal (Amoy), identity (Celo)