Risk-aware lending with auto top-ups, OCCR(credit risk), and Self-verified identities
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.
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)