Reputo

Risk-aware lending with auto top-ups, OCCR(credit risk), and Self-verified identities

Reputo

Created At

ETHGlobal New Delhi

Project Description

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)

background image mobile

Join the mailing list

Get the latest news and updates