Back to the catalog

Utuh watcher

Sweep bonded completeness claims on Creditcoin against Ethereum, and refute one that omits an event.

Open source Open in the app JSON README (API)

About

Sweep bonded completeness claims on Creditcoin against Ethereum, and refute one that omits an event.

Details

Kind
MCP servers
Topic
Finance & crypto
Publisher
pugarhuda
Origin
official
Category
ferramentas
Transport
local
Version
0.3.0
Last push
2026-09-07T17:07:47Z
Repository state
ativo
Language
TypeScript
License
MIT
Added
2026-09-07 16:03:58
Updated
2026-09-07 16:03:58
Origin id
io.github.PugarHuda/utuh-mcp

README

# Utuh

**A completeness layer for the Attestcoin Protocol, and undercollateralized credit built on it.**

_utuh_ — Indonesian: whole, intact, with nothing missing.

Any registry on this protocol can tell you what it holds. Ask one whether a borrower has ever been
liquidated and it can only answer with what someone chose to submit — and the borrower will not be
submitting that. Utuh bonds the claim that a set is all of them, and pays half the bond to anyone
who proves one event was left out.

Built for BUIDL CTC 2026 Fall on Creditcoin.

**Technical brief:** https://claude.ai/code/artifact/2caca05b-c659-463f-b5ca-28e207f95147

Deployed and verified on Creditcoin CC3 Testnet. **[The console is live at
utuh.vercel.app](https://utuh.vercel.app/)** — it reads the chain from your own
browser, lets anyone sweep Ethereum and break an incomplete claim, and lets a borrower be
underwritten end to end without cloning anything. `npm run web` runs the same page locally.

Building something else on Creditcoin that needs a sentence about events that did _not_ happen?
The registry is usable on its own — see **[docs/INTEGRATING.md](docs/INTEGRATING.md)**.

---

## The problem

The Attestcoin Protocol proves that a source-chain transaction _happened_. A Merkle proof places
the transaction in a block; a continuity proof anchors that block to an attestation stored on
Creditcoin. The Block Prover precompile at `0x0FD2` checks both natively, synchronously, inside a
single Creditcoin block.

What it cannot prove is that a _set_ of events is complete.

Whoever submits proofs chooses which proofs to submit. Every one of them verifies. Nothing in the
protocol notices the ones that were left out.

For credit, that gap is fatal. The sentence every on-chain credit system needs is:

> _This borrower has never been liquidated._

That is a statement about events which do not exist, and an inclusion proof can only ever speak
about events which do. A borrower assembles their own history, submits the flattering half, and
each proof checks out.

This is not hypothetical. Season 1 of this hackathon drew 76 submissions, and more than twenty of
them were some form of on-chain credit score or reputation-based lending; one of those, CreditX,
took a prize. Every one of them inherits this hole, the winner included — placing well is not the
same as closing it, and an inclusion proof gives none of them a way to.

## What this uses of Creditcoin's, and what it deliberately does not

`npm run doctor` ends by making a live request to every Creditcoin-owned surface this depends on,
so the list below is checkable rather than asserted. Today's run:

```
Creditcoin ecosystem surfaces
  ok    USC Oracle dashboard      200 — every verifyAndEmit here is listed by source height
  ok    testnet indexer           68,160 attestations of Ethereum, chain key 3
  ok    mainnet indexer           52,463 attestations of Ethereum, chain key 1
  ok    Creditcoin Mainnet RPC    chain id 102030
  ok    Blockscout · registry     UtuhRegistry verified
  ok    Sourcify · registry       chain 102031: match match, creation match
  ok    Blockscout · credit       UtuhCredit verified
  ok    Sourcify · credit         chain 102031: match match, creation match
```

On top of the protocol itself — both precompiles, both `verifyAndEmit` overloads, `EvmV1Decoder`,
`@gluwa/usc-contracts` and `@gluwa/usc-sdk`, the hosted Proof Builder under both of its hostnames,
and `RawProofBuilder` as the path that needs no hosted service at all.

Two of those lines are worth reading twice. Every append and every refutation goes through
`verifyAndEmit` rather than its `view` twin, so **Creditcoin's own oracle dashboard is the record**
— the network logged this project's work, not the other way round. And the **Creditcoin Mainnet**
indexer is read even though nothing here is deployed there: the console audits mainnet's attestors
against Ethereum itself, because auditing only the network you deployed to leaves the production
oracle unchecked by the one page that can check it.

### What is deliberately not integrated

PenguinBridge, PenguinSwap and PenguinBase are Creditcoin's own consumer applications and none of
them appears here. That is a design decision, not an oversight:

- **A bridge would contradict the product.** The whole claim is that nothing bridges — history
  stays on Ethereum, credit is issued on Creditcoin, and only proof crosses. Wiring a bridge in
  would make the pitch untrue.
- **A DEX price would reintroduce the oracle.** Crossing from a source asset to CTC is a price,
  and the lender states its own rate on-chain precisely so the protocol never pretends to know
  one. Reading a swap rate would replace a stated assumption with a hidden one.

An integration that weakens the thesis is not depth. Naming the two we refused is more honest than
listing eleven we bolted on.

## Where this sits next to Creditcoin's own example

Creditcoin ships a [loan-flow tutorial](https://github.com/gluwa/attestcoin-protocol-examples/tree/main/loan)
— `ASCLoanManager` on Creditcoin, an auxiliary contract on Sepolia, an offchain worker between
them. (Mid-season, gluwa reorganised that repo and began renaming USC to ASC — Attestcoin Smart
Contracts; the old `USCLoanManager` link now redirects to a 404, and everything below was re-read
against the renamed sources on 2026-09-04. The mechanics are unchanged.) It is a good tutorial and
it is the right shape for what it teaches. It is also a precise illustration of the gap, because it
is the reference every builder will start from.

Each event proves itself as it happens. `_markLoanAsFunded` and `_noteLoanRepayment` take one
proven transaction each; `ASCBase.execute` verifies it through `0x0FD2` and records the query so
it cannot be replayed. Every _present_ fact is cryptographic, and that part is sound.

Two things follow from proving one event at a time, and neither is a defect in the tutorial:

- Nothing asks whether the set is complete. That is fine when the loan is already registered on
  chain and the contract knows exactly which events it is waiting for. It stops being fine the
  moment the question is _"has this borrower ever been liquidated"_ — because that question is
  about events nobody submitted, and no number of inclusion proofs answers it.
- Default is declared, not proven. `markLoanAsExpired` is `onlyOwner`. Somebody trusted says the
  loan went bad. For a tutorial that is the honest simplification; for underwriting a stranger it
  is the whole problem moved one layer up.

Utuh is the layer that would sit under such a contract: a bonded claim that a set of events is
_all_ of them, refutable by anyone with one proof of one omitted event. Presence stays
cryptographic exactly as above. Absence becomes economic, which is the most that can be had.

## What Utuh does

Two halves, each sound on its own.

**Nothing invented.** Every event enters a claim through `appendBatch`, which runs the Attestcoin
Block Prover on it before it is recorded. Members must arrive in strictly ascending
`(blockHeight, txIndex, logIndex)` order, which the contract enforces rather than trusts. A claim
can only ever contain events that provably happened, so its aggregate cannot be inflated.

**Nothing omitted.** The claimant bonds the assertion that the set is complete. Anyone may break
the claim by proving a single in-scope event the set does not contain. Absence is never proven — a
claim of absence is _refuted by presence_, which Attestcoin does prove.

```
presence  →  cryptographic   (Merkle + continuity, verified by 0x0FD2)
absence   →  economic        (bonded assertion, refutable by one proof)
```

### What scales, and what does not

_Settling_ a claim is O(1): the registry never verifies a whole set, so a claim spanning ten
thousand events is broken by a single proof or by none at all.

_Building_ one is not. `npm run gas` measures it rather than reasoning about it — it finds every
transaction a registry has ever seen from the registry's own logs, reads the receipts, and fits a
cost model. No explorer involved. Across the four registries deployed so far, 139 transactions:

| Call                      | Gas (mean) | % of a 75M block |
| ------------------------- | ---------- | ---------------- |
| `open`                    | 252,750    | 0.33%            |
| `seal`                    | 206,010    | 0.27%            |
| `appendBatch` (1 event)   | 552,956    | 0.73%            |
| `appendBatch` (2 events)  | 893,903    | 1.19%            |
| `appendBatch` (10 events) | 2,662,045  | 3.54%            |
| `refute`                  | 611,556    | 0.81%            |
| `finalize`                | 209,258    | 0.27%            |
| `withdraw`                | 205,870    | 0.27%            |

Member count alone does not explain those. One append of **three** events cost 541,464 gas while
an append of **two** cost 878,903, because the cost follows the _size of the transactions being
proven_, not how many events sit inside them. A least-squares fit over all 56 appends the published
registries have seen, against the call's own calldata gas and its member count:

```
  290,899 gas fixed
    1.51 x the call's own calldata gas   (1.00 would be exact)
   81,427 gas per member on top of its bytes
  worst residual 294,878 gas, 32% of the mean append

each append carried median 94, 2..394 continuity hashes and median 16, 8..78 Merkle siblings
  charging the bytes at their own EVM price, 62 gas per continuity hash:
  5,866 gas at the median, 0.6% of the mean append
```

The calldata term is the solid one, and it is the interesting one: **a proven transaction costs
about twice its own calldata gas**, because those bytes are not merely paid for at the door — they
are copied, RLP-decoded by `EvmV1Decoder`, and hashed by the Block Prover. Proving one in-scope
log inside a fat mainnet transaction means carrying all thirty kilobytes of it, and that is not a
choice the claimant has.

The per-member term is **not** well determined, and it is worth saying so rather than quoting it.
An earlier fit over 25 appends put it at 20,526 gas — almost exactly a cold `SSTORE`, which was a
satisfying number and the reason to distrust it. Eleven more appends moved it to 61,265, and twenty
more to 81,427 while the calldata multiplier fell from 2.3× to 1.5× — the two terms trading against
each other, which is what collinear regressors do. Members
and bytes are correlated in this data (more members generally means more bytes), so separating the
two needs appends this repo has not made: many members with small transactions, and few with large
ones. What the data does support is the shape — fixed cost, a dominant per-byte cost, and some
per-member cost on top — not a precise value for the last of those.

The obvious missing term is the continuity proof, and it is instructive that it stays out. The
protocol prices verification almost entirely by it: the published figure is `2.3e-5 + 2.9e-7 ×
hashes` CTC, so at the median proof here the hashes are more than half of what the precompile
charges. A fourth regressor for them fits better — worst residual 32% of a mean append down to
16% — and reports **minus 860 gas a hash**. A hashing operation cannot pay you. Every hash is 32
bytes of the same calldata the second term already charges for, so given nothing of its own to
explain it trades against that term instead, dragging it to 2.21×; reparameterised until both
coefficients are positive it prices a root byte at 0.53×, under the floor a byte can cost. So the
hashes are measured rather than fitted: hold every byte at its own EVM price, fit only what is
left, and the hashing comes out at **62 gas each** — three specifications agree to within a gas,
and a keccak of two words is 42, so the number is the operation rather than an artifact. The
median append carries 94 of them and spends 5,866 gas on them, **0.6% of itself**. The term that
dominates what the precompile charges is not the term that prices the claim.

The practical ceiling is therefore set by bytes:

```
  a 100-event claim:     10 batches,    ~24.5M gas,  0.3 full blocks of it
  a 1,000-event claim:  100 batches,   ~245.2M gas,  3.3 full blocks
  a 10,000-event claim: 1000 batches, ~2451.6M gas, 32.7 full blocks
```

The asymmetry is still the point — challenging is one proof and a binary search, whatever the claim
holds — but a claim of ten thousand events is thirty blocks' worth of gas, and that is the number
that caps this rather than any argument about storage.

### The subtle part

A challenge window is only meaningful if a watcher could actually have acted inside it. So a claim
may not open until its entire block range is already attested on Creditcoin — checked against the
ChainInfo precompile at `0x0FD3`:

```solidity
if (!CHAIN_INFO.is_height_attested(scope.chainKey, toBlock)) revert RangeNotAttested(...);
```

Without that gate, a claimant could cover a range whose tail is not yet attested, and the window
would expire on a claim nobody was _able_ to refute. Attestation heights only advance, so once
`toBlock` is attested the whole range stays provable for the life of the claim.

A second detail: a refuter receives half the slashed bond, not all of it. If they took the whole
bond, a claimant caught lying could refute their own claim and walk away whole, which would make a
false claim free to attempt. The burned remainder is what puts a price on being wrong.

That price is smaller than the bond, and the difference matters. A claimant knows which event they
omitted from the moment they seal, so they can watch for an incoming refutation and send their own
from a second address, taking the refuter's share back. No ordering scheme closes this — an
earlier draft of these notes claimed commit-reveal would, which was wrong: the claimant holds the
private knowledge, so they simply commit first. What survives is the burn, which nobody can
recover.

So the registry reports `enforceableLoss` rather than the bond, and `isUsable` measures exposure
against that. Sizing a line against the whole bond, as `UtuhCredit` did at first, carried twice
the exposure the deterrent actually covered.

## Built on Ethereum mainnet, not Sepolia

CC3 Testnet attests **Ethereum mainnet** (`chainKey 3`) alongside Sepolia (`chainKey 1`), from
genesis height 0. Verified live:

```
$ npm run balance
attested   sepolia  chainKey 1  height 11530210
attested   mainnet  chainKey 3  height 25797540
```

The mainnet frontier tracks within roughly a hundred blocks of the real chain head. So contracts
on a free testnet can be underwritten on real Aave positions, real USDC flows, and real borrowers,
with no capital at risk and nothing simulated.

Budget for the attestation lag when running anything live: a freshly mined block takes on the
order of ten minutes to become provable on either chain. Historical blocks are immediate, and a
proof for one 210,000 blocks back still resolves in about seven seconds — it is only the tip that
you wait on.

Every number in the demos comes from Ethereum mainnet.

## UtuhCredit

An undercollateralized credit line on Creditcoin, underwritten on Ethereum. Nothing bridges: the
history stays on Ethereum, the credit is issued in CTC on Creditcoin, repayment happens back on
Ethereum. The only thing that crosses is proof.

Underwriting rests on two claims that are adversarial in **opposite** directions, which is what
makes the pair sound:

| Claim      | Assertion                                    | Who benefits from a lie | Defence                                     |
| ---------- | -------------------------------------------- | ----------------------- | ------------------------------------------- |
| **Volume** | proven Aave USDC repayments                  | inflating it            | every member verified by `0x0FD2` on append |
| **Clean**  | complete set of liquidations, normally empty | omitting one            | bond, refutable by one liquidation proof    |

```
limit = min( 20% of proven volume × the lender's rate , 10 × the bond behind the clean claim )
```

The second term is the consumer half of the mechanism. The registry cannot size a bond, because it
does not know what the claim will be used for — only the party about to lend knows its own
exposure. A line never risks more than a liar stood to lose.

### Two places where units have to be taken seriously

Aave's `Repay` carries `amount` in the reserve asset's own decimals. Scoping a volume claim to the
event alone would sum WETH's 18 decimals into USDC's 6 and call the total a credit history. So the
volume scope **pins the reserve** — `Repay` puts it in topic 1 — and a claim is denominated in
exactly one asset.

That leaves a second gap: the claim aggregates USDC at 1e6, a line is CTC at 1e18, and crossing
between them is a price. This contract has no oracle and does not pretend to: the lender fixes
`VOLUME_UNIT_IN_CTC` at deployment, in the open, where anyone can judge it. A lender wanting a live
price puts a feed in front of this contract rather than having the protocol invent one.

### Reading a history is not the same as owning it

Underwriting reads a public chain. Nothing about reading it proves the reader holds the key that
wrote it, so before a line opens the borrower must bind their Ethereum address to their Creditcoin
account:

```
calldata = bytes12("utuh:control") || <creditcoin account>
```

One ordinary transaction from the subject address carrying exactly that. `proveControl` verifies
it through the Block Prover and reads the sender out of the decoded transaction — no signature
scheme of our own, no trusted relayer. The tag stops the commitment colliding with real calldata,
and naming the account inside it stops anyone binding a stranger's address to their own account.

**Each commitment may be applied once**, and that is not bookkeeping. A subject can move their
binding by sending a second commitment naming a different account — which is how anyone rotates
away from a Creditcoin account they no longer control. The proof of the _first_ commitment stays
valid forever, and anyone may submit it. Without a used-marker the binding is therefore whichever
proof was replayed most recently, not whichever the subject meant: an attacker holding the
rotated-away account puts it back at will, including in front of the subject's own `openLine`.
Creditcoin's own `USCBase` records processed queries for exactly this reason, and this did not
until it was found. `controlIdOf` keys on the chain and the encoded transaction — which carries
its own signature — and deliberately not on the block height, so a reorg that moved the same
transaction cannot make the same commitment usable twice. `npm run control` and `npm run full`
both replay the commitment they just used and require `ControlProofAlreadyUsed`.

Any supported source chain will do: an EOA address derives from its public key and is identical on
all of them, so Sepolia gas proves exactly as much as mainnet gas.

The terms of a draw are the lender's, never the borrower's. `draw` takes an amount and nothing
else; what must come back and by when are computed from policy, converting CTC back through the
same rate that produced the limit and rounding up so no draw is small enough to owe nothing.

Two things about the money are worth stating because both were wrong once. Every rounding in the
contract lands against the party carrying the risk: `_repaymentFor` rounds up so no draw is small
enough to owe nothing, and `backingFor` rounds up so no limit is backed by less than a
`BOND_MULTIPLE`th of itself. That second one is asked at both ends of a line — when it opens and
when it settles — and it was a bare division at both until it was not.

And a lender can name where its own capital goes. `LENDER` is `msg.sender` at construction and
immutable, so a lender that is a contract without a payable fallback could `fund` this and never
get the money back out: `withdraw` would revert with `TransferFailed` forever. `withdrawTo` is the
way out. The authority check is unchanged — only the lender may call it.

Three smaller rules close the same class of hole. A finalized claim is **spent** when it opens a
line, so one underwriting funds one line and the cap bounds aggregate exposure rather than each
line separately. A line's deadline is fixed by its first draw and never moves — otherwise a
borrower who owes money could buy an unlimited extension by drawing one more wei. And each
settlement consumes the source-chain range it rests on, tracked per subject in `settledThrough`,
because marking a _claim_ spent does not stop a _payment_ being spent twice: two lines, two claims
over overlapping ranges, one transfer inside both.

### One history, one line

A finalized claim can only open one line: `openLine` marks it spent. That is not enough on its own,
and for a while nothing else was.

The registry will hold any number of claims over the same range with the same scope, and finalizing
one gives the bond back. So a borrower could build a second claim over the same three repayments,
finalize it, open a second line, and draw the limit again — every guard in `openLine` passing each
time, because each of them looks at one line in isolation. The bond cap bounded each line and
nothing bounded the total.

`underwrittenThrough[subject]` is the fix, and it is the same shape as the `settledThrough`
watermark that already stopped one payment discharging two debts. Opening a line consumes the range
it rests on. Borrowing again means new history: a range starting after the last one, still
`MIN_HISTORY_BLOCKS` long, still inside `MAX_STALENESS_BLOCKS` of the frontier. A credit line that
renews on performance, rather than a number that can be spent twice.

### One line at a time, and why that is a rule rather than tidiness

`markDefault` is permissionless and nobody is paid to call it. That was fine while it only wrote a
status, and stopped being fine the moment a standing default started blocking new lines: a borrower
whose deadline passed could wait, accumulate a fresh month of history, satisfy
`underwrittenThrough`, and open the next line with the first one still sitting there overdue and
unmarked. The guard was resting on a transaction nobody was obliged to send.

`activeLineOf[subject]` removes the dependency. A subject has one line at a time; an overdue line is
still `Active`, so it blocks by itself, and `markDefault` goes back to being bookkeeping. Each guard
then has exactly one job — the slot says _you have a line open_, the count says _you failed one_.

The rule needs an exit, or it is a trap. An undrawn line cannot be settled (nothing was borrowed)
and cannot be defaulted (`markDefault` refuses a `drawn` of zero, correctly — no money went out, so
nothing was missed), so `closeLine` gives the slot back. It does not give the history back:
`underwrittenThrough` has already moved, and it should have.

### Whose books you take

`defaultsOf` belongs to one deployment. A borrower who walks away from a line here opens one at the
lender next door with nothing in the way, and that is the gap a credit bureau fills.

The tempting shape is a shared contract everyone reports to. It did not survive being designed.
Reports have to be trusted, and a registry anyone may write to is a blacklist with extra steps —
deploy a contract, report a rival's borrower as a defaulter, done. Every fix for that is a
permission, and a permissioned bureau is the centralised thing this whole repository exists to
avoid.

So there is no bureau. A lender names the peers whose word it takes, in its constructor, and the
answer is _pulled_ from the peer's own storage — where the fact was recorded by the contract that
actually extended the credit. No reports, no writes, nothing to forge: a peer can only ever say what
happened on its own books, and the worst a hostile one can do is refuse credit it was never going to
extend. A lender that names nobody is unaffected by everyone, which is the safe default and has to
be a choice rather than an accident.

### Default without proving a negative, and the way back

A drawn line is settled by the borrower proving repayment landed at the lender's Ethereum address.
If no finalized claim arrives before the deadline, the line defaults. The contract never
establishes that a payment was missed — the burden sits with the only party who could discharge
it. Silence is the default condition, not an inference.

A default that costs nothing but the line it happened on is not a credit event, though, and that is
what it used to be: `markDefault` set a status and the borrower opened the next line the same block
on a later slice of history. `defaultsOf[subject]` counts defaults that still stand, and `openLine`
refuses while any do.

`cure` is the way back. The borrower proves the repayment late, on exactly the terms it was owed —
same scope, same watermark, same backing, same amount, every check `settle` makes, sharing one
function with it so the cheaper path cannot drift into existence. The line becomes `Settled`, the
count comes down, and the subject can borrow again on history it has not already spent. Nothing is
forgiven for being late; the deadline has already done its work, which was to record the default
while it stood.

That distinction is what separates a credit protocol from a blacklist, and it costs one counter.

## Deployed on CC3 Testnet (chain id 102031)

Every contract below is **verified on Blockscout** — source, ABI and decoded constructor arguments
are readable at its address. An unverified address is a wall of bytecode, and "the source is on
GitHub" is a different claim from "this address runs that source". `npm run verify` republishes
them after a redeploy, reading whichever record `DEPLOYMENTS` names — so
`DEPLOYMENTS=deployments.full.json npm run verify` covers the Sepolia-sourced set including its
ledger, with nothing reconstructed by hand.

Deploying refuses to overwrite an existing record without `REDEPLOY=1`. The addresses below are
the ones in it, all verified; `npm run demo` used to begin by replacing them, so following this
file to record a demonstration quietly made everything published about them false.

Blockscout reports a _partial_ match: the runtime bytecode agrees and the trailing metadata hash
does not, which is what happens when the compilation environment is not reproduced byte for byte.
The code is readable and the functions are callable.

[Sourcify](https://sourcify.dev) disagrees, in the right direction. It compares the metadata hash
too, and reports every contract below as a **full match** — `exact_match` for the decoder and the
Sepolia ledger, `match` for the registry and both credits — from a tree it read itself. `npm run
verify` now submits to both, because two verifiers that do not share a backend agreeing on the same
source is a stronger sentence than one, and because Blockscout forwarding to Sourcify is a thing it
usually does rather than a thing to rely on: the Sepolia ledger had not arrived until it was sent.
`repo.sourcify.dev/102031/<address>` has the sources.

### Mainnet-sourced deployment

| Contract       | Address                                                                                                                                                   |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `UtuhRegistry` | [`0x8FA0BD5301D998Be873E31453E53d114929a5Fac`](https://creditcoin-testnet.blockscout.com/address/0x8FA0BD5301D998Be873E31453E53d114929a5Fac?tab=contract) |
| `UtuhCredit`   | [`0x89FB81b1e453b7Bd18ac1A6AF03C84A40Ce10C57`](https://creditcoin-testnet.blockscout.com/address/0x89FB81b1e453b7Bd18ac1A6AF03C84A40Ce10C57?tab=contract) |
| `EvmV1Decoder` | [`0x5cab00c032D7d4436f312Dd51ef59Dc5b860df3F`](https://creditcoin-testnet.blockscout.com/address/0x5cab00c032D7d4436f312Dd51ef59Dc5b860df3F?tab=contract) |

`npm run credit` runs against these, on Ethereum mainnet data.

### Sepolia-sourced deployment — the completed loop

| Contract                     | Address                                                                                                                                                   |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `UtuhRegistry`               | [`0x26880c8980Cd54827543bD34c6c613253c69347b`](https://creditcoin-testnet.blockscout.com/address/0x26880c8980Cd54827543bD34c6c613253c69347b?tab=contract) |
| `UtuhCredit`                 | [`0x0177aDb82152c8673a85271F7F06336B820324b6`](https://creditcoin-testnet.blockscout.com/address/0x0177aDb82152c8673a85271F7F06336B820324b6?tab=contract) |
| `EvmV1Decoder`               | [`0x084c45552A6c45C7269F4a7041E757ABf4Bcc008`](https://creditcoin-testnet.blockscout.com/address/0x084c45552A6c45C7269F4a7041E757ABf4Bcc008?tab=contract) |
| `SettlementLedger` (Sepolia) | [`0xC8C9053C4E2c0590df684c12e5f2610EFeC9575B`](https://eth-sepolia.blockscout.com/address/0xC8C9053C4E2c0590df684c12e5f2610EFeC9575B?tab=contract)        |

Everything below is readable at those addresses rather than taken on trust — `claim(id)`,
`memberCount(id)`, `keyAt(id, i)`, `enforceableLoss(id)`, `line(1)`, `underwrittenThrough(subject)`
and `settledThrough(subject)` all answer to anyone, and the console at `npm run web` shows them
without a terminal.

| Read                                | Answer                                                                                                                                        |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `claim(1)`                          | Finalized, 3 members, aggregate 0.003 ETH of proven volume                                                                                    |
| `keyAt(1, 0..2)`                    | Sepolia blocks 11575883, 11575885, 11575886                                                                                                   |
| `claim(2)`                          | Finalized, 0 members — the clean claim, and there is nothing to show                                                                          |
| `claim(3)`                          | Refuted, `enforceableLoss` collapsed to 0                                                                                                     |
| `claim(4)`                          | Finalized, repayment of 0.000525 ETH                                                                                                          |
| `claim(5)`                          | Refuted — planted short by one, and broken from a browser                                                                                     |
| `burned()`                          | 2 CTC, two refuted claimants' halves that nobody collected                                                                                    |
| `line(1)`                           | Settled, limit 10 CTC, drawn 10 CTC, `repayRequired` 525000000000000                                                                          |
| `underwrittenThrough(borrower)`     | 11575891 — one past the range that opened the line                                                                                            |
| `settledThrough(borrower)`          | 11575986 — one past the range that discharged it                                                                                              |
| `defaultsOf(borrower)`              | 0                                                                                                                                             |
| `activeLineOf(borrower)`            | 0 — the line settled, so the slot is back                                                                                                     |
| `claim(6)`, `claim(7)`              | Finalized — 3 members and 0, built **from the browser** by a second borrower                                                                  |
| `line(2)`                           | **Settled** — opened, drawn, repaid and settled entirely from the console                                                                     |
| `claim(8)`                          | Finalized, 1 member — the repayment; opened by one run, resumed by the next                                                                   |
| `claim(9)`, `claim(10)`             | Finalized, unspent, useless — a 4-block range against a 5-block floor; the                                                                    |
|                                     | page now refuses that before a bond is posted                                                                                                 |
| `claim(11)`, `claim(12)`, `line(3)` | The second underwriting of the same key, on later history — line 3 Settled — the whole loop, twice, from the page (claim 13 is its repayment) |
| `peerCount()`                       | 0 — this lender takes nobody else's books, which is the safe default                                                                          |

The borrower's sweep read `publicnode=3  tenderly=3`: two independent endpoints agreeing, and the
claim built on the union rather than on whichever answered first.

Lines 2 and 3 belong to `0x0C2ffE823f1b64c975D768c9822F31eFED6f6a83`, a key that has never run a
script here. It paid the lender three times on Sepolia and then did everything else **through the
page** — sent its control commitment (the wallet switched to Sepolia and back), proved it, built
claims 6 and 7, waited out their windows, finalized, opened line 2, drew 1 CTC, paid the lender back
through the ledger with the same wallet, built claim 8 over that payment, waited out that window,
and settled — as `web/tests/borrow.live.spec.ts`, against these contracts. Then it did it again on
later history, for line 3. The limit is 5 CTC and not the 12 the volume would justify, because it
posted the 1 CTC minimum bond and a 1 CTC bond guarantees a 0.5 CTC loss.

Claim 5 is the interesting one. `npm run bait` sealed it deliberately short by one event and told
nobody. It was found and broken **from the console**, in a browser: the page swept Sepolia across
two endpoints (`publicnode=4  tenderly=4`), checked all four against the claim on-chain, found the
one it omitted, fetched a proof from the hosted builder and sent the refutation itself. That path
is a Playwright test — `UTUH_LIVE_UI=1 npm run web:test -- refute.live` — so it is a thing that is
checked rather than a thing that was done once.

Three figures there are the mechanism, not decoration. The limit is **10 CTC** —
`enforceableLoss` of 1 CTC times a `BOND_MULTIPLE` of 10 — and not the 12 CTC the 0.003 ETH of
volume alone would justify, because a 2 CTC bond only guarantees a 1 CTC loss and it is the
guarantee that lends. 0.000525 ETH is what drawing 10 CTC obliges at the lender's rate and 105%
terms; the borrower had no say in the figure. And the two watermarks now read one past the ranges
they consumed, which is what stops the same history opening a second line and the same payment
discharging a second debt.

`test/Lifecycle.t.sol` reproduces all three of those numbers locally, from the fixture's own
amount and the deployed policy, so they are a property of the code rather than of that afternoon.

**A default, and the way back.** `DEPLOYMENTS=deployments.full.json npm run cure` deploys a second
UtuhCredit over the same registry with a five-minute repayment window — the recorded run left one
at [`0x509fab6a2Fd8C1a50dAB8C05cD7C7e53cB29868f`](https://creditcoin-testnet.blockscout.com/address/0x509fab6a2Fd8C1a50dAB8C05cD7C7e53cB29868f) — and underwrites the same
borrower on the same finalized claims, draws, lets the deadline pass, is marked in default — and
then makes it good with the repayment claim the loop already finalized. Claims belong to the
registry and `claimSpent` belongs to the credit contract, which is why that costs one deployment
and one Sepolia transaction rather than a second loop.

**An earlier run, at earlier addresses, did not finish in one go** — twice — and both interruptions
are worth recording because the chain absorbed them. The first: the process died during the long
wait for Sepolia's attestation frontier to reach the repayment block. Nothing was lost, because
nothing was being held in the script; `npm run finish -- <registry> <credit> 1` read the line's
state off the chain and closed it. The second was mine. I stopped a resume that looked stuck and
started another, and the first was still running: two processes built the same repayment claim, both
holding the identical event. Nothing broke, because a settlement consumes both the claim and the
source-chain range it rests on — the duplicate could not settle the same line, and the watermark
meant that payment could not settle any other. It cost its author a bond locked until its own window
closed, and nothing else. Two guards written for a lying claimant turned out to cover a clumsy
honest one too.

## Two demonstrations, and why there are two

`npm run credit` reads **Ethereum mainnet**: real Aave positions, real liquidations, real
borrowers. It underwrites them, refutes a genuinely liquidated address that claims it was never
liquidated — and then stops, because nobody can prove control of a stranger's address. That
refusal is the honest end of that flow.

`npm run full` closes the loop instead. A borrower we control acts on **Sepolia**: they pay a
lender through `SettlementLedger`, bind their address with a control commitment, get underwritten
on what they actually did, draw CTC on Creditcoin, repay on Sepolia, and settle. Two parties, both
transacting for themselves.

The source-chain contract is not a stand-in for anything under test. The payments are real
transfers, the events are real logs in real blocks, and Creditcoin attests them exactly as it
attests Aave's. A scope is a scope — the registry cannot tell the difference, and does not need
to.

## The watcher as an MCP server, and why an agent can hold the role

```bash
npx utuh-mcp    # published on npm — no clone, no key, no build
npm run mcp     # the same server from this repo, for hacking on it
```

```json
{ "mcpServers": { "utuh": { "command": "npx", "args": ["-y", "utuh-mcp"] } } }
```

Listed in the official [MCP Registry](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.PugarHuda/utuh-mcp)
as `io.github.PugarHuda/utuh-mcp`, so a client that does not know the package name can still find
it. The registry hosts metadata only; it proves ownership by fetching the published tarball and
requiring its `mcpName` to equal the server name being claimed, which `offchain/build-mcp.ts`
writes. `.github/workflows/mcp-registry.yml` publishes the listing from CI over GitHub OIDC — no
secret to store — after checking that npm really serves the version being announced.

Every guarantee here rests on *anyone may refute a claim by proving one in-scope event it left
out*, and until now "anyone" meant a person: at a console, or at the published page. `offchain/mcp.ts`
puts the same three verbs — look, sweep, refute — behind the Model Context Protocol, which makes
the watcher a role an AI agent can hold. An agent needs no account and no capital to look, and the
one that finds an omission is paid half the bond for proving it: a business model that fits inside
a tool call.

Five tools: `tally`, `list_claims`, `sweep_claim`, `refute_claim`, `audit_attestors`. None of them
is new machinery — each is the same `offchain/lib` function the daemon and the browser console
already run, because an MCP server with its own logic would be a third implementation waiting to
drift, and "agents can enforce completeness" is only credible if agents run the code that
demonstrably does.

Tools are only one of the protocol's three primitives, and a server that stops there hands a model
prose it has to re-read every turn. Claims are also **resources** — `utuh://tally`,
`utuh://claims/{deployment}`, `utuh://claim/{deployment}/{id}` — live JSON a client can attach and
re-read, carrying the scope a refuter needs and a `refutable` flag that answers the only question
worth asking first. The watcher's job is a **prompt**, `hold_the_watcher_role`: sweep everything
still inside its window, treat "no gap found" as provenance rather than proof, and bring a finding
back rather than spending. And every tool carries its **annotations**, so a client can tell the four
that only read from the one that sends a transaction and slashes somebody's bond — the confirmation
belongs on exactly one of the five, and now the server says which.

`npm run mcp:test` builds that bundle and speaks the protocol to it — the three listings, a prompt
fetched, the tally read live off Creditcoin, and `refute_claim` asked to spend with `confirm`
withheld, which must refuse. Twenty-nine assertions, no key, and CI runs it on every push: what
reaches a user is an esbuild bundle with the registry ABI and both deployment records baked in, and
every one of those is a thing that can quietly stop being included.

Two things about it were earned rather than designed. The first client ever connected to this
server found the gap in a standing claim and refuted it — a real transaction, a real slashed bond,
1 CTC of reward — during its own smoke test. That is the pitch demonstrating itself, and it is
also why `refute_claim` now demands `confirm: true` before sending: an agent's "let me just try
the tool" must cost a deliberate second call, not a bond. And stdout is the protocol channel, so
the server reroutes every stray print to stderr up front — the SDK's attestation waiter logs
progress, and one such line in the middle of a JSON-RPC stream is a corrupted session.

## The console, and why the watcher belongs in a browser

```bash
npm run web     # http://127.0.0.1:5173
```

Everything on that page is read from CC3 Testnet as the page draws it. No server holds a key, no
indexer stands in between, and there is no seeded state to fall back on — if the chain is
unreachable the page says so rather than showing the last thing it knew. The ABIs come out of
forge's own artifacts, so a field the contract stopped having is a load failure rather than a
plausible-looking zero.

It shows four things: what Creditcoin says it can attest, read straight off `0x0FD3`; every claim
in the registry with its bond, its enforceable loss and its remaining window; the lender's policy
and every line; and a watcher.

The watcher is the part that had to exist. Every guarantee here rests on one sentence — _anyone may
refute a claim by proving one in-scope event it left out_ — and until something is actually
watching, that sentence describes a possibility rather than a fact. `npm run watch` is that
sentence made real for whoever runs a daemon with a funded key, which is a small number of people.
The console makes it true for whoever opens a page:

- it rebuilds the claim's scope from what the registry stores, trusting the claimant for nothing;
- it sweeps the source chain **from the browser**, across independent public endpoints, taking the
  union rather than a vote, and says how many answered — because "no gap found" from one endpoint
  is not the same claim as "no gap found" from two;
- it checks each event against the claim with `contains`, on chain;
- and if the claim is short, it fetches one proof from the Proof Builder and sends the refutation.

That is possible only because the pieces are CORS-open and public: `rpc.cc3-testnet.creditcoin.network`,
the source-chain endpoints, and the hosted Proof Builder all answer a browser directly. Nothing
needed to be built to make it work, and it means enforcement does not depend on anyone deploying
infrastructure. Creditcoin publishes exactly one RPC hostname, though, and it has had bad
afternoons — on 2026-09-03 its nginx spent half a day refusing large `eth_call` bodies with 413,
which the daily CI probe caught. So the page's reads fail over to Blockscout's `eth-rpc` proxy for
the same chain, the one second way onto CC3 that exists; its batch ceiling and burst rationing were
measured before being coded around (`web/chain.ts` has the numbers).

The sweep is the daemon's own function, imported rather than reimplemented — `scanScopeUnion` in
`offchain/lib/scope.ts`, bundled into the page. A browser cannot conclude that a claim is complete
on different reasoning than the daemon would.

### Both deployments, and a claim opened up

The console reads either published deployment — `?deployment=mainnet` for the one that underwrites
real Aave history, where the claims about real borrowers live, dozens of them, built by the daily
live suite and the mainnet demo. It pages through them twenty-five at a time, and a sweep of a
mainnet claim runs from the browser against Ethereum mainnet in ten-thousand-block pieces, the
size both default endpoints serve; Sepolia's are swept in five-hundred-block pieces because
publicnode stops answering past that. The daemon uses the same table, so the page and the daemon
cannot reach different verdicts by asking in different pieces.

A claim has an address. `?claim=N` opens claim N on arrival — what a post, a document or a
refuter's message points at — reaching past the first page if it has to, and the address bar
follows the picker, so the URL always says what is on screen. And a visitor with two wallets
installed is asked which: the page listens for EIP-6963 announcements and names every wallet that
answers, instead of taking whichever one grabbed `window.ethereum` last. A wallet that only does
the old thing still connects.

Selecting a claim opens it: every member decoded from the ordering key the registry stores — source
block, transaction index, log index — with the block linked into the source chain's explorer and a
pointer to Creditcoin's own oracle dashboard, where the verification the precompile emitted for it
sits by source height. The registry keeps only the key, so the block is what can be linked; the
transaction at that index inside it is the one. "Read it back yourself" is a click now.

Seventy-odd links sit between the top of that page and the one button that matters to a keyboard
user, so the first Tab lands on a skip link and Enter puts focus on the sweep itself.

### A watcher that is always on, holding nothing

"A page open in a tab is not a daemon" is under Known limits, and it stayed true after the console
shipped: the page makes refuting available to anyone and makes nobody do it. The smallest thing
that does it without being paid is `.github/workflows/watch.yml` — every hour, a dry sweep of both
published registries, the same union across independent endpoints the daemon and the console run,
and a red run if a sealed claim is short of an event. The same hour, the same workflow opens the
published console in a real browser and asks what a visitor would: it loads, it says which chain it
is on, the block it shows is this hour's, both deployments read, no pane is broken, and the page
asked its host for nothing but its own four files. Pages can serve a stale or broken build with
nothing else in CI noticing; this notices.

It holds no key. `npm run watch -- --dry` reads and never signs, so it no longer asks for one, and a
public repository can run it with nothing in its secrets. The red run is the alert: somebody sealed
a lie and nobody has taken the bond yet, and whoever reads that with a key and a few minutes is
paid half the bond to act on it. Or give it the key: a fork that sets a `WATCH_PRIVATE_KEY` secret
(any account with a little CTC for gas) runs the same hourly sweep without `--dry`, refutes what it
finds, and is paid the refuter's share — a watcher that costs a repository and nothing else.

### Borrowing from the page

The scripts could always do this, and that was the problem: being underwritten meant cloning a
repository, filling in a `.env` and running TypeScript. The Borrow pane is the same flow with the
visitor's own wallet — bind your address (it will send the control commitment on the source chain
for you, switching the wallet there and back), build the volume and clean claims, wait out their
challenge window, open the line, draw.

Nothing there is a shortcut around the protocol. The claims it builds are ordinary claims: swept
across independent endpoints, proven event by event through `0x0FD2`, bonded, sealed, refutable by
the watcher in the pane above, and finalized only once the window has actually elapsed. The claim
ids are kept in the browser's own storage, so closing the tab during a window costs nothing —
which matters, because a window is measured in blocks and nobody is going to sit and watch one.

The loop closes in the page too. Step 5 reads what a drawn line owes off the contract — source
units, deadline, the exact event and payee a repayment claim must contain — pays the lender through
the source-chain ledger with the same wallet when that is how the lender is paid, builds the
repayment claim, waits out its window, and settles. A line in default is cured the same way, on the
terms it was owed. A line never drawn on is given back. And a claim that was opened and never
sealed — a closed tab, a rejected signature — shows up under Claims with one button that abandons
it and returns the bond, because the alternative is a borrower who does not know the money is
there.

Two more things the live test taught the page. The precompile saying a block is attested and the
hosted builder having _indexed_ it are different moments, and a proof request in the gap comes back
422 — measured on a payment the precompile had attested a minute earlier, and answered 200 by the
same builder a minute later. The page now waits on both, the way the SDK does, and treats a 422 as
"ask again" rather than as a failed build. And a build that dies after `open` — that 422 did it
once — leaves a claim Open with a bond in it; the page finds that claim again on the next press,
by its scope and claimant rather than by anything the browser remembered, and resumes it from the
last key the registry recorded. Claim 8 on the published registry is the receipt: opened by one
run, appended and sealed by the next, one bond.

The lender has controls too, shown only to the lender: fund, withdraw undrawn. And an overdue line
carries a _mark default_ button for anyone, because recording a default is permissionless, unpaid,
and — since an overdue line blocks the next one by itself — no longer something the guards depend
on. It is still the record peers read, so whoever notices may write it.

### Published without a server

`npm run web:static` bakes the ABIs and the deployment record into the page and writes four files.
There is no server in the published build at all, and the browser tests assert exactly that: the
page boots, reads the live chain, and asks its host for nothing but `index.html`, `main.js`,
`style.css` and the font they use. A GitHub Actions workflow builds it from each commit's own artifacts, so the ABI the
page carries is the ABI the contracts were compiled with.

Three more files are written beside those and never requested by the page, because they are for
other readers. `.well-known/security.txt` is RFC 9116, for a researcher who found the deployment
rather than the repository. `whitepaper.pdf` is the document. And `llms.txt` is the
[convention](https://llmstxt.org) an agent reads on arrival — Creditcoin's own Attestcoin docs
publish one, which is where this project found it. That last one is not a summary of the page: the
page is for a person, and an agent landing here needs three things in the order it needs them —
that the watcher role exists, that holding it costs nothing while it is only looking, and that
`npx utuh-mcp` lets it hold the role without a browser. A project whose argument is that a watcher
need not be a person, leaving nothing at its own URL for a machine to read, is asking one to parse
a screenshot. `static.spec.ts` checks that every tool it advertises is one the server actually has,
and that the package it points at is really published under the name it gives.

`web/tests/borrow.live.spec.ts` is the test that makes the Borrow pane a claim rather than a hope.
A fresh key — derived from the operator's, holding nothing but a little Sepolia ETH and a little
CTC — pays a lender three times on Sepolia, then, **through the page**: sends the control commitment
(the wallet is switched to Sepolia and back), proves it, builds the volume and clean claims, waits
out the challenge window, finalizes, opens a line and draws — then pays the lender back through
the ledger with the same wallet, builds the repayment claim, waits out that window too, and
settles. Every step is a real transaction against the published contracts, and the assertions read
the registry and the credit back rather than the page. It resumes: a borrower with a drawn line
open picks up at repayment, the way a person would. What stands in for MetaMask is `web/tests/wallet.ts`: a real key in the test process,
an EIP-1193 provider on the page that routes reads to the real RPC of whichever chain it is on and
hands every `eth_sendTransaction` back to be signed. It spends money and takes twenty minutes, so it
is off unless asked for — `UTUH_LIVE_UI=1 npm run web:test -- borrow.live`.

`web/tests/angles.spec.ts` is the console from the angles nobody demos, and two of them found
things. In dark mode, with contrast checked: clean. At 375px: nothing sideways, everything
reachable. From the keyboard alone: Tab to the sweep, Enter, and it sweeps. With a wallet whose
owner presses _Reject_ on every signature — a real EIP-1193 provider answering 4001 — every write
path reports the refusal and stays usable. And with `rpc.cc3-testnet.creditcoin.network`
unreachable from the browser, the page used to sit on _loading_ for as long as anyone cared to
wait, because a provider pointed at a dead endpoint retries rather than failing; it now says
Creditcoin is not answering, inside twenty seconds, and shows no number it did not just read.

`web/tests/a11y.spec.ts` runs axe over the rendered page — the real DOM with the chain's answers in
it — against the WCAG 2.x A and AA rules, and any violation fails the build by name. The console
exists so that people who would never run a daemon can still refute or borrow, and "people" is not
"sighted people with a mouse". It reports zero.

`npm run web:test` drives it in a real browser against the live chain: the chain id it reports has
to match an independent RPC call, the attestation frontier has to be past genesis, the claims it
lists have to be the ones the registry holds, and the sweep has to produce a verdict with its
provenance attached. With `UTUH_LIVE_UI=1` a further test connects a real wallet, finds a claim
that is genuinely short, and refutes it — a real transaction, verified by the real precompile,
slashing a real bond.

## Layout

```
.github/workflows/pages.yml the published console, rebuilt from each commit's own artifacts
.github/workflows/watch.yml hourly: a keyless sweep of both registries, and a browser smoke of the published console
.github/workflows/codeql.yml CodeQL over the TypeScript that builds, proves, refutes and signs
.github/dependabot.yml      weekly bumps for npm and the actions; foundry stays pinned by hand
SECURITY.md                 how to report a way to make a false claim stand
.github/workflows/ci.yml    fmt, build, tests, gas snapshot, typecheck, slither — and a daily
                            job that proves real mainnet events against the live precompile,
                            checks the hosted and local provers still agree, and reports what the
                            registry has cost. All three need no key and write nothing.
slither.config.json         which detectors are off, with the reasons next to the code
knip.json                   what counts as reachable; @gluwa/usc-contracts is imported from
                            Solidity, which a TypeScript analyser cannot see
.prettierrc.json            TypeScript formatting, enforced in CI the way forge fmt is
.gas-snapshot               committed, and CI fails if gas moves more than 5%
src/
  UtuhRegistry.sol          the completeness layer
  source/SettlementLedger.sol   deployed on the *source* chain: payments and adverse events
  UtuhCredit.sol            undercollateralized credit built on it
  lib/EventScope.sol        which events a claim covers, and how each one counts
  interfaces/IBlockProver.sol   0x0FD2 — Merkle + continuity verification
  interfaces/IChainInfo.sol     0x0FD3 — attestation frontier and coverage
docs/
  INTEGRATING.md            using UtuhRegistry from someone else's contract — the registry is
                            infrastructure, UtuhCredit is one application of it
test/
  EventScope.t.sol          the matcher, ordering key, metrics and leaf identity
  Consumer.t.sol            a thirty-line consumer that is not Utuh, compiled and tested, so the
                            claim that the registry is reusable is checked rather than asserted
  RegistryInvariant.t.sol   four actors, random sequences, and the books have to balance after
                            every move — every wei escrowed, credited or burned, nothing else
  UtuhCredit.t.sol          deployment floors, control binding, scope identity, terms, liquidity
  Lifecycle.t.sol           the whole loop locally — claim, refute, finalize, underwrite, draw,
                            settle, default, cure — on real Sepolia transaction bytes, with only
                            the two precompiles' answers substituted
  fixtures/                 two real Sepolia transactions from a recorded run, and one real
                            mainnet transaction that reverted — inclusion is not success, and the
                            check that says so had no test until there were bytes to fail it with
  SettlementLedger.t.sol    what the source-chain ledger will and will not record as a payment
  EventScopeKey.symbolic.t.sol  halmos proofs of the ordering key, over every input rather
                            than 256 samples — `npm run symbolic`
  CreditRounding.symbolic.t.sol proofs of the money roundings, and a note on the one the
                            solver could not decide — `npm run symbolic:deep`
offchain/
  deploy.ts                 deploy decoder, registry, credit
  e2e.ts                    honest claim finalized; dishonest claim refuted and slashed
  creditDemo.ts             underwrite a real Aave borrower; refute a real liquidated one
  cureDemo.ts               draw, miss the deadline, be marked in default, prove the repayment
                            late — on chain, against claims the full loop already finalized
  watch.ts                  the watcher — follows ClaimSealed, sweeps, refutes what is short
  badClaim.ts               files a deliberately incomplete claim, so the watcher has prey
  liveTest.ts               the guards unit tests cannot reach, asserted against CC3
  fullFlow.ts               the whole loop on Sepolia, borrower and lender both acting
  finishLine.ts             resume an interrupted run — the state lives on-chain, not in the script
  doctor.ts                 preflight: endpoints, both provers, precompiles, balance
  verify.ts                 publish sources to Blockscout, constructor args and all
  proveControl.ts           bind a source-chain address to a Creditcoin account
  provers.ts                the same proof hosted and locally, compared and time

More