EXAMPLES


Every example below is a file in guide/examples/, run as it is on every push: each must exit without error and print what its expect: lines name. Download one and run it in an empty folder (keys go to ~/.ledgdex, or set LEDGDEX_HOME to keep them apart).

Example: First Trade: One seller bot sells licenses for model weights to one buyer bot, from start to finish, on your own computer.

Example: Marketplace: A small exchange: the organizer admits members and lists two sellers. A member buys the cheaper weights.

Example: Auction: A seller bot sells early access to new weights in an auction with secret bids. The highest bid wins.

Example: Disputes: A buyer pays but gets nothing, complains, and an independent judge orders a refund.

Example: Keys: Keep your ledger safe: a build server signs for the seller, an attacker gets into it, and a lost key gets a replacement.

Example: Publish: Putting a ledgdex online from two devices without the two ever disagreeing.

Example: Check: Find a cheat: a seller secretly rewrites its records, and a check proves it.

Example: Python API: For developers: using ledgdex from Python directly, with no files or website.

Example: Bots: Programs trading with each other, with no person involved: two buyer bots buy software licences from the cheaper of two seller bots.

Example: Robot Cleaning: Cleaning robots paid per clean by the shops of a mall, for one day.

Example: Extending: For developers: building your own report from a ledger, and adding it to your website.

Example: JavaScript: For developers: the same ledger code in JavaScript, for Node or a web page.

Example: Verify in Node: For developers: a Node program that reads a published ledger, checks it and lists what it sells.

Example: Shop Web Page: A web page that shows the offers of a seller. It reads them from the ledger and checks them in the browser of the reader.

Example: Sign in the Browser: A buyer bot with only a browser: a sealed key, a signed claim, and a seller that records it with the command line.

Smaller examples are on each topic page: Selling, Buying, Set Up a Marketplace, Checking for Tampering.

Example: First Trade

One seller bot sells licenses for model weights to one buyer bot, from start to finish, on your own computer.

Run it: In an empty folder: sh 01-first-trade.sh.

#!/bin/sh
set -eu

The seller biobot makes its ledgdex: a website (dex) with a signed record book (ledger) in it. Ledgdex keeps your keys in ~/.ledgdex.

ledgdex init biobot --name "biobot" --about "Maker of Penjointu, a neural network for knee bone density" --key biobot

Biobot offers 10 licenses for the Penjointu weights. Prices are in the smallest unit of the currency: 250000 cents is USD 2,500.00. The last command prints the id of the new offer. $OFFER keeps it.

cat > offer.json <<'JSON'
{"item": {"title": "Penjointu v1 weights", "text": "One license for each clinic."},
 "quantity": 10, "unit": "license", "currency": "USD", "price": 250000,
 "pay": [{"method": "card", "to": "https://pay.example/biobot"}]}
JSON
OFFER=$(ledgdex offer biobot offer.json | awk '{print $NF}')
echo "offer: $OFFER"

The buyer orthonalyzebot makes a ledgdex too, and claims 2 licenses. The buyer keeps a signed copy of the claim in claim.json.

ledgdex init orthonalyzebot --name "orthonalyzebot" --key orthonalyzebot
ledgdex claim orthonalyzebot biobot "$OFFER" --quantity 2 --output claim.json

Biobot collects the claim from the dex of the buyer and accepts it. Here the dex is a folder. Online, it is a website. Then the buyer keeps the signed record of the seller as a receipt. The seller cannot take back a receipt.

CLAIM=$(ledgdex record biobot --from orthonalyzebot | awk '/claim recorded/ {print $3}')
ledgdex receipt orthonalyzebot biobot

The buyer pays. The seller records the money and the delivery. The buyer tells the seller that the weights arrived. Each side signs its own step.

ledgdex pay orthonalyzebot biobot "$CLAIM" --method card --ref PAY-0001 --output paid.json
ledgdex record biobot --from orthonalyzebot
ledgdex received biobot "$CLAIM" 500000
ledgdex delivered biobot "$CLAIM" --note "weights sent by a private link"
ledgdex confirm orthonalyzebot biobot "$CLAIM" --output confirmed.json
ledgdex record biobot --from orthonalyzebot

Anyone can now verify the ledger of biobot and see the claim. The claim is closed.

ledgdex verify biobot
ledgdex state biobot | python3 -m json.tool | grep '"status"'

The whole file: guide/examples/01-first-trade.sh, run as it is on every push (py/tests/test_guide.py).

Example: Marketplace

A small exchange: the organizer admits members and lists two sellers. A member buys the cheaper weights.

Run it: In an empty folder: sh 02-marketplace.sh.

#!/bin/sh
set -eu

The organizer makes the ledgdex of the exchange. Its ledger is the list of members (the root) and the list of sellers (the index). Two sellers join and name the exchange that they trade in.

ledgdex init exchange --name "ONITO medmodel exchange" --about "Trained medical models, from bots to clinics" --key exchange
ledgdex init biobot --name "biobot" --key biobot --root exchange
ledgdex init osteobot --name "osteobot" --key osteobot --root exchange

The exchange admits both sellers and one buyer, and each seller admits the buyer too. The keys here are names of key files on this computer. Usually you paste the key text (ed25519:...).

ORTHO=$(ledgdex keygen orthonalyzebot)
ledgdex admit exchange biobot --name "biobot"
ledgdex admit exchange osteobot --name "osteobot"
ledgdex admit exchange "$ORTHO" --name "orthonalyzebot"
ledgdex admit biobot "$ORTHO" --name "orthonalyzebot"
ledgdex admit osteobot "$ORTHO" --name "orthonalyzebot"

The exchange lists both sellers, so that buyers can find them.

ledgdex list exchange biobot --note "knee bone density models"
ledgdex list exchange osteobot --note "knee bone density models, cheaper"

Both sellers offer weights for knee scans at different prices. "allow": "admitted" means that only members can buy.

printf '%s' '{"item": {"title": "Penjointu v1 weights"}, "quantity": 20, "unit": "license", "currency": "USD", "price": 250000, "allow": "admitted"}' > biobot-offer.json
printf '%s' '{"item": {"title": "Osteoscan v2 weights"}, "quantity": 20, "unit": "license", "currency": "USD", "price": 180000, "allow": "admitted"}' > osteobot-offer.json
ledgdex offer biobot biobot-offer.json
ledgdex offer osteobot osteobot-offer.json

The buyer makes a ledgdex. It reads the list of sellers of the exchange, and what each seller sells.

ledgdex init orthonalyzebot --name "orthonalyzebot" --key orthonalyzebot --root exchange
ledgdex discover exchange --offers --json > found.json
python3 -c '
import json
for l in json.load(open("found.json")):
    for o in l["offers"]:
        print(l["note"], "|", o["title"], o["currency"], o["price"], "per", o["unit"], "|", o["remaining"], "left")'

The buyer claims the cheaper weights, and that seller accepts the claim. $1 is the seller and $2 is the offer.

set -- $(python3 -c '
import json
found = json.load(open("found.json"))
best = min(((o["price"], l["url"], o["id"]) for l in found for o in l["offers"]), key=lambda x: x[0])
print(best[1], best[2])')
ledgdex claim orthonalyzebot "$1" "$2" --output claim.json
ledgdex record "$1" --from orthonalyzebot

A bot that is not a member tries the same thing. The seller rejects the claim as not_allowed.

ledgdex init unknownbot --name "unknownbot" --key unknownbot
ledgdex claim unknownbot "$1" "$2" --output unknownbot-claim.json
ledgdex record "$1" --from unknownbot

The whole file: guide/examples/02-marketplace.sh, run as it is on every push (py/tests/test_guide.py).

Example: Auction

A seller bot sells early access to new weights in an auction with secret bids. The highest bid wins.

Run it: In an empty folder: sh 03-auction.sh.

#!/bin/sh
set -eu

Two small helpers: at N gives the time N seconds from now, and wait_until T waits until just after the time T. Here the bids are open for seconds, so that the example is quick. A real auction is open for hours or days.

at() { python3 -c "import datetime as d, sys; print((d.datetime.now(d.timezone.utc) + d.timedelta(seconds=int(sys.argv[1]))).strftime('%Y-%m-%dT%H:%M:%SZ'))" "$1"; }
wait_until() { python3 -c "import datetime as d, sys, time; t = d.datetime.strptime(sys.argv[1], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=d.timezone.utc); time.sleep(max(0, (t - d.datetime.now(d.timezone.utc)).total_seconds() + 1))" "$1"; }

Biobot starts an auction for early access to the Penjointu v2 weights. Bids close in 20 seconds and reveals close in 35 seconds. The highest bid wins. Biobot accepts no bid under USD 10,000.00 (the reserve).

ledgdex init biobot --name "biobot" --key biobot
CLOSE=$(at 20); REVEAL_UNTIL=$(at 35)
cat > auction.json <<JSON
{"item": {"title": "Penjointu v2 early access", "text": "The v2 weights, 90 days before all other clinics."},
 "currency": "USD", "close": "$CLOSE", "reveal_until": "$REVEAL_UNTIL", "best": "highest", "reserve": 1000000}
JSON
AUCTION=$(ledgdex auction biobot auction.json | awk '{print $NF}')

Two clinic bots bid. Each amount stays secret on the computer of the bidder. The bid sends only a sealed hash of the amount.

ledgdex init orthonalyzebot --name "orthonalyzebot" --key orthonalyzebot
ledgdex init kneecarebot --name "kneecarebot" --key kneecarebot
ledgdex bid orthonalyzebot biobot "$AUCTION" --amount 1350000 --output orthonalyzebot-bid.json
ledgdex bid kneecarebot biobot "$AUCTION" --amount 1200000 --output kneecarebot-bid.json
ledgdex record biobot --from orthonalyzebot --from kneecarebot

After the bids close, each bidder reveals its amount.

wait_until "$CLOSE"
ledgdex reveal orthonalyzebot biobot "$AUCTION" --output orthonalyzebot-reveal.json
ledgdex reveal kneecarebot biobot "$AUCTION" --output kneecarebot-reveal.json
ledgdex record biobot --from orthonalyzebot --from kneecarebot

After the reveals close, biobot adds a note that closes the auction. Anyone can now see the winner: orthonalyzebot, at USD 13,500.00.

wait_until "$REVEAL_UNTIL"
ledgdex note biobot "$AUCTION" "Bidding is over: the state shows the result."
ledgdex state biobot | python3 -c '
import json, sys
a = list(json.load(sys.stdin)["auctions"].values())[0]
print(json.dumps({"winner": a["winner"], "amount": a["amount"], "status": a["status"]}, indent=1))'

The whole file: guide/examples/03-auction.sh, run as it is on every push (py/tests/test_guide.py).

Example: Disputes

A buyer pays but gets nothing, complains, and an independent judge orders a refund.

Run it: In an empty folder: sh 04-disputes.sh.

#!/bin/sh
set -eu

The judge (the arbiter) is the exchange, with its own ledgdex. The seller names the key of the exchange in its offer.

ledgdex init exchange --name "ONITO medmodel exchange" --key exchange
ARBITER=$(ledgdex state exchange | python3 -c 'import json,sys; print(json.load(sys.stdin)["owner"])')
ledgdex init biobot --name "biobot" --key biobot
cat > offer.json <<JSON
{"item": {"title": "Penjointu v1 weights"}, "quantity": 5, "unit": "license", "currency": "USD", "price": 250000,
 "arbiter": {"key": "$ARBITER", "url": "exchange"}}
JSON
OFFER=$(ledgdex offer biobot offer.json | awk '{print $NF}')

The buyer claims a license and pays.

ledgdex init orthonalyzebot --name "orthonalyzebot" --key orthonalyzebot
ledgdex claim orthonalyzebot biobot "$OFFER" --output claim.json
CLAIM=$(ledgdex record biobot --from orthonalyzebot | awk '/claim recorded/ {print $3}')
ledgdex pay orthonalyzebot biobot "$CLAIM" --method card --ref PAY-0555 --output paid.json
ledgdex record biobot --from orthonalyzebot

The weights do not arrive. The buyer opens a dispute, with the payment reference as evidence.

ledgdex dispute orthonalyzebot "$CLAIM" --seller biobot --text "Paid on the 1st. No weights came." --evidence PAY-0555 --output dispute.json
DISPUTE=$(ledgdex record biobot --from orthonalyzebot | awk '/dispute recorded/ {print $3}')

The seller tries to rule on its own case. The ledger records the ruling, but the ruling does not count (not_arbiter).

ledgdex ruling biobot biobot "$DISPUTE" --outcome release --text "We sent the weights." --output self-ruling.json
ledgdex record biobot --from biobot || true

The exchange rules: refund. The seller records the ruling, and the claim shows as refunded.

ledgdex ruling exchange biobot "$DISPUTE" --outcome refund --text "No proof of delivery: refund in full." --output ruling.json
ledgdex record biobot --from exchange
ledgdex state biobot | python3 -c '
import json, sys
st = json.load(sys.stdin)
print(json.dumps({"status": [c["status"] for c in st["claims"].values()][0],
                  "ignored": [i["reason"] for i in st["ignored"]]}))'

The whole file: guide/examples/04-disputes.sh, run as it is on every push (py/tests/test_guide.py).

Example: Keys

Keep your ledger safe: a build server signs for the seller, an attacker gets into it, and a lost key gets a replacement.

Run it: In an empty folder: sh 05-keys.sh.

#!/bin/sh
set -eu
KEYS=${LEDGDEX_HOME:-$HOME/.ledgdex}

An exchange (the root) can name a new key for a member that lost its key. Biobot is a seller under the exchange.

ledgdex init exchange --name "ONITO medmodel exchange" --key exchange
ledgdex init biobot --name "biobot" --key biobot --root exchange

The build server of biobot gets its own key, never the main key. Biobot lets that key sign. LEDGDEX_HOME selects the folder of keys, so buildserver-keys acts as the build server.

BUILD=$(LEDGDEX_HOME="$PWD/buildserver-keys" ledgdex keygen buildserver)
ledgdex device add biobot "$BUILD" --name "build server"

On the build server, the key of the build server signs daily work, for example a new offer.

printf '%s' '{"item": {"title": "Penjointu v1.1 weights"}, "quantity": 4, "unit": "license", "currency": "USD", "price": 260000}' > offer.json
LEDGDEX_HOME="$PWD/buildserver-keys" ledgdex offer biobot offer.json

An attacker gets into the build server, so biobot revokes its key. Ledgdex refuses the next note from the build server. It cannot sign now.

ledgdex device revoke biobot "$BUILD" --reason "the build server was compromised"
FIRST=$(ledgdex state biobot | python3 -c 'import json,sys; print(list(json.load(sys.stdin)["offers"])[0])')
LEDGDEX_HOME="$PWD/buildserver-keys" ledgdex note biobot "$FIRST" "from the compromised server" 2>&1 || true

Biobot replaces its main key, for example one time each year.

ledgdex rotate biobot --key biobot-2027

Biobot loses its main key (here, you move the key away). The exchange makes sure who the owner is, and names a new key. The owner takes the ledger back with that key.

mkdir -p lost && mv "$KEYS/biobot-2027.key" lost/
NEW=$(ledgdex keygen biobot-new)
ledgdex recover exchange biobot "$NEW"
ledgdex recovered biobot --root exchange --key biobot-new
ledgdex note biobot "$FIRST" "back with the new key"
ledgdex verify biobot --root exchange

The whole file: guide/examples/05-keys.sh, run as it is on every push (py/tests/test_guide.py).

Example: Publish

Putting a ledgdex online from two devices without the two ever disagreeing.

Run it: In an empty folder: sh 06-publish.sh.

#!/bin/sh
set -eu

You publish a ledgdex like any dex, to a git repository. Online, that is GitHub Pages (--dest git@github.com:YOU/YOU.github.io.git). Here, it is a local repository. Biobot publishes its first offer.

git init -q --bare -b main site.git
ledgdex init biobot --name "biobot" --key biobot --url https://biobot.example --dest "file://$PWD/site.git" --branch main
printf '%s' '{"item": {"title": "Penjointu v1 weights"}, "quantity": 10, "unit": "license", "currency": "USD", "price": 250000}' > offer.json
OFFER=$(ledgdex offer biobot offer.json | awk '{print $NF}')
ledgdex publish biobot && echo published

A second device, the build server, gets a copy of the dex and its own key. Its first publish brings it up to date, and tells it that it is a device of biobot.

cp -r biobot buildserver && rm -rf buildserver/.publish
BUILD=$(LEDGDEX_HOME="$PWD/buildserver-keys" ledgdex keygen buildserver)
ledgdex device add biobot "$BUILD" --name "build server" && ledgdex publish biobot
LEDGDEX_HOME="$PWD/buildserver-keys" ledgdex publish buildserver

Both devices write a note at the same time, and both publish. Ledgdex puts the note of the build server after the note of the main computer. Nothing gets lost, and there is only one history.

ledgdex note biobot "$OFFER" "from the main computer"
LEDGDEX_HOME="$PWD/buildserver-keys" ledgdex note buildserver "$OFFER" "from the build server"
ledgdex publish biobot
LEDGDEX_HOME="$PWD/buildserver-keys" ledgdex publish buildserver

The published ledger is whole and has both notes, in order.

git --git-dir=site.git show main:ledgdex.jsonl > published.jsonl
ledgdex verify published.jsonl
python3 -c '
import json
for line in open("published.jsonl").read().split("\n")[1:]:
    if line and json.loads(line)["msg"]["type"] == "note":
        print(json.loads(line)["msg"]["body"]["text"])'

The whole file: guide/examples/06-publish.sh, run as it is on every push (py/tests/test_guide.py).

Example: Check

Find a cheat: a seller secretly rewrites its records, and a check proves it.

Run it: In an empty folder: sh 07-check.sh.

#!/bin/sh
set -eu

Biobot sells a license to orthonalyzebot, and the buyer keeps a receipt.

ledgdex init biobot --name "biobot" --key biobot
printf '%s' '{"item": {"title": "Penjointu v1 weights"}, "quantity": 10, "unit": "license", "currency": "USD", "price": 250000}' > offer.json
OFFER=$(ledgdex offer biobot offer.json | awk '{print $NF}')
ledgdex init orthonalyzebot --name "orthonalyzebot" --key orthonalyzebot
ledgdex claim orthonalyzebot biobot "$OFFER" --output claim.json
ledgdex record biobot --from orthonalyzebot
ledgdex receipt orthonalyzebot biobot

A first check finds no problems. ledgdex check keeps a copy of each ledger that it reads.

ledgdex check biobot orthonalyzebot --quiet

Biobot rewrites its ledger to remove the sale. It keeps the first two entries (open and offer), adds a note, and signs all with its own key. Alone, the rewritten ledger verifies.

python3 - <<'PY'
from ledgdex.core import Ledger, message
from ledgdex.dex import load, load_key, write
led = load('biobot')
new = Ledger(led.header_line + b'\n' + b''.join(line + b'\n' for line in led.lines[:2]))
new.append(new.next_entry(load_key('biobot'), message(load_key('biobot'), 'note', {'ref': led.ids[1], 'text': 'no sale'})))
write('biobot', new.data)
PY
ledgdex verify biobot

A second check finds the rewrite. The ledger is different from the kept copy, and the receipt of the buyer proves the sale. The check keeps the proof in .ledgdex-check/proofs/.

ledgdex check biobot orthonalyzebot || echo "check failed with exit code $?"
ls .ledgdex-check/proofs/

The same check as a GitHub workflow that runs each day. Without GitHub, use cron or --every 3600.

ledgdex workflow biobot/ledgdex.jsonl orthonalyzebot/ledgdex.jsonl > ledgdex-check.yml
head -3 ledgdex-check.yml

The whole file: guide/examples/07-check.sh, run as it is on every push (py/tests/test_guide.py).

Example: Python API

For developers: using ledgdex from Python directly, with no files or website.

Run it: In an empty folder: python3 08-python-api.py.

import json
from ledgdex.canon import canon, hash_
from ledgdex.core import Ledger, message, new_ledger, new_secret, public
from ledgdex.state import state

Make two keys: one for the seller and one for the buyer. The keys are secrets. Keep them out of each dex.

seller, buyer = new_secret(), new_secret()
print('seller key', public(seller))

A ledger is a header line, then signed entries. new_ledger writes the header and the first entry, "open".

led = new_ledger(seller, 'biobot', 'Maker of Penjointu', 'https://biobot.example')

The seller signs an offer and adds it to its own ledger.

offer = message(seller, 'offer', {
    'item': {'title': 'Penjointu v1 weights', 'text': 'One license', 'media': []}, 'quantity': 3, 'unit': 'license',
    'currency': 'USD', 'price': 250000, 'allow': 'any', 'pay': [{'method': 'card', 'to': 'https://pay.example/biobot'}],
    'arbiter': {'key': public(seller), 'url': ''}, 'terms': ''})
offer_id = led.append(led.next_entry(seller, offer))

The buyer signs a claim that names the offer by its id and its hash. Thus the offer cannot change under the claim. The seller records the claim, and that entry is the receipt of the buyer.

claim = message(buyer, 'claim', {'offer': offer_id, 'offer_hash': hash_(offer), 'quantity': 2, 'price': 250000})
claim_id = led.append(led.next_entry(seller, claim))

Anyone can read the ledger from its bytes, verify it, and calculate its state. The claim is accepted, and 1 license is left.

again = Ledger(led.data, cache=False)
print('whole', again.whole, 'entries', len(again.entries), 'id', again.id[:19])
st = state(again)
print(st['claims'][claim_id], st['offers'][offer_id]['remaining'], 'left')

Change one byte of the recorded claim (quantity 2 becomes 3). Verification breaks at that entry.

data = bytearray(led.data)
data[data.index(b'"quantity":2') + 11] = ord('3')
tampered = Ledger(bytes(data), cache=False)
print('broken at', tampered.broken_at, '-', tampered.error)

Canonical JSON gives the bytes that ledgdex hashes and signs: sorted keys, no spaces, UTF-8.

print(canon({'b': 1, 'a': 'é'}).decode())

The whole file: guide/examples/08-python-api.py, run as it is on every push (py/tests/test_guide.py).

Example: Bots

Programs trading with each other, with no person involved: two buyer bots buy software licences from the cheaper of two seller bots.

Run it: In an empty folder: python3 09-bots.py.

import contextlib, io, json
from ledgdex.cli import main

ledgdex(...) runs one ledgdex command in this program and returns its output. state(...) reads the state of a ledgdex.

def ledgdex(*args):
    out = io.StringIO()
    with contextlib.redirect_stdout(out), contextlib.redirect_stderr(io.StringIO()):
        main([str(a) for a in args])
    return out.getvalue()


def state(src):
    return json.loads(ledgdex('state', src))

In each round, a seller bot collects claims and payments from the dexes of its buyers. For each accepted claim with a payment, it records the money and delivers a licence key. A real bot first looks up the payment reference at its bank.

class SellerBot:
    def __init__(self, dex, buyers):
        self.dex, self.buyers = dex, buyers

    def round(self):
        ledgdex('record', self.dex, *[x for b in self.buyers for x in ('--from', b)])
        for cid, c in state(self.dex)['claims'].items():
            if c['status'] == 'accepted' and 'paid' in c and not c.get('delivered'):
                ledgdex('received', self.dex, cid, c['price'] * c['quantity'])
                ledgdex('delivered', self.dex, cid, '--note', 'licence key LIC-' + cid[7:15].upper())

A buyer bot wants one licence within a budget. In its first round, it finds the cheapest open offer in the index and claims it. In each later round, it keeps the receipts of the seller. It pays when the seller accepts the claim, and it confirms when the licence arrives.

class BuyerBot:
    def __init__(self, dex, index, title, budget):
        self.dex, self.index, self.title, self.budget = dex, index, title, budget
        self.me = state(dex)['owner']
        self.seller = None

    def round(self):
        if self.seller is None:
            found = json.loads(ledgdex('discover', self.index, '--offers', '--json'))
            offers = [(o['price'], listed['url'], o['id']) for listed in found for o in listed['offers']
                      if o['title'] == self.title and o['price'] <= self.budget and o['remaining'] > 0]
            if offers:
                price, self.seller, oid = min(offers)
                ledgdex('claim', self.dex, self.seller, oid, '--output', self.dex + '-claim.json')
            return
        ledgdex('receipt', self.dex, self.seller)
        for cid, c in state(self.seller)['claims'].items():
            if c['buyer'] != self.me:
                continue
            if c['status'] == 'accepted' and 'paid' not in c:
                ledgdex('pay', self.dex, self.seller, cid, '--method', 'card', '--ref', 'PAY-' + cid[7:13],
                        '--output', self.dex + '-paid.json')
            elif c.get('delivered') and not c.get('confirmed'):
                ledgdex('confirm', self.dex, self.seller, cid, '--output', self.dex + '-confirmed.json')

The market: an index listing two software shops, Acme at USD 49.00 and Globex at USD 39.00, and two buyers.

ledgdex('init', 'index', '--name', 'Software Market', '--key', 'index')
for shop, price in (('acme', 4900), ('globex', 3900)):
    ledgdex('init', shop, '--name', shop.title() + ' Software', '--key', shop)
    with open(shop + '.json', 'w') as f:
        json.dump({'item': {'title': 'Photo editor licence'}, 'quantity': 100, 'unit': 'licence',
                   'currency': 'USD', 'price': price}, f)
    ledgdex('offer', shop, shop + '.json')
    ledgdex('list', 'index', shop, '--note', 'software')
for buyer in ('alice', 'bob'):
    ledgdex('init', buyer, '--name', buyer.title(), '--key', buyer)

Each bot takes a turn in each round, as on a timer. Online, each bot also publishes its dex after its turn. After four rounds, both buyers bought from Globex, the cheaper shop, and both deals closed.

sellers = [SellerBot('acme', ['alice', 'bob']), SellerBot('globex', ['alice', 'bob'])]
buyers = [BuyerBot('alice', 'index', 'Photo editor licence', 5000), BuyerBot('bob', 'index', 'Photo editor licence', 5000)]
for n in range(1, 5):
    for bot in buyers + sellers:
        bot.round()
    claims = state('globex')['claims']
    print('round', n, sorted(c['status'] + (' delivered' if c.get('delivered') else '') for c in claims.values()))
print('all deals closed:', all(c['status'] == 'closed' for c in state('globex')['claims'].values()) and
      len(state('globex')['claims']) == 2)

The whole file: guide/examples/09-bots.py, run as it is on every push (py/tests/test_guide.py).

Example: Robot Cleaning

Cleaning robots paid per clean by the shops of a mall, for one day.

Run it: In an empty folder: python3 10-robot-cleaning.py.

import contextlib, hashlib, io, json, os
from ledgdex.cli import main
HOME = os.environ.get('LEDGDEX_HOME') or os.path.join(os.path.expanduser('~'), '.ledgdex')

ledgdex(...) runs one ledgdex command. keys picks the folder of keys to sign with: a robot's own, or by default its company's.

def ledgdex(*args, keys=None):
    os.environ['LEDGDEX_HOME'] = keys or HOME
    out = io.StringIO()
    with contextlib.redirect_stdout(out), contextlib.redirect_stderr(io.StringIO()):
        main([str(a) for a in args])
    return out.getvalue()


def state(src):
    return json.loads(ledgdex('state', src))

Two companies have one robot each. The company holds the main key. The robot gets its own device key and signs with it. Each robot offers 10 cleans for today, at USD 4.00 each. Three shops join.

robots = {'roomba': 'CleanCo', 'hoover': 'Brightfloor'}
for robot, operator in robots.items():
    ledgdex('init', robot, '--name', robot.title() + ' (' + operator + ')', '--key', operator.lower())
    robot_keys = os.path.join(os.getcwd(), robot + '-keys')
    device = ledgdex('keygen', robot, keys=robot_keys).strip()
    ledgdex('device', 'add', robot, device, '--name', robot)
    with open(robot + '-offer.json', 'w') as f:
        json.dump({'item': {'title': 'Floor clean, 2026-10-01'}, 'quantity': 10, 'unit': 'clean',
                   'currency': 'USD', 'price': 400, 'expires': '2099-01-01T00:00:00Z'}, f)
    ledgdex('offer', robot, robot + '-offer.json', keys=robot_keys)
shops = ['bakery', 'pharmacy', 'bookshop']
for shop in shops:
    ledgdex('init', shop, '--name', shop.title(), '--key', shop)

One clean has these steps:

1. The shop claims the clean from the open offer of the robot.

2. The robot records the claim, cleans, and records a photo as evidence: its address and hash. Here the hash is an example.

3. The shop confirms the clean.

def open_offer(robot):
    return next((i for i, o in state(robot)['offers'].items() if o['status'] == 'open' and o['remaining']), None)


def clean(shop, robot):
    keys = os.path.join(os.getcwd(), robot + '-keys')
    ledgdex('claim', shop, robot, open_offer(robot), '--output', shop + '-claim.json')
    claim = ledgdex('record', robot, '--from', shop, keys=keys).split('claim recorded: ')[1].split()[0]
    photo = hashlib.sha256((robot + claim).encode()).hexdigest()
    ledgdex('delivered', robot, claim, '--note', 'photo https://photos.example/' + photo + '.jpg sha256:' + photo,
            keys=keys)
    ledgdex('confirm', shop, robot, claim, '--output', shop + '-confirmed.json')
    ledgdex('record', robot, '--from', shop, keys=keys)
    return claim

In the morning, each shop gets a clean from each robot. At midday, the battery of the hoover fails, so the hoover withdraws its offer. In the afternoon, each shop uses the roomba.

done = [(shop, robot, clean(shop, robot)) for shop in shops for robot in robots]
ledgdex('withdraw', 'hoover', open_offer('hoover'), keys=os.path.join(os.getcwd(), 'hoover-keys'))
print('robot offline: hoover; open offers:', {r: open_offer(r) is not None for r in robots})
done += [(shop, 'roomba', clean(shop, 'roomba')) for shop in shops]

In the evening each shop pays for each of the day's cleans, and each robot records the money it received.

for shop, robot, claim in done:
    ledgdex('pay', shop, robot, claim, '--method', 'card', '--ref', 'DAY-2026-10-01', '--output', shop + '-paid.json')
for robot in robots:
    keys = os.path.join(os.getcwd(), robot + '-keys')
    ledgdex('record', robot, *[x for s in shops for x in ('--from', s)], keys=keys)
    for cid, c in state(robot)['claims'].items():
        if 'paid' in c and 'received' not in c:
            ledgdex('received', robot, cid, c['price'], keys=keys)

The report of the day comes only from the ledgers, so anyone can make sure that it is correct.

for robot in robots:
    claims = state(robot)['claims'].values()
    print(robot, 'day total: USD', sum(c['price'] for c in claims if c['status'] == 'closed') / 100,
          'for', sum(c['status'] == 'closed' for c in claims), 'cleans')
print('paid per clean:', all(c['status'] == 'closed' for r in robots for c in state(r)['claims'].values()))

The whole file: guide/examples/10-robot-cleaning.py, run as it is on every push (py/tests/test_guide.py).

Example: Extending

For developers: building your own report from a ledger, and adding it to your website.

Run it: In an empty folder: python3 11-extending.py.

import contextlib, csv, io, json, os
from ledgdex.cli import main
from ledgdex.dex import load
from ledgdex.render import MARKER, render
from ledgdex.state import state

ledgdex(...) runs one ledgdex command in this program and returns what it printed.

def ledgdex(*args):
    out = io.StringIO()
    with contextlib.redirect_stdout(out), contextlib.redirect_stderr(io.StringIO()):
        main([str(a) for a in args])
    return out.getvalue()

A seller with three sales, made as in the first trade example.

ledgdex('init', 'biobot', '--name', 'biobot', '--key', 'biobot')
with open('offer.json', 'w') as f:
    json.dump({'item': {'title': 'Penjointu v1 weights'}, 'quantity': 50, 'unit': 'license', 'currency': 'USD',
               'price': 250000}, f)
offer = ledgdex('offer', 'biobot', 'offer.json').split()[-1]
for buyer, qty in (('orthonalyzebot', 2), ('kneecarebot', 5), ('spinecarebot', 1)):
    ledgdex('init', buyer, '--name', buyer, '--key', buyer)
    ledgdex('claim', buyer, 'biobot', offer, '--quantity', qty, '--output', buyer + '.json')
    ledgdex('record', 'biobot', '--from', buyer)

Your own tool: a spreadsheet (CSV) of sales, calculated only from the verified ledger.

led = load('biobot')
st = state(led)
with open('sales.csv', 'w', newline='') as f:
    w = csv.writer(f)
    w.writerow(['claim', 'buyer', 'quantity', 'amount', 'status'])
    for cid, c in st['claims'].items():
        w.writerow([cid, c['buyer'], c['quantity'], c['quantity'] * c['price'], c['status']])
total = sum(c['quantity'] * c['price'] for c in st['claims'].values() if c['status'] != 'rejected')
print('sales.csv written:', len(st['claims']), 'claims, USD', total / 100)

Your own page: a ledgdex is a dex. Thus add a page to data.json as in any dex, then render the site.

with open('biobot/data.json') as f:
    pages = json.load(f)
pages.append({'title': 'Sales report', 'body': [
    led.header['name'] + ' sales: ' + str(len(st['claims'])) + ' orders, USD ' + '{:,.2f}'.format(total / 100) + '.',
    'Every figure here is computed from the signed ledger: <a href="ledgdex.jsonl">ledgdex.jsonl</a>.']})
with open('biobot/data.json', 'w') as f:
    json.dump(pages, f, indent=4, ensure_ascii=False)
render('biobot')
print(open('biobot/gen/salesreport.html').read().split('<p>')[1].split('</p>')[0])

A new ledger entry rewrites only the pages that ledgdex made (they start with &lt;!-- ledgdex --&gt;). Your page stays.

ledgdex('note', 'biobot', offer, 'new licenses added')
with open('biobot/data.json') as f:
    kept = [p for p in json.load(f) if p['title'] == 'Sales report' and not p['body'][0].startswith(MARKER)]
print('your page kept:', len(kept) == 1, '| ledgdex pages:', sum(p['body'][0].startswith(MARKER) for p in pages))

The whole file: guide/examples/11-extending.py, run as it is on every push (py/tests/test_guide.py).

Example: JavaScript

For developers: the same ledger code in JavaScript, for Node or a web page.

Run it: From the repository root (or with LEDGDEX_JS set to the folder or URL of the JavaScript core): node guide/examples/12-javascript.mjs.

import { pathToFileURL } from 'url';

Load the JavaScript core: from LEDGDEX_JS (a folder or a URL) if set, else from js/ in the ledgdex repository.

const where = process.env.LEDGDEX_JS || new URL('../../js/', import.meta.url).href;
const base = /^[a-z]+:/.test(where) ? where : pathToFileURL(where.endsWith('/') ? where : where + '/').href;
const { Ledger, message, newLedger, newSecret, publicKey } = await import(new URL('core.js', base));
const { state } = await import(new URL('state.js', base));
const { hash, canonString, ID_KEY } = await import(new URL('canon.js', base));

Make two keys and a ledger. Then sign an offer, and a claim of a buyer for it, as in the Python example.

const seller = newSecret(), buyer = newSecret();
const led = newLedger(seller, 'biobot', 'Maker of Penjointu', 'https://biobot.example');
const offer = message(seller, 'offer', {
  item: { title: 'Penjointu v1 weights', text: '', media: [] }, quantity: 3, unit: 'license', currency: 'USD', price: 250000,
  allow: 'any', pay: [], arbiter: { key: publicKey(seller), url: '' }, terms: '' });
const offerId = led.append(led.nextEntry(seller, offer));
const claim = message(buyer, 'claim', { offer: offerId, offer_hash: hash(offer), quantity: 1, price: 250000 });
led.append(led.nextEntry(seller, claim));

Read the ledger from its bytes, as the viewer does with a ledgdex.jsonl that it downloaded. The ledger is whole, the claim is accepted, and a new read gives the same state.

const read = new Ledger(led.data);
console.log('whole', read.whole, 'entries', read.entries.length);
const st = state(read);
console.log(Object.values(st.claims).map((c) => c.status).join(', '));
console.log('same state as a new read:', canonString(st, ID_KEY) === canonString(state(new Ledger(led.data)), ID_KEY));

The whole file: guide/examples/12-javascript.mjs, run as it is on every push (py/tests/test_guide.py).

In a web page, load the same core as a module and read any published ledger:

<script type='module'>
  import { Ledger } from 'https://ledgdex.github.io/viewer/core.js';
  const res = await fetch('https://biobot.example/ledgdex.jsonl');
  const led = new Ledger(new Uint8Array(await res.arrayBuffer()));
  console.log(led.whole, led.entries.length, led.error);
</script>

Example: Verify in Node

For developers: a Node program that reads a published ledger, checks it and lists what it sells.

Run it: From the repository root (or with LEDGDEX_JS set to the folder or URL of the JavaScript core): node guide/examples/13-js-verify.mjs.

import { readFile } from 'fs/promises';
import { pathToFileURL } from 'url';

Load the JavaScript core: from LEDGDEX_JS (a folder or a URL) if set, else from js/ in the ledgdex repository.

const where = process.env.LEDGDEX_JS || new URL('../../js/', import.meta.url).href;
const base = /^[a-z]+:/.test(where) ? where : pathToFileURL(where.endsWith('/') ? where : where + '/').href;
const { Ledger } = await import(new URL('core.js', base));
const { state } = await import(new URL('state.js', base));
const { amount } = await import(new URL('render.js', base));

Read a ledger from a dex address, a ledgdex.jsonl address or a file, given as the first argument. Without one, this reads the ledger of these docs.

const source = process.argv[2] || 'https://ledgdex.github.io/docs';
async function read(src) {
  if (!/^https?:\/\//.test(src)) return new Uint8Array(await readFile(src));
  const url = src.endsWith('.jsonl') ? src : src.replace(/\/+$/, '') + '/ledgdex.jsonl';
  const res = await fetch(url);
  if (!res.ok) throw new Error(url + ': HTTP ' + res.status);
  return new Uint8Array(await res.arrayBuffer());
}
const bytes = await read(source);

Verify each hash and each signature. whole is true only if each entry is correct. If not, broken_at and error say where and why.

const led = new Ledger(bytes);
console.log(led.header.name + ': ' + (led.whole ? 'whole' : 'BROKEN at entry ' + led.broken_at + ', ' + led.error) +
  ', ' + led.entries.length + ' entries');

The state says which offers are open and how many units each offer has. The price and the unit are in the signed offer. Find the offer by its id.

const st = state(led);
for (const [id, o] of Object.entries(st.offers)) {
  if (o.status !== 'open') continue;
  const offer = led.entries[led.index.get(id)].msg.body;
  console.log(o.title, '|', amount(offer.currency, offer.price), 'per', offer.unit, '|', o.remaining, 'left');
}

Change one letter of the offer's title and read the ledger again: it is no longer whole, and it says where.

const changed = bytes.slice();
changed[Buffer.from(changed).indexOf('Penjointu')] = 'B'.charCodeAt(0);
const bad = new Ledger(changed);
console.log('one changed letter breaks it at entry', bad.broken_at + ':', bad.error);

The whole file: guide/examples/13-js-verify.mjs, run as it is on every push (py/tests/test_guide.py).

Example: Shop Web Page

A web page that shows the offers of a seller. It reads them from the ledger and checks them in the browser of the reader.

Run it: Save it as a file and open it in a browser, or put it on your website.

<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>biobot: models for sale</title>
</head>

The page shows a line while it works, and a list that the script fills in.

<body>
<h1>Today's offers</h1>
<p id='status'>Checking the shop's ledger...</p>
<ul id='offers'></ul>
<script type='module'>

Load the JavaScript core from the ledgdex site. For a real shop, copy js/ from a release onto your own site and load it from there.

import { Ledger } from 'https://ledgdex.github.io/viewer/core.js';
import { state } from 'https://ledgdex.github.io/viewer/state.js';
import { amount } from 'https://ledgdex.github.io/viewer/render.js';

Read the ledger of the seller. This example reads the ledger of these docs: put the address of your seller here. The site that serves the ledger must let other sites read it. GitHub Pages does.

const SHOP = 'https://ledgdex.github.io/docs/ledgdex.jsonl';
const res = await fetch(SHOP);
const led = new Ledger(new Uint8Array(await res.arrayBuffer()));

Check every hash and signature in the reader's own browser, then list the open offers. Text from a ledger goes in with textContent, never as HTML, so nothing written in a ledger can run on your page.

const status = document.getElementById('status');
if (!led.whole) {
  status.textContent = 'This ledger is broken at entry ' + led.broken_at + ': ' + led.error;
} else {
  status.textContent = led.header.name + ': whole, ' + led.entries.length + ' entries';
  const list = document.getElementById('offers');
  for (const [id, o] of Object.entries(state(led).offers)) {
    if (o.status !== 'open') continue;
    const offer = led.entries[led.index.get(id)].msg.body;
    const item = document.createElement('li');
    item.textContent = o.title + ': ' + amount(offer.currency, offer.price) + ' per ' + offer.unit + ', ' + o.remaining + ' left';
    list.append(item);
  }
}
</script>
</body>
</html>

The whole file: guide/examples/14-js-web-page.html, run as it is on every push (py/tests/test_guide.py).

Example: Sign in the Browser

A buyer bot with only a browser: a sealed key, a signed claim, and a seller that records it with the command line.

Run it: From the repository root (or with LEDGDEX_JS set to the folder or URL of the JavaScript core): node guide/examples/15-js-sign.mjs.

import { execFileSync } from 'child_process';
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
import { pathToFileURL } from 'url';

Load the JavaScript core: from LEDGDEX_JS (a folder or a URL) if set, else from js/ in the ledgdex repository. The same modules run in a browser.

const where = process.env.LEDGDEX_JS || new URL('../../js/', import.meta.url).href;
const base = /^[a-z]+:/.test(where) ? where : pathToFileURL(where.endsWith('/') ? where : where + '/').href;
const { Ledger, messageA, newLedgerA, newSecret, publicKey } = await import(new URL('core.js', base));
const { state } = await import(new URL('state.js', base));
const { hash } = await import(new URL('canon.js', base));
const { sealKey, openKey } = await import(new URL('keystore.js', base));
const { signer } = await import(new URL('sig.js', base));

The seller biobot uses the command line, as in the first trade example. It makes its ledgdex and offers 10 licenses for the Penjointu weights.

const ledgdex = (...args) => execFileSync('ledgdex', args, { encoding: 'utf8' });
ledgdex('init', 'biobot', '--name', 'biobot', '--key', 'biobot');
writeFileSync('offer.json', JSON.stringify({ item: { title: 'Penjointu v1 weights' }, quantity: 10, unit: 'license',
  currency: 'USD', price: 250000 }));
const offerId = ledgdex('offer', 'biobot', 'offer.json').trim().split(' ').pop();

The buyer has only a browser. It makes a key and seals it with a passphrase, as the viewer does. A browser can keep the sealed box in localStorage. Only the passphrase opens it.

const secret = newSecret();
const box = await sealKey(secret, 'a long passphrase that only orthonalyzebot knows', publicKey(secret));
console.log('key sealed with a passphrase:', box.kdf + ',', box.rounds, 'rounds');
const opened = await openKey(box, 'a long passphrase that only orthonalyzebot knows');

signer signs with Web Crypto's Ed25519 when the browser (or Node) has it, and with ledgdex's own code when it does not.

const sgn = await signer(opened);
console.log('signed with', sgn.name);

The buyer reads the ledger of biobot and finds the offer. It signs a claim for 2 licenses that names the offer by its id and its hash.

const seller = new Ledger(new Uint8Array(readFileSync('biobot/ledgdex.jsonl')));
const offer = seller.entries[seller.index.get(offerId)].msg;
const claim = await messageA(sgn, 'claim', { offer: offerId, offer_hash: hash(offer), quantity: 2, price: offer.body.price });

The buyer keeps the claim as "sent" in a ledger of its own, and publishes it: here, a folder with its ledgdex.jsonl. In the viewer, Download your dex does this.

const mine = await newLedgerA(sgn, 'orthonalyzebot', 'A buyer bot with a browser', '');
mine.append(await mine.nextEntryA(sgn, await messageA(sgn, 'sent', { to: seller.header.owner, msg: claim })));
mkdirSync('orthonalyzebot');
writeFileSync('orthonalyzebot/ledgdex.jsonl', mine.data);

Biobot collects the claim from the dex of the buyer with the command line, and accepts the claim.

console.log(ledgdex('record', 'biobot', '--from', 'orthonalyzebot').trim());
const after = state(new Ledger(new Uint8Array(readFileSync('biobot/ledgdex.jsonl'))));
console.log('biobot accepted the claim:', Object.values(after.claims).map((c) => c.status).join(', '));

The whole file: guide/examples/15-js-sign.mjs, run as it is on every push (py/tests/test_guide.py).








BACK TO LEDGDEX DOCS