Run your own shop

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.

Where this stands

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/.

The five pieces

PieceWhat it isWho holds it
Your offerA 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 pageThe 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 poolWhere a transaction waits while signatures are added. It submits to Hedera once every required signature is in.Shared infrastructure. It holds no keys.
Your signerKeaton 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 walletHashPack 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.
How one sale flows The published offer sits above the flow. The shop page builds a transaction from the offer and raises it to the pool. The buyer's wallet signs it. Your Keaton device checks it against the same offer and signs your side. With both signatures the pool submits it to Hedera, which settles both legs at once. YOUR OFFER — published its id is the hash of its contents Shop page builds it, holds no key Buyer's wallet signs the buyer's side Pool waits for signatures ONE TRANSACTION Your Keaton device checks, then signs your side Hedera both legs, or neither 1 raise 2 sign 3 sign 4 submit built from checked against
The page and your device both answer to the same published offer. The page builds the sale from it, and your device refuses any sale that the offer would not have produced. Steps 2 and 3 can happen in either order.

Before you start

1Set up your signing device

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.

2Make your selling key and account

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.

For real money: two of three

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).

3Write your offer

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
BlockWhat it decides
#constantsEverything fixed about the offer: who sells, what, at what price, how many. Change one and it is a different offer with a different id.
#inputsThe only things a buyer gets to choose. The shop page cannot send a price, because a price is not an input.
#observeWhat 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.
#requireThe rules. Each one has a reason in plain words, and a refused sale is refused with that reason.
#transactionThe 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.

4Publish it

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.

5Tell your signer to approve it

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.

6Open the shop

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.

What your signer checks on every sale

  1. Is this one of my offers? The offer's id must be in your config. Anything else is not your business and gets no answer at all.
  2. Did the page send only the declared inputs? Anything undeclared (a price, say) is dropped when the sale is built, and a sale that still carries one is refused.
  3. Do the rules hold right now? Every rule is re-run with fresh chain reads, so "in stock" means in stock when your device looks.
  4. Are these bytes exactly what the offer produces? Your device rebuilds the transaction from the offer and the inputs and compares it with what it is being asked to sign. A tenth of the price, the token sent to someone else, five when three were ordered: each is refused.
  5. Is it within this hour's ceiling? Orders count once each. The same order raised again does not count twice.
  6. Only then does it sign, and only for your account. The buyer's side is the buyer's.

Checks 3 to 5, and the refusals in 4, are exercised against real transaction bytes in hbarlive_hts qa/shop/kbar-tests.js.

Watching it run

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.

Before you take real money

Known gaps, in the order they matter

Questions

QuestionAnswer
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.