Call it, check it, run it.
Dendra is a public testnet. Miners, community GPUs and CPUs, answer AI requests through a chat-completions API (/v1/chat/completions). Other miners re-check a random sample of answers, and a miner convicted of cheating loses part of the stake (bond) it locked on chain. Terms are defined in the glossary.
How it works
You send a prompt to the Dendra gateway, the server behind the API. The gateway opens a job on chain and pays its fee, in DNDR, the network's token, from a testnet account it holds. A miner answers with an open model sized to its hardware. The chain holds the fee while the answer is settled (recorded on chain) and, if the job is drawn at random for audit, checked.
- You send a prompt over HTTPS to the Dendra gateway.
- The gateway reads the prompt, runs the content filter and encrypts it to the serving miner.
- The encrypted prompt travels via the relay to a miner.
- The miner decrypts it in memory and answers with an open model.
- The answer comes back the same way.
- The chain holds the fee, a hash of the prompt and an embedding of the answer, never the prompt.
Miners are not validators. Miners answer requests; validators order blocks. Owning a GPU never secures consensus.
Validators order the blocks. They finalise the chain with CometBFT consensus. Read the set on the Explorer, or with curl -s http://testnet-api.dendranetwork.com:26657/validators.
Call the API
Dendra serves the chat-completions API (/v1/chat/completions), so no custom SDK is needed. Any chat-completions client works: only base_url changes.
| Base URL | Key | What you get |
|---|---|---|
https://testnet-api.dendranetwork.com/demo/v1 | none | 200. The keyless demo tier, rate-limited. |
https://testnet-api.dendranetwork.com/v1 | required | 401 {"error": "unauthorized"} without a key; to call without one, use the demo base URL. |
| Anything that answers HTML | — | You reached the website. dendranetwork.com serves pages, not the API. |
# Python, with a chat-completions client package (pip install openai)
from openai import OpenAI
client = OpenAI(base_url="https://testnet-api.dendranetwork.com/demo/v1", api_key="unused")
r = client.chat.completions.create(
model="dendra-network",
messages=[{"role": "user", "content": "Capital of Australia?"}],
)
print(r.choices[0].message.content)
# curl, keyless demo tier curl -s https://testnet-api.dendranetwork.com/demo/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"dendra-network","messages":[{"role":"user","content":"Capital of Australia?"}]}' # keyed tier: same body, plus the bearer token curl -s https://testnet-api.dendranetwork.com/v1/chat/completions \ -H "Authorization: Bearer YOUR_KEY" -H "Content-Type: application/json" \ -d '{"model":"dendra-network","messages":[{"role":"user","content":"Capital of Australia?"}]}'
Set a generous timeout, in minutes rather than seconds. One consumer GPU serves each answer, and requests queue. Latency depends on the capacity online when you call: /capacity lists the hardware operators declare.
The dendra block
A response that a miner served carries one extra block. When streaming, it arrives in the last chunk. No dendra block means no miner served the job: handle that case instead of raising on it.
"dendra": {
"job_id": "job_…", // the job's id on chain
"miner_id": "dm1…", // the miner that answered; null when several did
"committee": ["dm1…"], // assigned to serve the job, not the audit jury
"beacon": "…", // public random seed (VRF) that picked the miner
"settled": true, // false: settlement refused, the answer is unverified
"audit_state": "pending", // always "pending" here: the audit comes later
"cost_udndr": …, // settled cost · 1 DNDR = 1,000,000 udndr
"escrow_udndr": …, // the amount held for the job
"verify": { // for checking later (dendrad: see the glossary)
"proof_endpoint": "/proof", // resolve against https://testnet-proof.dendranetwork.com
"query": "dendrad query jobs get-job job_…"
}
}
After the answer
Every answer settles on chain, but settled is not verified: a random sample of jobs is audited afterwards.
- Held at settlement (hold_bps).
- A later block's random seed draws whether the job is audited (audit_sample_bps).
- Not drawn: released at finality, the miner is paid.
- Drawn: a jury of other miners, seated from present, eligible miners with the audited one excluded; the floor is audit_min_quorum + 1.
- Upheld: the miner is paid. Convicted: the miner is slashed (slash_leak_bps) and the client refunded.
- Too few jurors, a deferred draw, or no verdict within audit_unwind_blocks: the client is refunded and nobody is slashed.
Not drawn, the job is paid at finality (the release of the held fee) with no verdict. The deterrent is that a miner cannot know in advance which jobs will be drawn; the seed comes from the validators' verifiable random function (VRF) while committee_seed_source is 1, after the miner has committed its answer.
To convict, "invalid" verdicts must come from at least two thirds of the anchored seats, rounded up (jury seats drawn and recorded on chain before anyone votes), and never from fewer than audit_min_quorum jurors; the larger of the two is the bar (antievasion.go::auditRelativeBar). They must also come from a strict majority of the voting stake (the bonds of the jurors who voted). The miner is slashed, losing part of its bond, and the client refunded.
No verdict, no slash. When no verdict comes within audit_unwind_blocks, the held fee goes back to the client and nobody is slashed. That happens below the jury floor (audit_min_quorum + 1 present, eligible miners), when the jurors are silent or split, or when the draw stays deferred for lack of randomness. A drawn job's held fee is paid only if its jury, or a governance vote, upholds the answer.
At finality, at genesis values (governable), each 100 DNDR of fee pays 80 to the miner, 7.5 to validators, 4.5 to the treasury and 3 to the team; 5 are burned. A refund burns nothing. A paid job also opens a work subsidy, a reward from the Reserve (DNDR set aside at genesis) that the miner software claims.
hold_bps: share held at settlement. audit_sample_bps: share of jobs drawn. audit_min_quorum: floor of the jury bar; a verdict needs the larger of it and two thirds of the anchored seats. audit_unwind_blocks: blocks before an unresolved job is refunded. slash_leak_bps: share of stake a conviction takes. min_stake: smallest miner bond, in udndr. The fee split is there too. Read them live, never from this page:curl -s https://testnet-api.dendranetwork.com/rest/dendra/jobs/v1/paramsA field missing from that output holds zero. For some,
audit_min_quorum among them, zero selects a default compiled into the chain, not “no floor”. silence_slash_bps has no effect: the only code that reads it, antievasion.go::clawbackPayment, is not called anywhere in production, so nothing is taken for silence by this parameter.Check an answer
The audit lands after the answer, so check later. Look for your job_id in the recent_audits of the public audit feed, https://testnet-proof.dendranetwork.com/proof, shown live on the home page and read by the Explorer, or read the job straight from the chain:
curl -s https://testnet-api.dendranetwork.com/rest/dendra/jobs/v1/job/<job_id>
Its state field carries the same markers as the audit feed. On a node, verify.query is the same read with dendrad. "Verified" means a jury audited the answer and upheld it. A state can carry several markers at once: read them in the table's order.
| What you find | Meaning | What to do |
|---|---|---|
| Not in the feed | Not drawn, outside the feed's window, or its draw deferred. | Read the job; unwound there means refunded. |
unwound | Refunded: no jury concluded in time. Nothing was verified. | Treat the answer as unverified; the fee went back to the client. |
clawed | Rejected: the miner was slashed and the client refunded. | Do not act on the answer. |
vindicated | Audited and upheld by the jury. The strongest state. | Rely on the audit. |
resolved | Closed another way: a re-hearing (the state also carries adjudicated), a governance vote, or a challenge no jury ruled on. | Read the job. A slash_records entry for its miner_id means it was convicted: do not act on the answer. adjudicated with no entry means its jury upheld it. Otherwise, treat it as unverified. |
| None of these | The audit is still running. | Read again later. |
import json
import urllib.request
CHAIN = "https://testnet-api.dendranetwork.com/rest/dendra/jobs/v1"
def audit_outcome(state):
"""How the chain says an audit ended. Markers are whole words: noquorum is not quorum."""
marks = set(state.split("+"))
if "clawed" in marks:
return "convicted" # a jury convicted the miner and the client was refunded: do not act on the answer
if "resolved" not in marks:
return "no_verdict_yet" # not drawn, waiting for a jury, or still open: most jobs stay here
if "unwound" in marks:
return "refunded" # no jury ruled and the fee went back to the client: nothing was verified
if {"vindicated", "quorum"} <= marks:
return "upheld" # a jury at the bar upheld the answer: that jury's verdict, not a proof
return "unclear" # a re-hearing, a governance vote, a challenge no jury ruled on, or an older state: read the job
meta = resp.model_extra["dendra"] # the client package keeps extra fields here
job = json.load(urllib.request.urlopen(CHAIN + "/job/" + meta["job_id"]))["job"]
outcome = audit_outcome(job["state"])
What is checked, and what is not
Verification here comes in tiers, and each tier says what it can see and what it cannot. Nothing below is a count or a state typed into this page: whether a check is switched on is a reading, and the table at the end of the section reads it live.
Tier 1: a jury checks a sample of answers
A random draw (audit_sample_bps) selects settled jobs. A jury of other present miners, with the audited miner excluded, re-reads the answer and votes. A verdict needs votes the same way at the jury bar, the larger of audit_min_quorum and two thirds of the anchored seats, rounded up (antievasion.go::auditRelativeBar), and a strict majority of the voting stake. A conviction slashes part of the stake and refunds the client; an upheld answer releases the held fee.
What this tier does not see: a job the draw does not select is checked by nobody. A juror judges with a model, so a verdict is an opinion of that model, not a recomputation. A drawn job whose jury does not reach the bar is refunded to its client after audit_unwind_blocks, and nothing was verified.
Tier 2: checks that are built and wait on a parameter
- The availability challenge. Armed, every
avail_epoch_blocksblocks each bonded miner must post a VRF proof over that window's challenge, which is the AppHash of the window boundary, beforeavail_deadline_blockshave passed (msg_server_prove_availability.go::ProveAvailability). An absence counts as one failure. A miner withavail_fail_kfailures within a sliding window ofavail_fail_windowwindows is slashed, by a bounded share of its stake (avail_slash_bps, capped byavail_slash_max) that is burned (availability.go::runAvailabilitySlash). It checks that a bonded miner is online and holds its key. It does not check that any inference was done, and it does not check that an ordinary job was computed honestly. The slash is dormant whileavail_slash_bps,avail_fail_koravail_fail_windowis 0.avail_threshold_bpsis read by no consensus code. - The model registry. With
enforce_model_registryon, a commit must name a registered, active model, and when the registry holds a weights hash for it, the hash in the commit must match (msg_server_commit.go::CreateCommit). It compares what the miner writes against the registry. It cannot see which weights ran.
Presence proofs are separate from both: each window a miner proves it is online with a VRF proof, and only present miners are drawn for work or audit. A proof of presence says a key answered, not that any inference was done well.
Tier 3: nothing checks it yet
- A judge is a signed byte. The chain checks who signed a verdict commit and nothing about an inference (
msg_server_commit.go::CreateCommit); the tally reads one value as the accusation and any other as a vindication (antievasion.go::auditVerdictTally). A judge that read the answer and a judge that posted a byte look the same on chain. - That a model ran. Miners can sign a fingerprint of their client code and model weights, and the relay can require it for confidential jobs (
DENDRA_ATTEST_REQUIRE). The shippeddocker-compose.ymldoes not set that variable for the relay, so no miner is required to present one: the attestation gate is dormant. Armed, it would check a signed statement, not the machine. - What a report declares. The hardware, the judge role and the kit version in a capacity report are what the operator writes, signed or not.
- Hardware-sealed tiers. Enclave hardware and secret-sharing across miners are on the roadmap. No job is verified or sealed that way.
Read it live
reading the chain
| Parameter | Reads | Meaning |
|---|---|---|
audit_sample_bps | ... | Share of settled jobs drawn for a jury, in basis points (10000 = 100 %). |
audit_min_quorum | ... | Floor of the jury bar. At 0 the chain uses the compiled floor of 4. |
audit_unwind_blocks | ... | Blocks after which an audit with no verdict is refunded; 0 turns the refund off. |
avail_epoch_blocks | ... | Length of the availability window in blocks; 0 turns the online check off. |
| Availability challenge | ... | Armed only when avail_epoch_blocks, avail_slash_bps, avail_fail_window and avail_fail_k are all above 0, the four the slash function reads (availability.go::runAvailabilitySlash). The parameter check also requires avail_deadline_blocks above 0 once avail_slash_bps is. |
enforce_model_registry | ... | Whether a commit must name a registered, active model. |
audit_judge_model | ... | The model the kit judge reads to choose what it runs. No consensus code reads it: the chain does not constrain which model a judge runs. |
“Unavailable” marks a failed read, not a zero. A field missing from the chain's answer holds zero, because the chain omits zero values. Read the same fields yourself with curl -s https://testnet-api.dendranetwork.com/rest/dendra/jobs/v1/params and curl -s https://testnet-api.dendranetwork.com/rest/dendra/modelregistry/v1/params.
Pick a role
Each role runs different software and risks different things. Pick one before installing. Join with a GPU or with a CPU: a GPU (NVIDIA) mines, and a CPU with enough RAM judges, for now, and serves its share too; a full node or a validator needs no GPU.
Miner
Answers requests with a local open model, paid from their fees.
Needs an NVIDIA GPU to mine with a model sized to the card: the hardware probe picks it. For a machine without a GPU, kit v0.2.2 keeps the rule that kit v0.2.1 set: it joins only as a judge (next box, and the rule below). Risks its bond, if a jury convicts a drawn answer.
Judge
A miner run with --judge: it also votes when drawn onto a jury, which lets audits conclude. The chain draws jurors among all present miners, so a miner without an active judge is drawn all the same, and its seat casts no vote: a silent seat. A juror is a registered miner, so a judge also serves requests, on its GPU if it has one.
Needs at least MOE_CPU_MIN_RAM_MB of system RAM, the kit's judge floor (the report of bash deploy/hw_probe.sh prints it, and from kit v0.2.1 bash deploy/hw_probe.sh --judge-floor-mb prints it alone), with or without a GPU: the judge runs on the CPU, with the model the kit reads from the model registry (audit_judge_model, which no consensus code enforces), a mixture-of-experts. Under Windows, that is the RAM of WSL 2, about half of the PC's by default; the memory= line of the [wsl2] section of %UserProfile%\.wslconfig raises it. The installers take this role by default and refuse it under 32 GB of free disk: pass --miner (-Miner on Windows) to install a miner only. The probe in Run a miner, step 1, decides. Risks the same bond.
Validator
Orders blocks and adds to the draw's randomness; answers no requests.
Needs an always-on server (a VPS), no GPU, and port 26656 reachable: bash deploy/node_reachability.sh checks it. Risks its bond: long downtime gets it jailed (dropped from block production) and slashed.
How: an installer with --validator (-Validator on Windows), or the by-hand join.sh command of Run a miner, step 3, with --validator in place of --judge. Step by step: node and validator.
Full node
Follows the chain and serves RPC and REST. Signs nothing.
Needs no GPU. Risks nothing, earns nothing.
How: deploy/testnet-node/ in the repository.
MOE_CPU_MIN_RAM_MB of system RAM, and it answers the requests drawn for it with the judge's model, not with a mining model. With no GPU and less RAM, those kits refuse to join, and leave untouched a miner an earlier kit installed. From the instant KIT_DEADLINE names (read it with curl -sf https://testnet-api.dendranetwork.com/network-info.txt), the relay refuses a kit below KIT_MIN, and from the season day that kit_rules_from_day names (curl -s https://testnet-api.dendranetwork.com/final-season/v1/status) the season pays it nothing. Such a machine leaves the network with deploy/testnet-miner/exit-miner.sh (bash deploy/testnet-miner/exit-miner.sh --help prints the steps). The chain cannot see the hardware: a miner of an earlier kit keeps running and keeps being drawn, and as a juror it votes nothing.More roles and technologies for CPU-only machines are planned.
Stake and the work draw
Every miner locks a stake (bond) of at least min_stake to take work. The chain draws who serves each request among present miners, weighting each identity by its stake up to a ceiling of assignment_stake_cap_multiple × min_stake (committee.go::capAssignmentWeights). At a multiple of 1, a stake above min_stake buys no more work. Governance sets the multiple: proposal 1 on this testnet proposes 1, and its status is read with curl -s https://testnet-api.dendranetwork.com/rest/cosmos/gov/v1/proposals/1. Read the value in force rather than this page:
curl -s https://testnet-api.dendranetwork.com/rest/dendra/jobs/v1/params # assignment_stake_cap_multiple, min_stake; a field missing from the output holds zero: # a multiple of 0 selects the compiled 2, a min_stake of 0 sets no ceiling
The faucet's drip size is the amount in its status (step 2), next to amount_source, where it comes from. The faucet code of kit v0.2.1 can read it from the chain's min_stake (DENDRA_FAUCET_AMOUNT=min_stake), so that one drip bonds one identity. The audit jury is drawn by stake too, and that draw does not read the ceiling (audit_committee.go::drawMembersWithDomain). A stake earns no Final Testnet Season reward by itself.
Run a miner
Four steps, one of them optional, take a machine from nothing to serving requests.
- Check: hw_probe.sh decides what your machine can serve.
- Wallet, optional: an address for your rewards; the miner funds its own key from the faucet.
- Install: install.sh on Ubuntu, Debian or HiveOS; install.ps1 on Windows, via WSL 2; or join.sh by hand from a clone. Both scripts hand over to join.sh; install.sh adds the desktop app.
- Serve: bond at least min_stake, then answer jobs; with --judge, which the installers pass, the node also posts a verdict when it is drawn onto a jury, if the probe allows it.
- Your miner only opens outbound connections, to the relay, the chain and the faucet: no inbound port to open, and a miner behind a home router (NAT) is fine.
Check your machine
The probe decides what your machine can serve, and the installers run it for you. To see its answer first on Linux, clone the repository and run it: it prints a role line, mine only or mine + judge. On Windows, skip this step:
install.ps1runs the probe once WSL, the Linux layer it installs on Windows, is ready.git clone https://github.com/DendraNetwork/dendra-network.git && cd dendra-network bash deploy/hw_probe.sh
Optional: a wallet for your rewards
You need no wallet to mine. When it starts, the miner creates its own key and funds it from the public faucet, a service that hands out testnet DNDR. Job pay goes to that key. Final Testnet Season rewards go there too, unless you declare another address, such as one made on the wallet page (how: Final Testnet Season). Your wallet balance is not your season reward: only what the season pays counts (what counts).
A wallet that only receives rewards needs no funds. If you fund one anyway, the faucet's limits apply per address, per IP and overall;
429means one is reached (which one, and what to do).Use a dedicated testnet wallet. Never reuse a recovery phrase that controls real funds.Fund a wallet by hand: the proof-of-work claim
Ask the faucet first: its reply gives the drip size and the difficulty of a small proof of work, a hash puzzle that slows abuse. After a claim, check the credit in the wallet.
# difficulty and drip size, straight from the faucet curl -s http://testnet-api.dendranetwork.com:4500/ # prints {"status": "ok", "from": "...", "amount": "...", "pow_bits": ...} # mine the nonce, then claim python3 - <<'EOF' import hashlib, itertools, json, urllib.request ADDR, BITS = "dendra1...", 20 # BITS = pow_bits reported above for n in itertools.count(): h = hashlib.sha256(("%s:%d" % (ADDR, n)).encode()).digest() if int.from_bytes(h, "big") >> (256 - BITS) == 0: break req = urllib.request.Request("http://testnet-api.dendranetwork.com:4500/", json.dumps({"address": ADDR, "pow": str(n)}).encode(), {"Content-Type": "application/json"}) print(urllib.request.urlopen(req).read().decode()) EOF
Install
Pick one path. Each ends in
deploy/join.sh, which joins the network. Download, read, run: take the script from the repository and read it first. The installers change nothing until you pass--yes(-Yesin PowerShell). There is nocurl … | sh: it would run bytes you never saw.Ubuntu, Debian, HiveOS.
deploy/install.shinstalls Docker, and the NVIDIA container toolkit when a GPU is present, clones the repository, then runsjoin.sh. On a desktop session it adds the Dendra app, which shows your recovery phrase and earnings.bash install.sh # plan only, changes nothing bash install.sh --yes # install, then join
Windows.
deploy/install.ps1installs WSL 2 and Ubuntu, then runsinstall.shinside it. Run it from an administrator PowerShell, in the folder that holds it. It stops for a restart, then for Ubuntu's first launch: run it again each time.powershell -ExecutionPolicy Bypass -File .\install.ps1 # plan only powershell -ExecutionPolicy Bypass -File .\install.ps1 -YesBy hand. Read the script in your clone (quit the pager with
q), then run it.less deploy/join.sh CONFIG_URL=https://testnet-api.dendranetwork.com/network-info.txt bash deploy/join.sh --judge
The genesis check.
join.shchecks the genesis it downloads against theGENESIS_SHA256line ofnetwork-info.txtand againstdocker/GENESIS_SHA256in your clone, and stops on a mismatch. By hand:curl -s https://testnet-api.dendranetwork.com/genesis.json | sha256sum, then compare it with both. This page prints no digest, since a reset changes it: compare against the two sources above.Serve
The miner registers and bonds by itself: by default
min_stake, read from the chain (parameters).Success looks like
[OK] miner alive and registeredat the end ofjoin.sh. Follow it from your clone (~/dendra-networkwhen an installer made it; on Windows, inside Ubuntu):docker compose -f deploy/testnet-miner/docker-compose.yml logs -f miner. "Waiting for jobs" is normal.Back up your identity. The Docker volume
dendra-miner_miner-keysholds your miner's key: delete it and you need a new bond. How to back it up and encrypt it: Back up and encrypt your keys. To leave the network: Leave and get the stake back.Judge too. The installers pass
--judge, as does the command above:join.shthen judges if the probe allows it, and only mines otherwise. The judge runs on the CPU, whether or not the machine has a GPU: the probe allows it fromMOE_CPU_MIN_RAM_MBof system RAM (bash deploy/hw_probe.sh --judge-floor-mbprints it).
Operate your miner
Tasks for a miner that is already running. Run them from your clone (~/dendra-network when an installer made it; on Windows, inside Ubuntu). The exit and the key encryption print what they would do and change nothing until you add --yes.
Leave the network and get the stake back
exit-miner.sh deregisters the miner on chain (delete-miner). The chain returns the remaining stake to the key that registered the miner, minus anything slashed earlier. Run it once without a flag: it reads your miner and the chain, asks the chain whether it would accept the exit, and changes nothing.
bash deploy/testnet-miner/exit-miner.sh # read and simulate, changes nothing bash deploy/testnet-miner/exit-miner.sh --yes # drain, then leave if the chain accepts bash deploy/testnet-miner/exit-miner.sh --yes --wait # keep asking until the chain accepts
With --yes the script first sets the drain: the miner takes no new work and no new jury seat, and it still files the reveals for its own audited jobs. Keep a draining miner running.
The chain refuses while the miner holds an obligation: a fee held on a job it served, a seat on an audit that has not resolved yet, a job under dispute, or an answer that is not settled. The script prints the chain's own message and lists what it can read of what holds the miner. A refusal is a reading to repeat later, not a failure, and --wait repeats it for you.
After the exit the miner stays stopped. Starting it again registers it again and stakes again. To take the kit off the machine, run bash deploy/uninstall.sh: it keeps the miner's keys unless you pass --delete-keys, and it makes a verified backup of them before it removes anything.
Owner mode. When the stake sits on an owner key that is not on this machine (below), the script never signs with this machine's key. With --yes it prepares delete-miner.json next to the script and prints the command that signs it offline, on the owner's machine. A second run with --yes --signed delete-miner.signed.json checks the file, sends it and reads the chain back.
Several cards. With one identity per card (below), --slot <k> names the identity that leaves. On such a machine --yes without --slot is refused, because an exit is never taken on slot 0 by default.
Back up and encrypt your keys
The miner's identity is its key: the chain derives the miner id from the key's address, so a lost key is a lost identity, with its stake. The keys live in the Docker volume dendra-miner_miner-keys: the key that signs and holds the stake, the encryption key, the VRF key and the attestation key. Never run docker compose down -v or docker volume prune on a machine that holds it.
- New installs are encrypted.
join.shcreates a random passphrase file,~/.config/dendra/miner-secrets/keyring-passphrase(directory and file readable by you only), outside the volume and outside the clone.--plain-keyskeeps a new install unencrypted, on purpose. - Keys that are not encrypted yet stay so until you run the migration. It keeps the same address.
- The 24 words. The miner keeps the recovery phrase of a key it creates. The Dendra app shows it. Write it on paper: it is the only way back if the passphrase file is lost, and the migration refuses to run until you have.
- Back up both the volume and the passphrase file, and keep the copies apart: together they are an unencrypted key. Without the passphrase an encrypted keyring does not open, and the miner stops with that reason instead of creating a new key.
bash deploy/testnet-miner/encrypt-keys.sh --yes # encrypt keys that are not (run it once without --yes first) bash deploy/testnet-miner/backup-keys.sh backup # one encrypted archive of the volume and the passphrase file bash deploy/testnet-miner/backup-keys.sh verify ARCHIVE # open it again and check it
backup-keys.sh asks for a passphrase of its own, reads the archive back and compares it with the volume before it says it is written. Copy the archive off the machine. restore puts it back and never writes over a key; restore before the miner first starts on a new system, since a miner that starts on an empty volume creates a new identity.
Encryption protects the volume and every backup of it. It does not protect a machine that is compromised while the miner runs: the passphrase sits on the same machine, so that the miner restarts without you.
Where rewards go: payout address and owner mode
Without a declaration, rewards go to this machine's own key, which anyone who copies the machine's keys can spend. Declare an address whose key is not on this machine, such as one made on the wallet page.
# at install: the checksum is verified before anything changes CONFIG_URL=https://testnet-api.dendranetwork.com/network-info.txt bash deploy/join.sh --payout-address dendra1... # later, from deploy/testnet-miner docker compose exec miner python3 final_season_miner.py payout --address dendra1...
The miner declares the address once it is registered, and again only when it changes. The Dendra app has the same step on its Where rewards go card. Add --payout-lock to join.sh (or --lock to the command above) and the programme holds the address for good: any other address is refused for that identity, and the lock cannot be lifted. What counts toward the season is on the Final Testnet Season page.
Owner mode is an advanced option for an operator with a second device that holds a key and runs dendrad to sign offline. join.sh --owner dendra1... puts the stake on that key:
- The owner key creates the miner, supplies the stake, is the only key that updates or deletes the miner, and receives the stake back on exit.
- This machine's key is the operator. It signs the commits and availability proofs, and the chain pays it.
- The miner never sends
create-minerin this mode. It writes the unsigned transaction and prints three complete commands: copy it out, sign it on the owner's device, broadcast it from this host. Read them withdocker compose -p dendra-miner logs miner | grep -A16 'OWNER MODE'. The miner works once the chain records this machine's key as the operator. - A payout declaration in this mode is not signed by this machine:
final_season_miner.py payout-preparewrites the transaction, you sign it on the owner's device, andpayout-submitsends it. - The miner's identity derives from the owner address. A wrong owner is another identity, so check the address before you run it.
Publish your hardware
The network page shows the hardware that miners declare. join.sh schedules the publisher (publish-capacity.sh) with an hourly cron job and runs it once the registration is seen. The registry ages out a report that is not renewed, so the repetition matters: a miner that stops publishing drops off the capacity listing.
- The report is signed, and it is sent only after the chain records this identity with the signing key as its operator. Until then nothing is sent and the next run asks again.
- It runs on the host, not in the container, because the container has no GPU. The cron line uses
bash -lc: without a login shell the probe may not findnvidia-smi(under WSL it lives in/usr/lib/wsl/lib) and would publish a smaller tier than your card. - The registry address comes from the node address, and only when that is a host name. On a bare IP address nothing is derived; set
DENDRA_CAPACITY_URLto the registry's address. - The public chat does not count these reports. It counts the miners registered on chain with a presence proof the chain accepted.
bash deploy/testnet-miner/publish-capacity.sh # every active identity of this machine bash deploy/testnet-miner/publish-capacity.sh --slot 1 # one identity, with one identity per card cat deploy/testnet-miner/publish-capacity.log # what the hourly job said
Exit code 0 means published and signed. 2 means the chain records no such miner, or records it for another operator. 3 means the registration could not be read, and it is never taken as registered. 1 is a configuration or probe failure, or a refusal by the registry. If your host has no crontab, join.sh prints the command to schedule yourself.
Several cards in one machine
By default one machine is one miner identity. With --gpus, join.sh gives each NVIDIA card an identity of its own, called a slot, with its own keys, its own 24 words and its own stake. Slot 0 is the installation as it always was; the others are named dendra-miner-g1, dendra-miner-g2, and so on. Add --plan first: it prints what would start and changes nothing.
CONFIG_URL=https://testnet-api.dendranetwork.com/network-info.txt bash deploy/join.sh --gpus all --plan
CONFIG_URL=https://testnet-api.dendranetwork.com/network-info.txt bash deploy/join.sh --gpus all
bash deploy/testnet-miner/slots.sh list # slot, project, state, card, miner id
--gpustakesall, card indices (0,2) or card UUIDs. A card that leaves the list has its slot stopped. A card that is no longer in the machine is not started and not retired: nothing is bound to another card in silence.- To bind an existing slot to another card on purpose:
--reassign <slot>=<card index or UUID>. The identity, the stake and the keys do not move. join.shmeasures the free RAM before it starts each added slot, and the judge is counted once per machine.- Every slot but slot 0 declares itself as a secondary share of the host, so the registry counts the machine's RAM and cores once.
- Key tools take
--slot <k>:encrypt-keys.shalso takes--all, andexit-miner.shneeds it with--yes. Back up the volume of every slot:slots.sh listnames them.
When docker compose up fails
join.sh ends with "docker compose up failed" when Docker refused to start a container. The reason is in the output just above that line. The usual ones:
| What you see | What it means | What to do |
|---|---|---|
could not select device driver | The GPU override asks Docker for an NVIDIA card, and Docker cannot give one. | Run join.sh again: it removes the override when no card is usable. To mine on a card, install the NVIDIA container toolkit (install.sh does it when it sees a GPU), then check with the command join.sh prints. |
| The pinned image could not be pulled | The registry is down, unreachable or private. | Nothing to do: join.sh builds the same image from your clone instead. It takes longer and needs free disk. |
The node's preflight exits with code 78 and says Refusing to start | The node's volume already holds a chain home that this copy of the kit was not told to use, for example one left by an earlier network. | Read docker compose -f deploy/testnet-node/docker-compose.yml logs preflight node; join.sh prints the last lines of it. If the home is yours, from a node you started by hand, say so once: echo 'DENDRA_ADOPT_EXISTING_HOME=1' >> deploy/testnet-node/.env, then run join.sh again. If it came from another network, do not. |
A machine without Docker or the Compose v2 plugin never gets this far: join.sh stops at its pre-flight with [FAIL] Docker/Compose v2 missing, before it starts anything. Install Docker, or, on an apt-based system (Ubuntu, Debian, HiveOS), run install.sh.
Free disk matters too: the served model, the images and the chain state land under the clone and Docker's data root. If the output does not match any row, ask in #operator-support on the Discord and paste the lines above the failure.
When a request is refused
The faucet and the relay name the reason when they refuse a request, and their reply carries a help field with the address of the section here or on the update page that settles it.
The faucet's limits
The faucet protects its budget with three limits, all set by its operator: one drip per address in a cooldown, a number of drips per IP address per day, and a total for the whole faucet per day. This page prints no value, since the operator can change them. A refused request spends none of them. The reply is a 429 and its info says which one you met:
info | What it means | What to do |
|---|---|---|
address funded recently (cooldown) | This address got a drip within the cooldown. | Wait. A miner that is registered already holds its stake, and a wallet that only receives rewards needs no funds. |
too many requests from this IP (daily quota) | This IP address used its drips for the day. Machines behind one home router share an IP address, and a rig asks once per identity. | Wait, then run join.sh again. |
global daily cap reached (anti-drain) | The faucet has paid out its total for the day. | Try again later. Nothing on your side is wrong. |
A 403 with stale kit is not a limit: the kit is below the network's floor. Update it: two commands. A 400 with invalid kit_version means a client wrote the kit number in a form that is not a whole number; the kits write it correctly, so update the kit or fix the client.
Why the relay refused a write
A refused write gets a 401 with four fields: error, why, how and help. why starts with the reason word and ends with a detail. Your miner logs the relay's own reason once per route; read it before you suspect the token. Reading the work queue needs no token, so a refused write does not hide the jobs from a miner. A reason that starts with STALE_KIT points to the update page.
| Reason | What it means | What to do |
|---|---|---|
STALE_KIT | The signed body declares a kit version below the network's floor, or declares none. | Update the kit. |
unsigned, malformed, MALFORMED, UNREADABLE_HEIGHT | The write has no signature headers, a header or the signed message cannot be read, or the signed message has no height. | Update the kit. A miner started outside the kit needs a signing key (DENDRA_SIGN_KEY). |
off | The relay does not check signatures (DENDRA_RELAY_SIGN=off), so it accepts only a write that carries its shared token, and this one carried none or a wrong one. | Send the relay's token in X-Dendra-Token, or ask the relay's operator to arm signature checking. |
INVALID_SIGNATURE | The signature does not cover this message: the body or a header changed after signing. | Check that nothing between the miner and the relay rewrites requests. |
KEY_MISMATCH | The key files the deposit under one miner and the header names another. | Keep each identity's own keys. On a rig, each slot signs as itself. |
FOREIGN_KEY | The signature is valid, but its key is not the operator the chain records for this miner. | Sign with this machine's key, the one in the miner's volume. |
UNKNOWN_MINER | The registry the relay read does not list this miner. | Wait until the registration is on chain. The relay reads the registry again after an unknown miner. If the miner is not registered, read its log. |
REGISTRY_MUTE, JOB_REGISTRY_MUTE | The relay has never read the registry or the job list, so it can attribute nothing. | It is on the relay's side. Try again later, then ask in #operator-support. |
REPLAY | The relay already accepted these exact bytes. | Nothing, for a deposit a miner makes again at every start: the relay keeps the first copy. |
HEIGHT_REGRESSION, TOO_OLD, TOO_FAR_AHEAD, UNSEALED_HEIGHT | The signed height is below the highest one accepted from this key, or, where the relay has a chain head to compare with, outside the freshness window. | The next write carries a current height. If it repeats, ask in #operator-support with the log line. |
A client that pays for its own request signs a req deposit, and the relay checks the job as well. Most readers never meet these reasons: the public gateway is the one that writes requests.
| Reason | What it means |
|---|---|
UNKNOWN_JOB, FOREIGN_CLIENT | The job is not in the list the relay read, or the key is not the account that paid for it. |
PRICE_UNSET, UNDERPAID | This relay sets no request price, so it takes no signed request, or the job escrowed less than the price. |
FEE_UNREADABLE, JOB_STATE_UNREADABLE, COMMITTEE_UNREADABLE | The relay could not read the job's fee, its state or its committee from the chain. |
JOB_NOT_OPEN | The job no longer waits for its request: it is settled, paid, expired or disputed. |
UNASSIGNED_MINER | The deposit names a miner that is not in the committee the chain assigned to the job. |
NOT_ATTESTED | The relay requires attestation (DENDRA_ATTEST_REQUIRE) and this miner is not attested. |
EMPTY_ATTRIBUTION | The write verified but named no address. It is a relay fault: report it. |
Three refusals have no reason word. 429 rate limited: one address sent too many requests in a short window, so slow down. 401 unauthorized (X-Dendra-Token): the route takes the shared token and the request carried none or a wrong one. 403 miner not attested: the relay requires attestation and this miner is not attested.
Privacy and security
Your prompt is encrypted from the gateway to the miner; on chain, a job keeps a hash of the prompt and an embedding of the answer, not their text. The relay between them carries ciphertext only. Besides your device, three parties read the prompt and the answer to do their part: the public gateway, which filters the prompt and returns the answer to you; the serving miner, which writes the answer; and, if the job is drawn for audit, the jurors, who judge it.
From kit v0.2.1 the miner seals that reveal for the jurors the chain anchored for the audit and for no other miner; a miner on an earlier kit seals it for every other registered miner. Each copy is sealed to the encryption key the juror registered on chain; for a juror that registered none, to the key the relay serves for it, which a relay that replaces that key can open.
Outside the project, a prompt reaches the public network only through the public gateway: handing it to a miner is a write on the relay's request route, which takes a token the project does not distribute (relay_server.py, Handler._policy). A gateway you run yourself serves your own stack, a separate chain served only by the miners you attach to it, until the relay opens that route to other gateways.
- You: plain text on your device, sent over HTTPS.
- Dendra gateway: reads it, runs the filter, then encrypts it to the miner.
- Relay: ciphertext only; it passes the prompt on and cannot read it.
- Miner memory: reads it to answer it, guarded by the miner software.
- Jurors: if the job is drawn for audit, they read the prompt and the answer to judge it. From kit v0.2.1 only they receive them; a miner on an earlier kit seals them for every other registered miner.
- Chain: a hash of the prompt and a public embedding of the answer, never the prompt.
- Outside the project, a prompt reaches the public network only through the Dendra gateway. A gateway you run serves your own stack, a separate chain served by the miners you attach to it, until the relay opens its request route to other gateways.
In force
- Real bonds, slashed on chain when a jury convicts.
- A verdict needs a strict majority of the jury's voting stake on its side.
- The relay sees only encrypted data, and the chain stores no prompt or answer text.
- No module can mint: the supply can only fall.
Live slash and audit counts: slashes.events, jobs.resolved_by_quorum and jobs.deferred_no_jury on https://testnet-proof.dendranetwork.com/proof, with the height they were read at.
Protocol details and sources
- Conviction rule:
antievasion.go::auditSlashDecision. - The juror abstains on a missing reveal (the prompt and answer the miner re-encrypts for its jury, from kit v0.2.1 for the anchored jurors only):
judge_worker.py,NOREVEAL_ABSTAIN. - Work-draw cap,
assignment_stake_cap_multiple×min_stake:committee.go::capAssignmentWeights. Audit draw:audit_committee.go::drawMembersWithDomain. - The draw pool is frozen when a job opens: a miner registered later cannot be drawn for it:
pool_freeze.go::frozenPoolAnchor,minerInFrozenPool. - Fixed supply:
app/fixed_supply_test.gofails if a minting module or a minter permission appears.
Reference
Endpoints
Paths are on https://testnet-api.dendranetwork.com. Rows that start with a port are on http://testnet-api.dendranetwork.com:<port>. Each row shows what a working route returns.
| Endpoint | What it is | Key | Code |
|---|---|---|---|
POST /demo/v1/chat/completions | inference, keyless demo tier | no | 200 |
POST /v1/chat/completions | inference, keyed tier | yes | 401 without |
GET /demo/v1/models · /v1/models | model list, keyless · keyed | no · yes | 200 · 401 without |
GET /health | gateway status | no | 200 |
GET /capacity | hardware inventory, operator-declared | no | 200 |
GET /rpc/status · /rpc/validators | CometBFT RPC through the gateway | no | 200 |
GET /rest/cosmos/… · /rest/dendra/… | Cosmos REST through the gateway | no | 200 |
https://testnet-proof.dendranetwork.com/proof | the proof feed: read-only audit outcomes | no | 200 |
:26657/status · :26657/validators | CometBFT RPC, direct | no | 200 |
:1317/cosmos/bank/v1beta1/supply · :1317/dendra/jobs/v1/params | Cosmos REST, direct: supply and parameters | no | 200 |
GET /network-info.txt · /genesis.json | join configuration and genesis file, over HTTPS | no | 200 |
:8088/genesis.json · :8088/network-info.txt | the same files, in plain HTTP | no | 200 |
:4500/ | faucet: drip size and proof-of-work difficulty | no | 200 |
:8645/ | encrypted relay between the gateway and the miners | — | 401 |
Token facts
| Fact | Detail |
|---|---|
| Supply | 10,000,000 DNDR, fixed. |
| Unit | udndr: 1 DNDR = 1,000,000 udndr. |
| Burn | A share of every final fee, fee_burn_bps: 5 % at genesis, governable. A refunded job burns nothing. |
| Rewards | Released from a pre-allocated Reserve at reserve_release_bps per epoch, a fixed span of blocks: how fast that pool drains, never a yield. Apart from the Final Testnet Season's published rewards, no parameter is a rate, yield or return. |
| No sale | No token sale, presale or ICO; testnet DNDR is never sold. Faucet grants, the stake, transfers, on-chain job pay and the subsidy are not Final Testnet Season points. |
Check the supply: curl -s https://testnet-api.dendranetwork.com/rest/cosmos/bank/v1beta1/supply returns one denom, starting at 10000000000000 udndr. Allocation and emission: litepaper.
Glossary
- Client
- The account that paid for a job. Behind the public gateway, it is an account the gateway holds.
- Genesis
- The file that sets the chain's starting state and parameters. "At genesis" means as launched.
- Escrow
- The client's fee, locked by the chain (in a module account) when the job opens.
- Settlement
- The answer is recorded on chain; the fee is still held.
- Finality
- The fee is released, to the miner or back to the client.
- bps
- Basis points: 10000 = 100 %.
- Bond (stake)
- The DNDR a miner locks on chain to take work. A conviction takes part of it.
- Slash
- The share of a convicted miner's bond taken on chain (
slash_leak_bps). - Draw (VRF seed)
- A random pick made from a per-block seed, the VRF seed: a random value the validators produce and anyone can verify. The work draw assigns a job to a miner; the audit draw picks the jobs to audit and their juries.
- Deferred draw
- An audit draw postponed because its block lacks enough randomness contributors (
committee_min_vrf_contributors) or too few jurors can be seated. The chain retries; a job still unjudged afteraudit_unwind_blocksis refunded. - Present miner
- Proved it was online, or registered, in the current or previous window of
avail_epoch_blocksblocks. - Eligible miner
- Anchored an answer, or registered, recently enough to sit on a jury.
- Jury floor
audit_min_quorum+ 1 present, eligible miners: the voters plus the audited miner, who never judges itself.- Voting stake
- The bonds of the jurors who voted on an audit.
- Reveal
- The prompt and answer that an audited miner re-encrypts for its jury: from kit v0.2.1 for the jurors the chain anchored for that audit only, and from an earlier kit for every other registered miner.
- LLM-as-judge
- A language model that a juror runs to grade another miner's answer.
- Relay
- The service that carries encrypted prompts and answers between the gateway and the miners. It cannot read them.
- Embedding
- A list of numbers that sums up what a text means.
- Subsidy
- A work reward from the Reserve that a paid job opens.
dendrad- The chain's command-line client, inside the node container that
join.shstarts:docker compose -f deploy/testnet-node/docker-compose.yml exec -T node dendrad …
Further reading
Litepaper
Why the design holds: verification, the token and the security model.
Explorer
The chain's live state: validators, jobs, held fees, parameters.
Proof feed
Recent jobs and audit outcomes, read live from the read-only proof feed.
Source on GitHub
Apache-2.0: the chain, the miner, the gateway, the judge.