KEA is a signer you control. It reads a transaction, checks it against rules you published, and signs the ones that match. Its authority is the rule; its answer is a signature.
Assembling a transaction and approving one are separate jobs, done by separate parties. Whoever builds it hands it on; whoever holds a key approves what matches a rule published in advance.
Five jobs, and the useful part is that five different parties can do them — the author of a rule, the one proposing against it, and the one holding a key are free to be three different people:
| Job | Who does it | What they hold |
|---|---|---|
| Write the rule | anyone — a business, a person, a script | Writes down what they are willing to pay, and on what conditions, and publishes it. That document is the authority everything else answers to. |
| Ask | a bot, a page, a person | Proposes. That is the whole job — approval is a separate step, taken by someone else. |
| Assemble and hold | the pool (kpool) | The transaction itself, in the open, while approvals arrive. It relays the bytes onward exactly as they were signed. |
| Approve | KEA — one or more devices | A key, used for one purpose: adding one approval to a transaction that matches a published rule. |
| Settle | Hedera | The ledger. It counts the approvals against the account's own rule and settles once they satisfy it. |
You publish what you are willing to do; someone proposes it; a venue assembles it; and a signer you control approves it when it matches what you published. Your key stays with you throughout.
A signer adds one signature and hands it back. The ledger keeps count, and settles the moment the approvals satisfy the account's own rule — so with two of three devices, the first signature moves the transaction one step closer and the second completes it.
That is what makes approvers cheap to add and safe to spread around: each one carries a share of the decision, and the ledger does the arithmetic.
KEA talks outbound only: it opens a connection to the pool and to a public Hedera mirror node, and everything it needs arrives over those. That is what lets a signer live on a phone behind an ordinary home router.
This is the part worth understanding properly. Think of a transaction as a ball dropped into the top of a machine. It falls through a series of gates, and each gate either passes it on or sets it aside with a named reason. A ball that clears every gate reaches the bottom and gets signed.
| Gate | The question | Why it matters |
|---|---|---|
| policy | What is the identity of these rules? | The rule document is hashed, and that hash is its id. The signer computes it from the document in front of it, so the name always follows the contents. |
| authority | Is this id on my list? | The allowlist belongs to the signer. Whatever arrives, the answer comes from the signer's own list — which is what lets it listen to an open venue. |
| inputs | Were only the declared inputs supplied? | A rule declares exactly what it accepts, and the signer hands it precisely those inputs. |
| decode | What do these bytes actually move? | Read straight from the raw transaction — the bytes are the thing that will execute, so the bytes are what gets examined. |
| observe | What does the chain say right now? | The signer fetches every fact for itself from a public mirror node, so its verdict rests on what it read. |
| derive / require | Do the conditions hold? | The rule's own conditions — a deadline, a balance, a winner, an owner — evaluated against what was just observed. |
| reproduce | If I built this myself, would I get the same thing? | The strongest check. The signer constructs the transaction from the rules and compares it leg by leg with what it was handed; an exact match is the condition for going on. |
| caps | Is this more than I agreed to approve in an hour? | A ceiling per asset, counting what this signer has already approved this hour. Your backstop, set by you. |
| signers | Does this even need my key? | Derived from the bytes and the chain: which accounts are debited or pay the fee, and whether one of the keys this deal is signed with is on their key list. Those are the keys I use — or all of them, if the deal is marked witness. |
Every gate emits { id, status, detail } as the transaction passes — pass,
fail, refuse, skip or error — and that list comes
back on every decision. A picture of the ball falling through the machine is therefore a rendering
job: the data is already there, waiting to be drawn.
Every refusal is loud, and names its gate. A verdict always says which gate produced it, so an answer you did not expect is traceable to the line that made it.
Authority lives in the rule; a hint is only a shortcut. Filters and notifications narrow what a signer looks at first. The signer then re-checks everything it reads against its own rules, so the verdict is the same either way.
KEA's job ends at the signature: it holds a key, it signs, it posts the signature back. Counting the approvals is the ledger's job.
A Hedera account states its own requirement — "any one of these three", "any two of these three", or "this one, or any two of those three". So a payout can be made to require two separate devices held by two separate people, each contributing a share of the approval. That is a change to the account's key; the signer carries on exactly as before.
The approval rule lives on the ledger and on your devices. "Sign transactions matching these published rules, under these limits, when two of these three devices agree" is an authorisation policy enforced where the funds are, by the ledger itself.
A signer is its own thing, running beside your application. It watches the pool and holds a key your application's accounts depend on. That separation is the point: your application can be rewritten and redeployed as often as you like while the key stays exactly where you put it.
| Shape | Good for | Status |
|---|---|---|
| Node.js process a server, a Raspberry Pi, a phone in Termux | Unattended signing. The usual choice for a service that must keep running. | Working today. |
| Browser / PWA | A person approving on screen, or a device that signs while somebody has it open. Installable straight from the browser. | Working today as a web app; the same decision code as the Node body. |
| Native app | A phone as a dedicated signing device, with the key held by the platform's own secure storage. | Planned. |
| Edge / small nodes | Several cheap signers in different places, so reaching a threshold takes agreement across locations. | Follows naturally — a signer is outbound-only and needs no inbound address. |
All of these run the same decision. The checks in the machine above are one pure piece of code; each shape supplies the surroundings around it — how it hears about a transaction, where the key is kept, how it reads the chain. That is why a phone and a server reach identical verdicts, and why a new shape is a new set of surroundings rather than a new rulebook.
Each signer works alone: it hears about the same transaction, decides for itself, and posts its own signature. Two signers reaching the same conclusion is the point — that is the agreement the ledger counts. When they differ, the dashboard shows you both verdicts side by side.
KEA currently lives inside a larger backend repository and is being separated into its own public one. Everything described above is running today; what is still in progress is the packaging. The shape below is what integrating will look like, and the repository link and package name land here when the split does.
{
"network": "testnet",
"pool": { "base": "https://<the pool>" },
"mirror": "https://testnet.mirrornode.hedera.com/api/v1",
"keysets": { "mine": ["env:MY_SIGNING_KEY"] },
"resolve": ["vendor/<your app>/deals"],
"deals": {
"<dealId>": { "sign": ["mine"], "limits": { "<asset>": 1000000 } },
"<dealId>": { "sign": ["mine"], "witness": true }
},
"report": "refusals",
"presence": { "everyMs": 30000 }
}
Three things in that file carry most of the safety:
deals — a deal's id is the hash of its document, so listing it pins it: the
signer refuses a document that does not hash to the id it was asked for. Updating your rules is
therefore a deliberate act: review, then change one line. No accounts appear anywhere:
which accounts a key answers for is read from the chain, per transaction.limits — the most this signer will approve per asset per hour, whatever the rules allow.
This is the signer's own private budget; a rule can only see the chain, and your signature is not on it yet.keysets — references (env:NAME or a file path), so the config is
a file you can commit, review and share freely. The first key is the device key: it signs the
signer's announcements and verdicts.bin/kea is the launcher (put bin/ on your PATH and it is just kea go).
It defaults to the remote config — a phone is never the box — and to the name this device remembers:
bin/kea name honor remember what this device is called — once, ever
bin/kea check is this device ready to sign? (the doctor: config, keys, chain, pool, doorbell, who else is on)
bin/kea watch only — decide and log, never sign (dry run)
bin/kea go sign what passes
bin/kea go quiet sign, but announce nothing this run
bin/kea who who else is announcing right now
Stopping one: Ctrl-C says goodbye — one last signed announcement, mode: "stopped",
so the board shows stopped rather than a device that silently went stale — and exits.
Ctrl-\ exits at once and says nothing: the quiet restart. The long form is
node kea/node.js --config <file> [--go] [--quiet|--announce]; there is no default config.
A signer can announce itself so it appears on the dashboard — its name, whether it will sign, which keys it holds and which accounts list them, which deals it will sign and with what, and the exact filters it is subscribed to. You turn that on in its config or on its command line. Everything announced is already public on its own; what announcing adds is the correlation — these keys, this device, awake at this minute — which is why it is yours to switch on deliberately. Announcing is observation only: signing carries on the same either way.
The nine checks fall into two halves, and only one of them is worth anyone's attention.
| half | checks | the question | what a refusal means |
|---|---|---|---|
| addressing | policy, authority, signers | Is this aimed at me? | Nothing. It is not a deal, or not one of my deals, or it needs no key of mine. On a pool with many signers, most traffic is this for most signers — a hundred watchers would otherwise post a hundred shrugs against every raise. The signer says nothing and counts it. |
| judgement | inputs, decode, observe, derive, require, reproduce, cap | Is it valid, and will I sign it? | News. This transaction was mine to decide and I turned it down — the rules did not hold, the chain had moved, or it was over my own limit. This is what gets reported and what a dashboard shows. |
The addressing checks run before any chain read, so deciding that something was never yours costs nothing.
A signer chooses how much of its judgement to publish with report: none,
refusals (the default), all, or debug — the last one also publishes the
transactions that were never its business, flagged as such, which is only useful while working out why a
signer is or is not being offered something.
What /kpool/signers returns per device is the signed
announcement, kept whole, plus four things the pool adds. Nothing derived is added beside it: the accounts
are the values of keys, the deal ids are the keys of deals.
| field | who says so | meaning |
|---|---|---|
| by | signed | The device's identity — the key it chose to be known by (its identity ref, else its first signing key). The signature proves this. Everything else in the announcement is this identity's claim. |
| keys | signed | { publicKey: [accounts] } — the signing keys it holds, each with the accounts whose key list the chain showed it in. Claimed, not proven: a signing key is proven by a signature the pool accepts on a transaction. |
| deals | signed | { dealId: [publicKeys] } — for this deal, I will sign with these keys. The signer's config, aliases resolved. |
| subscription | signed | The literal kpool_watch filters on its socket — what it is actually rung for. null from a device that has not pulled the version that says so. |
| mode | signed | go, dry-run, or stopped (it said goodbye). |
| version, startedAt, label, at, network | signed | Runtime and git sha of the code; when it started (a moved start time is flagged restarted); its nickname; the announcement's own clock; the network. |
| sig, v | signed | The ed25519 signature and the wire version it covers. v: 1 means an older device, normalised into this shape by the pool; its signature covered a shape this record no longer holds, so it is not returned. |
| firstSeen, restarted, ageSeconds, fresh | the pool | Bookkeeping: first sight of this key, whether startedAt moved, seconds since at, and whether that is within freshSeconds. |
To verify one yourself (v2): build the JSON object { v, network, label, mode, at, startedAt, version, keys, deals, subscription }
with the keys in exactly that order and the values as returned (startedAt, version, subscription as null when absent),
take JSON.stringify of it as UTF-8 bytes, and check sig against by as a raw ed25519 signature over those bytes.
That is the same primitive that signs a transaction body; the pool did exactly this before storing the record.
| Word | Means |
|---|---|
| deal | A published document saying what you are willing to pay, to whom, and under what conditions. Its hash is its name. |
| catalogue | The list of deals an application publishes — how a signer's operator finds them. The signer itself trusts only the deal ids in its own config. |
| pool / kpool | Where transactions wait for signatures. It assembles and holds them, and keeps them for about an hour. |
| raiser | Whoever asked for the transaction to be built. |
| mirror node | A public read-only view of Hedera. How a signer checks facts for itself. |
| presence | A signer announcing that it is here, signed with a key it claims to hold — so the claim carries its own evidence. |
| dry run | Decide everything, report the verdict, hold the signature. |
| limit | The most a signer will approve per asset per hour, whatever the rules allow — its own private budget, outside the rules. |
| verdict | A signer's signed report of what it decided about one transaction, and at which step it refused — for transactions addressed to it. Observation only; never a vote. |
| addressed | A transaction that got past a signer's first three checks: it is one of that signer's deals and needs one of its keys. Anything else was never that signer's business, and it says nothing about it. |
| witness | Signing a deal's transactions even when none of your keys is required — attesting rather than releasing. |
Everything above says what each part does. These are the questions that tend to follow, and the answers are the boundaries of it.
| Question | Answer |
|---|---|
| Does the pool hold keys? | No. It holds transactions and relays them. It has no key for your accounts and adds no signature of its own, which is why it can be treated as a convenience rather than a trusted party. |
| Can whoever raised the request approve it? | Only if they also hold a key the account requires. Raising and approving are separate steps, and being the one who asked carries no weight at the approval step. |
| Does one signature move the money? | Only when the account requires exactly one. With two of three, a single signature settles nothing on its own — the transaction waits for a second. |
| What if a signer goes offline? | Nothing is stuck by it, so long as enough other approvers remain to satisfy the account. This is the reason to choose the threshold from how many losses you should survive. |
| What if a device is stolen? | That key can approve whatever the account's rule lets it approve alone — which is exactly why thresholds and hourly caps exist. Rotate the account's key list to retire it. |
| Does KEA build transactions? | No. It reads one that already exists and answers yes or no. Building is the venue's job, proposing is the raiser's. |
| Does KEA hold funds? | No. It holds a key for accounts that hold funds. The funds stay in the accounts throughout. |
| Can a bad rule still be approved? | Yes. The signer guarantees the transaction matches the rule; judging whether the rule was sensible is the review you do once, in advance. The hourly cap is the backstop for a rule that turns out to be wrong. |
| Can the venue trick a signer by sending something misleading? | It can send anything it likes. The signer computes the rule's identity from the document, re-reads the facts from the chain, and rebuilds the transaction itself, so what arrives alongside the bytes carries no authority. |
| Is a signer that stays quiet a problem? | A quiet signer signs exactly as much as a talkative one — announcing is observation only. It simply appears on the dashboard under its signatures rather than in the roster. |
| How long does the pool remember? | About an hour from a transaction's start time. Anything you want as a durable record should come from the chain. |