Publish an offer, put a buy button in front of it, and approve every sale on a device you hold. The buyer pays and receives in one transaction, signed by both of you. Nothing in the middle (not the shop page, not the pool, not this site) can move your stock without your device agreeing that the sale matches your offer.
This page describes the shop once its tooling is finished. Each step is marked today where the commands shown work now, or planned where the tool is still to be built. Planned steps also show how to do the same thing today. The worked example, used throughout, is a testnet offer selling KBAR at 1 HBAR each: /deals/kbar/.
| Piece | What it is | Who holds it |
|---|---|---|
| Your offer | A short document: what you sell, at what price, in what quantities, under which rules. Its id is the hash of its contents, so it cannot be edited under a buyer. | Published on a website. Anyone can read it. |
| The shop page | The buy button. It turns the buyer's choices into a transaction built from your offer and puts it in the pool. | Any website. It holds no keys. |
| The pool | Where a transaction waits while signatures are added. It submits to Hedera once every required signature is in. | Shared infrastructure. It holds no keys. |
| Your signer | Keaton on a device of yours. It re-checks each sale against your offer and signs your side if it matches. | You. Your selling key lives only here. |
| The buyer's wallet | HashPack or any WalletConnect wallet. The buyer signs their side of the sale. (The network fee is paid by whichever account the offer names as its operator. In the KBAR offer that is the buyer, because the offer says so.) | The buyer. |
KEA_ALLOW_MAINNET=1). Free testnet HBAR comes from the
Hedera faucet.today On the device:
pkg install nodejs-lts git # Termux; elsewhere: Node 20+ and git
git clone https://github.com/topshef/hbarlive_hts && cd hbarlive_hts
npm ci --ignore-scripts --omit=dev
bin/kea name myshop # what this device is called on the dashboard
planned Keaton in its own public repository, so a merchant clones only the signer and not the backend it currently lives beside. Today the repository is private and cloning it needs an access token from its owner.
today Make the key on the signing device. The private half is
written into that device's .env and only the public half is printed:
node kea/tools/keygen.cjs --env .env --name SHOP_SELLER_KEY
# SHOP_SELLER_KEY written to .env — public 3b7e…
Then create a Hedera account whose key is that public key, and move your stock into it. If the account allows automatic token associations, tokens can arrive without its signature.
planned A "new seller account" page: paste the public key, get an account that is ready to receive stock. Today, create it from a wallet that can set the key, or ask whoever runs your pool to create it for you. They only ever see the public key.
One device means one stolen phone can sell, and one sleeping phone stops sales. Give the account
a two-of-three key over three devices, each made with keygen.cjs on its
own device. Each device runs the same signer, two must agree, and any one can be lost.
today node kea/tools/threshold.cjs --account 0.0.X --threshold 2
--keys PUB_A,PUB_B,PUB_C … (see its header for the signatures the change itself needs).
today Write a deal turns a short script into the offer document and shows its id as you type. This is the KBAR offer as a script:
#deal
name GoMint — KBAR at 1 HBAR each (testnet)
network testnet
#constants
SELLER is 0.0.your-account
KBAR is 0.0.5301020
UNIT is 1000000 # 1 KBAR in its smallest units (6 decimals)
PRICE is 100000000 # 1 HBAR in tinybars, per UNIT
QTY_MIN is 1
QTY_MAX is 10
#inputs
buyer account
qty integer
ref string
#observe
stock = hedera.mirror /accounts/{SELLER}/tokens?token.id={KBAR} -> tokens[0].balance
#require
qty is at least the minimum
qty >= QTY_MIN
qty is at most the maximum
qty <= QTY_MAX
the seller has that much KBAR in stock
stock >= qty * UNIT
the buyer is not the seller
buyer != SELLER
ref is 1 to 12 characters
ref != "" and substr(ref, 0, 12) == ref
#transaction
transfer
SELLER receives -1 * qty * UNIT KBAR
buyer receives qty * UNIT KBAR
buyer receives -1 * qty * PRICE hbar
SELLER receives qty * PRICE hbar
memo gomint KBAR x{qty} {ref}
operator buyer
valid from +3s, for 180s
| Block | What it decides |
|---|---|
| #constants | Everything fixed about the offer: who sells, what, at what price, how many. Change one and it is a different offer with a different id. |
| #inputs | The only things a buyer gets to choose. The shop page cannot send a price, because a price is not an input. |
| #observe | What is read from the chain at the moment of the sale. Your device reads it again for itself, so a sold-out offer stops being signable. |
| #require | The rules. Each one has a reason in plain words, and a refused sale is refused with that reason. |
| #transaction | The legs, in smallest units. operator buyer makes the buyer the payer of the network fee. valid … for 180s is how long the sale waits for signatures. |
An offer is published when a website lists it. Two files, at fixed addresses:
https://your-site/deals/index.json # the list: one row per offer
https://your-site/deals/dealId.json # the offer itself
Anyone can check that the second file hashes to its name, and your signer does. The deal lookup finds offers by id on the sites it knows.
planned Publish from Write a deal straight to
any static host you own (GitHub Pages, Cloudflare Pages, your own server).
today Offers on this site are kept as sources in the
phpjabbers repository (deals-src/), and
node deals-src/publish.mjs writes both files. This is how the KBAR offer is published.
today Copy your published offers onto the device. Every file is re-hashed before a byte is written:
node kea/tools/catalogue.cjs vendor --from https://your-site --to vendor/myshop
Then a config file beside the checkout, say kea.shop.json. It never holds a key, only
the name of the line in .env that does:
{
"network": "testnet",
"doorbell": { "ws": "wss://api.katomia.com/hapi/ws", "mode": "kea" },
"pool": { "base": "https://api.katomia.com/hapi" },
"mirror": "https://testnet.mirrornode.hedera.com/api/v1",
"envFile": ".env",
"keysets": { "seller": ["env:SHOP_SELLER_KEY"] },
"resolve": ["vendor/myshop/deals"],
"deals": {
"dealId": { "sign": ["seller"], "limits": { "0.0.5301020": 50000000 } }
},
"presence": { "everyMs": 30000 }
}
limits is your hourly ceiling per token, in smallest units: here at most 50 KBAR leaves
your account per hour, whatever the offer allows. Then check it and start it:
KEA_CONFIG=kea.shop.json bin/kea check # is this device ready? names every problem
KEA_CONFIG=kea.shop.json bin/kea go --name myshop # approve sales that pass
It runs happily beside other signers on the same device, in its own Termux or tmux session.
planned A config page: tick your offers from a published list, set the ceilings, download the file. It writes no key material, so it is safe to do in a browser.
today /deals/kbar/ is a working buy page for the KBAR offer. It reads the offer, checks its hash, shows stock and whether a signer covering the offer is online, checks the buyer's balance and token association, raises the sale, opens the buyer's wallet, and watches until it settles. It is written for this one offer's shape (quantity × price).
planned One buy page for any published sale offer, and a snippet to put a buy button on your own site, which needs only the offer's id.
Checks 3 to 5, and the refusals in 4, are exercised against real transaction bytes in
hbarlive_hts qa/shop/kbar-tests.js.
Your device appears on the Keaton dashboard under the name you gave it, with
the keyset it signs with and the last four characters of the key (seller …3b7e). Tick
deals only on the pool list to see your sales without the rest of the traffic. On the device,
bin/kea who lists who else is on duty.
.env and nowhere else: not in the config, not in a screenshot, not in a
message.| Question | Answer |
|---|---|
| What if my device is off? | Sales wait for it, then expire after three minutes. An expired sale is never submitted, so nothing moves and nobody pays anything. |
| Can the shop page cheat me? | It can build any transaction it likes, but your device signs only one that the offer produces exactly. A page that lies gets a refusal with a reason. |
| Can it cheat the buyer? | The buyer's wallet shows exactly what is being signed before they approve it, and the offer is public for anyone to check. |
| How do I change the price? | Publish a new offer and add its id to your config. Remove the old id from your config when you stop honouring it. Taking it off the page alone is not enough. |
| Who pays the network fee? | Whichever account the offer names as its operator: that account is the transaction's payer, so its signature pays the fee. Nothing makes it the buyer by default. The KBAR offer says operator buyer; an offer that says operator SELLER has your signature pay it instead. |
| What does a buyer need? | A Hedera account with enough HBAR, a WalletConnect wallet, and either the token already associated or a free automatic-association slot. The buy page checks the balance and the association before it lets them buy. |
| What about refunds? | Nothing is built in. A refund is another transaction, which you sign like any other payment. |
| Does whoever runs the pool hold my key? | No. They see your public key and your signatures. Your private key is only on your devices. |