emj
shrx

Lesson 3 · ~12 min · Lesson 2

How lenders discover offers

Covenants enforce rules on-chain. The indexer watches the chain and serves data to wallets and the demo UI.

Why not read the chain directly?

A wallet could scan every Liquid transaction. In practice that is slow, expensive, and error-prone. The lending indexer:

  • Recognizes protocol transactions by structure and covenant hashes
  • Maintains a database of offers, statuses, and tracked UTXOs
  • Exposes a REST API so apps list pending offers and show “your loans”

The indexer does not hold private keys or sign transactions. Wallets still build and broadcast covenant spends; the indexer catches up afterward.

Two layers of trust

LayerWhoWhat it guarantees
On-chain Simplicity covenants Only valid witness paths can move funds
Off-chain Indexer Correctly identifies and tracks protocol txs; serves queryable state

When reviewing an indexer client (e.g. HTTP bindings in a wallet library), ask: does it parse the same fields the API returns, and does it treat indexer data as hints while the chain remains authoritative?

Offer discovery and verification

Official docs (Pre-lock / parameter tokens)

From the docs, a valid offer-initialization tx must match a template (input/output counts, OP_RETURN with BIP340 pubkey, parameter UTXOs on inputs 0–1). The indexer then:

  1. Extracts parameters from the transaction
  2. Recompiles the Pre-lock covenant
  3. Compares the resulting CMR (script hash) to output 0

If they match, the tx is a genuine offer — not a lookalike with fake terms.

Reference implementation (LendingOffer)

In simplicity-lending, discovery uses LendingOffer::try_from_tx: parse OP_RETURN metadata, validate output layout, confirm covenant address. Offer creation is only attempted when the same transaction also spent a known IssuanceFactory (factory issued the role NFTs first). Detailed walkthrough →

Stale README warning The indexer README still says “PreLock” in places. The code tracks LendingOffer and pending_offer UTXOs. Trust the implementation and tests when reviewing.

Per-block pipeline

For each block, the indexer worker (block processor):

  1. Fetches block txs from Esplora
  2. For each transaction, runs trackers in order:
Per-transaction processing order Factory spends Auth NFT spends Offer UTXO spends Participant NFTs Factory creation? Offer creation? (if factory issued) Spending a tracked UTXO triggers status transition pending_offer → active | cancelled · active_offer → repaid | liquidated
Spend detection drives state updates. New offers are detected only after factory issuance in the same tx.

Status transitions (reference impl)

Spend of…Tx patternNew status
pending_offerCancellation outputscancelled
pending_offerOtherwise (acceptance)active
active_offerRepayment outputs (5+ outs, pattern)repaid
active_offerOtherwiseliquidated
repayment vaultLender claimclaimed

Full status list and API fields: Indexer API reference.

What wallets consume

The demo web app reads the indexer for listings and details, then uses LWK to sign covenant transactions. Typical flow:

  1. GET /offers?status=pending — lender browses open offers
  2. GET /offers/{id} — fetch NFT asset ids, UTXOs, amounts for tx building
  3. GET /borrowers/offers?script_pubkey=… — borrower sees their loans
  4. GET /factories/by-script?script_pubkey=… — borrower finds factory before creating offer

Interest is quoted in basis points (1000 = 10%). Amounts are decimal satoshi strings.

Review lens An HTTP client wrapper should map API types faithfully (status enum, asset hex, factory UUID, expiration height). It should not re-derive covenant logic — that stays in the contracts layer.

Check your understanding

1. What verifies a new offer creation tx is genuine?

2. When does the indexer try to index a new offer?

3. Spending a pending-offer UTXO updates status to?

Ask your teacher
Try: “what’s the difference between repaid and claimed?” or “how would I test an indexer client without mainnet?”