Independent node protocol

Build a BitKuruş-compatible node

BitKuruş is a closed-source reference implementation, but the federation protocol is open. This page documents the wire format, endpoints, validation rules, and onboarding handshake — enough for you to write your own node in any language and join as a trusted peer.

Note — the reference implementation source code is not public. This page describes the protocol contract only; you implement everything from scratch in your stack of choice (Go, Rust, Node, Python, .NET, JVM…).

01

Federation model

BitKuruş is a signed-gossip federation across a small allowlist of trusted operators. Each node owns an independent database and never writes to its peers directly — convergence happens through Ed25519-signed HTTP envelopes that every node verifies, applies idempotently, and forwards on retry.

There is no global consensus algorithm or fork-choice rule. The protocol assumes operators are mutually trusted; safety comes from cryptographic identity, deterministic state, and out-of-band coordination. A new node is accepted only after every existing operator has added its public key to their trusted registry.

  • UTXO-style ledger: tokens are immutable objects with token_id, owner, value (18 decimals), version, and status (active or burned). A transfer burns inputs and creates new outputs atomically.
  • Ed25519 everywhere: user wallets sign transactions; nodes sign every replication envelope and every API response.
  • Canonical JSON: every signature payload is encoded with sorted object keys so signers and verifiers agree byte-for-byte.
  • No auto-discovery: peers are configured manually on every node. Joining requires a coordinated public-key swap.
02

Node identity & canonical JSON

A node has one long-lived identity: an Ed25519 keypair stored only on the operator's servers. Public keys are 64-character lowercase hex; signatures are 128-character lowercase hex (detached signature, 64 raw bytes).

Every signed payload must be serialized with canonical JSON. The rules are:

  • Object keys sorted ascending using a byte-wise (binary) comparison; this matches the behaviour of SORT_STRING in PHP and sort() on UTF-8 byte strings in most languages.
  • Arrays keep their original order — only object keys are sorted.
  • Unicode characters are emitted as raw UTF-8 (no \u00e7 escapes).
  • Forward slashes are not escaped (https://example, not https:\/\/example).
  • Numeric values preserve their zero fraction when emitted from a float (e.g. 1.0, not 1). Money amounts must be carried as decimal strings instead — see below.
  • Money amounts are always 18-decimal fixed-point strings — e.g. "1.000000000000000000". Never emit floats for token values.

An Ed25519 signature is computed as:

signature_hex = lowercase_hex( Ed25519_sign(secret_key, canonical_json(payload_without_signature_field)) )
03

Standard response envelope

Every JSON response from a node — both public reads and peer endpoints — uses the same shell:

{
  "result": "ok",
  "state_hash": "<64-hex sha256 of active token set>",
  "data": { ... endpoint-specific ... },
  "node_signature": "<128-hex Ed25519 signature over canonical JSON of the response without node_signature>"
}

Clients verify node_signature with the responder's public key (looked up via your peer registry). If the signature does not verify, treat the response as untrusted. Errors use "result": "error" and an HTTP 4xx/5xx status; the envelope shape stays identical.

04

State hash

state_hash is the SHA-256 (lowercase hex) of canonical JSON over the sorted list of every active token in your ledger. Burned tokens, transactions, and ledger events are not hashed.

active_tokens = [
  { "token_id": "apple-…", "owner": "<64-hex>", "value": "1.000000000000000000", "version": 1 },
  …  // sorted ascending by token_id
]
state_hash = sha256_hex( canonical_json(active_tokens) )

All peers in agreement on the active token set will compute the same state_hash. Use this for cheap convergence probes — poll GET /api/state on every peer and alert when one drifts for longer than your replication window.

05

Endpoints peers and clients expect

A compatible node must expose at least the following HTTP endpoints under a public HTTPS URL. All responses follow the envelope from section 03.

Peer-only (other nodes call these)

POST /api/peer/replicate
Accept an inbound signed envelope (see section 06). Reject if signature, freshness or trusted-registry check fails.
GET /api/peer/committed-transactions
Return paginated, ordered list of envelopes you have committed locally. Used by new peers to backfill after onboarding and by existing peers to recover from an outage. Each item carries the original signed envelope verbatim so the receiver can re-verify and apply it.

Public reads (wallets, peers, monitors)

GET /api/state
Empty data payload; the value is the signed state_hash in the envelope.
GET /api/network
Signed identity card: node id, public key, configured peers, validator pubkeys, commission wallet, capabilities, total supply cap, commission rate.
GET /api/health
Lightweight liveness — DB ping, queue lag, last replication timestamp.
GET /api/wallet/{public_key}
Balance and active tokens owned by a wallet address.
GET /api/token/{token_id}
Token status (active or burned), owner, value, version, creation metadata.
GET /api/tx/{tx_id}
Transaction status, validation outcome, inputs, outputs, commission tokens.
GET /api/ledger
Paginated ledger events for explorers.
GET /api/ledger/export
Cross-node deterministic JSON snapshot — every compatible node must return byte-identical bytes here. The document is { "meta", "ledger_events", "token_parents", "tokens", "transactions", "canonical_hash" }. canonical_hash = SHA-256(canonical_json({ledger_events, token_parents, tokens, transactions})). Two nodes agree iff their canonical_hash values are equal. Drop per-node fields (autoincrement ids, created_at, node_id, generated_at, public_url). Restrict transactions to status=committed. Restrict events to { token_created, token_burned, fee_burned }validator_commission_minted is excluded because only the race-winning node persists it; the commission data is still available through the deterministic commission Transaction row (sender=validator_commission) and its accompanying token_created event. Canonical event rows expose only { amount, event_type, token_id, tx_id } — the meta column is dropped because peer-sync replay overwrites it on backfill, and every key it could carry (owner, source, validator_node_id, base_tx_id, …) is already present in the Tokens and Transactions sections. Sort every section by content (tx_id, then token_id, then parent_token_id); per-row keys come out alphabetical.

Optional writes

POST /api/tx/submit
Accept a signed transaction from a wallet. Required for active validators; observer nodes may omit this endpoint and return 404 / a 405.
POST /api/wallet/register
Optional: persist a freshly created wallet for explorers. Replicates as a wallet_registered envelope.
06

Replication envelope shape

Every POST /api/peer/replicate body is a JSON object with this exact set of top-level fields. Extra fields are ignored on read but you must not add them when signing — the signature is computed over the canonical JSON of the envelope minus origin_signature.

{
  "kind":              "transaction | issuance | transaction_validation | wallet_registered | peer_fence",
  "origin_node_id":    "node4",
  "origin_public_key": "<64-hex Ed25519 public key of the sender node>",
  "issued_at":         "2026-05-13T16:42:11+00:00",   // ISO-8601 with timezone
  "payload":           { ... kind-specific body ... },
  "origin_signature":  "<128-hex>"
}

Signing rule

signature_input = canonical_json({
  kind, origin_node_id, origin_public_key, issued_at, payload
})
origin_signature = Ed25519_sign(node_secret_key, signature_input)

Acceptance rules a receiving node MUST enforce

  1. Shape: all six top-level fields present.
  2. Freshness: issued_at within the configured TTL (the reference network uses 60 seconds; reject anything more than 5 minutes in the future to absorb clock skew).
  3. Trusted registry: origin_node_id must be in your configured peer/validator list and origin_public_key must match the pubkey you have on file for that id.
  4. Signature: origin_signature must verify against origin_public_key over the canonical-JSON signing input above.
  5. Idempotency: apply the envelope only if the underlying ledger row does not already exist. A duplicate envelope MUST return success — not an error — to keep retries cheap.
  6. Fail-closed: if your trusted registry is empty or misconfigured, reject every inbound envelope.
07

Envelope kinds

issuance

A new token enters the ledger from a permitted source (e.g. the apple-tree distribution). Replicated by the issuing node; receiving nodes mint the same token row.

"payload": {
  "tx_id":    "fruit-node1-29631619",   // globally unique, namespaced by origin
  "token_id": "apple-atn0vwr9oezf",
  "value":    "1.000000000000000000",
  "owner":    "<64-hex receiver address>",
  "source":   "apple_tree",
  "meta":     { ... optional context ... }
}

transaction

A user-signed transfer that has already been committed locally on the origin node. Receivers burn inputs, create outputs, and mint commission tokens deterministically.

transaction_validation

Pre-flight check from the origin node before committing locally. Receivers verify the transaction shape against their current state and return ok or a rejection reason. They do NOT mutate anything on this kind.

wallet_registered

Propagates the first sighting of a new wallet address, so all nodes can list it in explorers and lookups.

"payload": {
  "public_key":          "<64-hex>",
  "first_seen_node_id":  "node4",
  "label":               "Optional display label"
}

peer_fence

Operator-driven peer suspension propagation. Carries the suspended peer's id and a reason; receiving nodes stop sending it new envelopes until lifted.

08

Transaction format (the part the user signs)

Wallets sign a transaction body and submit it to one node. That node — after local validation — wraps the same body in a transaction envelope and replicates it. The body itself is:

{
  "tx_id":      "<client-generated unique id, e.g. uuid or hex digest>",
  "type":       "transfer",
  "sender":     "<64-hex public key of the spender>",
  "nonce":      "<monotonic per-sender counter or random string>",
  "inputs":  [ { "token_id": "<id>", "version": 3 }, … ],
  "outputs": [ { "token_id": "<new-id>", "value": "1.230000000000000000", "owner": "<64-hex>" }, … ],
  "signature":  "<128-hex Ed25519 signature over canonical JSON of the body without `signature`>"
}

Validation rules every compatible node MUST enforce

  • Signature verifies against sender over canonical JSON of the body minus signature.
  • Every input {token_id, version} must point to an active token owned by sender whose version matches the supplied one (optimistic concurrency control).
  • Inputs must not be locked by another in-flight transaction (lock TTL is part of the public capabilities — see /api/network).
  • Sum of output values must not exceed sum of input values (sum(outputs) ≤ sum(inputs)); otherwise the transaction is rejected. Any positive difference is an optional user fee that is burned (fee_burned) — zero by default. The validator commission is not part of this balance: it is minted separately as fresh tokens (inflationary), never deducted from inputs or outputs.
  • Output token_ids must be globally unique; each output must have a 64-hex owner.
  • Commit must be atomic: burn inputs, create outputs, and emit ledger events in a single database transaction. On failure, no row may persist.
  • Before committing, the origin node MUST collect min_peer_validations approvals from peers via transaction_validation envelopes. Default quorum is majority — floor(N/2) + 1 of the configured peer set, including the origin.
09

Validator commission — deterministic, inflationary

Commission is paid by minting fresh tokens to validator wallets, never deducted from user outputs. The rate is published as validator_commission_rate_ppm on /api/network (default 200 ppm = 0.02%) and is identical on every node.

Calculation

total_value      = sum(outputs[].value)         // 18-decimal fixed point
total_commission = floor(total_value * rate_ppm / 1_000_000)  // 18-decimal units

eligible_validators = sort_ascending_by_id( validators_with_a_configured_commission_wallet )
share, remainder    = divmod(total_commission, len(eligible_validators))

for i, validator in enumerate(eligible_validators):
    amount = share + (1 if i < remainder else 0)
    if amount > 0:
        mint( commission_token(validator, parent_tx_id, amount) )

Determinism

Each commission token id and tx id is a hash of the parent transaction id, the validator's node id, and the validator's wallet address. Because every node has the same validator set and the same rate, all nodes compute byte-identical commission rows from the same parent transaction — no extra envelopes required.

If a validator does not yet have a commission wallet configured (or is in the trusted registry but commented out), its share is dropped — it does NOT roll over to other validators on later transactions.

10

Onboarding handshake

There is no auto-discovery. Joining is a coordinated exchange via an out-of-band channel (Signal, email, GitHub issue, conference room) with the existing operators. Plan a short maintenance window so every node updates together.

  1. Stand up your node on a public HTTPS URL. Until you are in the registry, your node serves /api/state with an isolated state_hash; that is fine.
  2. Generate your Ed25519 identity and a commission wallet address (a second Ed25519 keypair used to receive validator commission). Keep both secrets offline; share only the public keys.
  3. Send the operators a join packet: a JSON document with { node_id, public_url, node_public_key, commission_wallet_pubkey, contact }.
  4. The existing operators add you to their peer list, validator pubkey allowlist, and commission-wallet map; restart workers and reload config.
  5. You mirror the same configuration on your node — every peer must appear in your registry the same way you appear in theirs.
  6. Backfill the ledger by paginating GET /api/peer/committed-transactions from any healthy peer, verifying each envelope's signature, and applying it through your own replication path until your state_hash matches the rest of the federation.
  7. Cut over: peers start pushing live envelopes to you and accepting yours. Run a periodic state_hash diff for a few days to catch any subtle implementation gap.
11

Compatibility checklist

Before requesting onboarding, walk through the list below against your implementation. Existing operators will run a matching audit before signing your join packet.

  • Canonical JSON serializer produces byte-identical output to the reference network across the test vectors operators provide (sorted keys, unescaped slashes, raw UTF-8, preserved zero fractions).
  • Ed25519 detached signatures (libsodium / RFC 8032) verify reference envelopes and your signatures verify on reference nodes.
  • State hash computed from your active token set matches a known reference snapshot.
  • Inbound replication enforces all six acceptance rules in section 06 and is fail-closed when registry is empty.
  • Transaction validation, atomic commit, lock TTL, and quorum behaviour match section 08.
  • Commission tokens are minted with the same deterministic ids as the reference network for the same parent transaction.
  • Public endpoints respond with the standard signed envelope (section 03) and a verifiable node_signature.
  • Operational rails are in place: HTTPS, queue worker, retry policy, per-peer suspension, alerting on signature/state divergence, off-host backup of node and commission private keys.
12

FAQ

What language should I use?
Any language with a solid Ed25519 implementation (libsodium, native crypto) and a deterministic JSON encoder. The reference network has been tested against Go, Rust, Node.js, and Python prototypes.
Can I run a read-only observer?
Yes. Observers omit POST /api/tx/submit and stay out of the validator pubkey allowlist. They still pull /api/peer/committed-transactions, verify envelopes, and serve public reads with the standard signed envelope.
What if I lose my signing seed?
Generate a fresh keypair, send the new public key to every operator out-of-band, ask them to swap your entry in their peer / validator registry, then restart workers everywhere. Your existing ledger rows remain valid — only future envelopes need the new identity.
What stops a validator from blocking transactions?
Quorum is majority (floor(N/2)+1). A single validator can refuse to validate, but the origin still gets a quorum from the rest and commits. A coalition larger than the minority can stall the network — this is by design and managed operationally rather than by Byzantine consensus.
Is there a fork-choice rule?
No. The protocol relies on signed gossip, idempotent application, and human alerting on state divergence. BitKuruş is not Byzantine-fault tolerant. If two nodes diverge, operators investigate manually.
What is the supply cap?
Published as total_supply_cap on /api/network (currently 21,000,000 BK$). Implementations must enforce the cap on issuance.
How are operators audited?
/api/ledger/export returns a deterministic snapshot. Anyone can pull it from two nodes, compare canonical_hash, and conclude in O(1) whether they agree. Differences localise to the four content sections. Persistent divergence is grounds for suspension.

Quick reference: endpoints

POST /api/peer/replicate
Inbound signed envelope (kinds: transaction, issuance, transaction_validation, wallet_registered, peer_fence).
GET /api/peer/committed-transactions
Paginated tail of committed envelopes (used for backfill).
GET /api/state
Signed state_hash.
GET /api/network
Identity card, peers, validator pubkeys, capabilities.

Protocol invariants

  • Canonical JSON: sorted keys, raw UTF-8, unescaped slashes, preserved zero fractions.
  • Money: 18-decimal fixed-point strings everywhere; no floats.
  • Envelope signature covers the envelope minus origin_signature.
  • Replication is idempotent; duplicates return success.
  • state_hash is SHA-256 of canonical JSON of the sorted active token list.
  • Quorum is majority of the configured peer set.
  • Commission is inflationary, deterministic, and split evenly across validators with configured wallets.