Become a Witness
21 witnesses scheduled on 2026-11-03; up to 21 elected, 1 required. For the witness repository at commit 704a68f, with pixadock/pixagram:1.30.1 and pixarex/bigmac-feed:v1.3.0. Steps 1 to 4 were run on 2026-10-05 with 1.30.0, which 1.30.1 replaced on 2026-10-15 without a hardfork; the feed's dry runs on 2026-11-03. Commands that need a registered witness were not run.
A witness runs a node that produces blocks in turn, publishes a price feed every hour and asks stakeholders for their votes. What witnesses decide, how they are elected and how they are paid is on Witnesses and DPoS. This page is the procedure: keys, node, registration, price feed and upkeep. pixa.org's node operator guide walks through the same setup, together with an API node, from renting the server onward; this page pins the versions and gives the expected output of each step.
What you commit to
- A node that never sleeps. Every block you miss is public and permanent in your
total_missedcount, and missed blocks are not paid. - An exact clock. A slow clock costs the next witness its block (below).
- A price feed every hour. Feeds older than 7 days stop counting.
- Upgrades before hardforks. After activation, a witness on an older version can no longer produce valid blocks (Protocol Upgrades).
- A public page. Stakeholders vote for people they can identify: say who runs the node, where, and how to reach you.
Before you start
- A server as in Run a Node: 2 vCPU, 4 GB, 50 GB SSD, x86-64. Prefer a provider and a region the other witnesses do not use: a chain is only as resilient as its least diverse failure.
- An account with its active key at hand. Create it in the app if you have none (Create an Account).
- A page to register as your witness URL, for example a post on Pixagram.
The two keys
| Key | Signs | Lives in |
|---|---|---|
| Block-signing key, new, made in step 1 | Blocks, and witness_set_properties updates to your witness's own settings (URL, price feed, parameter votes). It cannot move funds. |
pixagram/config.ini on the witness server |
| Active key of your account | The registration (witness_update), the price feed (feed_publish) and every transfer |
Your password manager, and the feed's .env file |
- Do not mix them up. The active key in
config.iniproduces no blocks; the signing key in.envpublishes no feed. - Guard both. Whoever holds the signing key can produce blocks and change your witness's settings in your name. Whoever holds the active key can move your funds. Both files get
chmod 600, and neither is ever committed or pasted into a chat. - The owner key never goes on a server.
1. Create a signing key
Generate it offline, with the wallet inside the node image. Nothing is sent anywhere.
Shelldocker run --rm -it --entrypoint /home/hived/bin/cli_wallet pixadock/pixagram:1.30.1 -o
At the new >>> prompt, type:
suggest_brain_key
Expected output (your values differ):
JSON{
"brain_priv_key": "<16 words>",
"wif_priv_key": "5…",
"pub_key": "PIX…"
}
Type exit to leave the wallet. Store all three values with your other keys: wif_priv_key goes into config.ini in step 2, pub_key goes on chain in step 4, and the 16 words recreate both. More on the wallet: CLI Wallet.
2. Set up the node
Shellgit clone https://github.com/pixagram-blockchain/witness.git
cd witness
grep 'image: pixadock/pixagram' docker-compose.yml
Expected output: the line image: pixadock/pixagram:1.30.1, indented. The repository pins every image version; this page describes its commit 704a68f.
Fill in pixagram/config.ini. Near the end, two lines ship commented out, because hived stops with unable to parse private key when private-key = is present but empty. Open the file with nano pixagram/config.ini, remove the # from both lines and fill them in:
INIwitness = "your-account"
private-key = 5…the wif_priv_key from step 1
Protect the file. It now holds the signing key, and git tracks it as a template. Mark it so that git leaves your copy alone:
Shellchmod 600 pixagram/config.ini
git update-index --skip-worktree pixagram/config.ini
git ls-files -v pixagram/config.ini
grep -E '^(witness|private-key)[[:space:]]*=' pixagram/config.ini
Expected output: S pixagram/config.ini, then your two lines.
-
With the mark,
git statusno longer lists the file,git addrefuses it, andgit reset --hardkeeps your copy. -
Without it,
git reset --hardreplaces your copy with the template. hived reads the file only when it starts, so the node keeps producing until its next restart, perhaps days later, and then stops. -
When the template changes upstream,
git pullstops withAbortingand updates nothing, image versions included. Take the change this way, which keeps your key out of git:Shell
cp pixagram/config.ini ~/config.ini.mine # your copy, outside the repository git update-index --no-skip-worktree pixagram/config.ini git checkout -- pixagram/config.ini # back to the template git pull nano pixagram/config.ini # fill in the two lines again, from ~/config.ini.mine chmod 600 pixagram/config.ini git update-index --skip-worktree pixagram/config.ini rm ~/config.ini.mine # once the node runs on the new file
Create .env for the price feed, with your account's active key. Use an editor rather than a command line, so the key does not land in your shell history: run nano .env, enter these two lines, save, then chmod 600 .env.
INIWITNESS_ACCOUNT=your-account
WITNESS_WIF=5…your active key
Leave two settings out. The node that started the chain needed enable-stale-production = true and required-participation = 0 to produce alone. On a node joining a live network, they remove the checks that stop it from building on a stale chain or producing during a network split. The shipped config.ini contains neither; keep it that way.
Check the clock to the millisecond
hived decides when to produce from the system clock, and each slot lasts 3 seconds. If your clock runs a second or two slow, your block reaches your peers during the next witness's slot. That witness has not seen it, produces a competing block at the same height, loses the fork, and the miss is recorded against it, not you. Your own count stays clean while you cost the network blocks. On this network, one witness running 2.672 seconds slow caused 121 of 123 same-height collisions.
Install a time daemon as in Run a Node, then measure the real offset from the server itself. Comparing date over SSH measures your connection, not the clock.
Shellpython3 - <<'PY'
import socket, struct, time, statistics
def probe(host):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(4)
try:
t1 = time.time(); s.sendto(b'\x1b' + 47 * b'\0', (host, 123))
d, _ = s.recvfrom(1024); t4 = time.time()
finally:
s.close()
u = struct.unpack('!12I', d[:48])
t2 = u[8] + u[9] / 2**32 - 2208988800
t3 = u[10] + u[11] / 2**32 - 2208988800
return ((t2 - t1) + (t3 - t4)) / 2
offsets = []
for host in ('pool.ntp.org', 'time.google.com', 'time.cloudflare.com'):
for _ in range(3):
try: offsets.append(probe(host))
except Exception: pass
print(f'clock offset: {statistics.median(offsets):+.4f}s ({len(offsets)} samples)'
if offsets else 'could not reach any NTP server')
PY
Expected output: a line such as clock offset: +0.0031s (9 samples). Under about 50 ms is fine. Past about half a second, you cost your neighbours in the schedule their blocks.
3. Start the node and let it sync
Start the node alone; the feed comes in step 5.
Shelldocker compose up -d pixagram
docker compose logs -f pixagram
Wait for entering live mode, followed by one Got … transactions on block … line every 3 seconds (what the log shows). Register only once the node is in live mode.
4. Register on chain
Registration is one witness_update operation, signed with your active key. This script sends it with dpixa through api.pixagram.com. Run it on any machine with Node.js 18 or newer:
Shellmkdir witness-tools && cd witness-tools
npm init -y
npm install @pixagram/dpixa@1.5.4
Save as register-witness.js, with your values in the first three constants:
JavaScript// register-witness.js: run once, and again whenever the URL, key or properties change
const { Client, PrivateKey } = require("@pixagram/dpixa/dist/dpixa.js");
const ACCOUNT = "your-account";
const SIGNING_PUB = "PIX…"; // pub_key from step 1
const URL = "https://pixagram.com/…"; // your witness page
const client = new Client(["https://api.pixagram.com"]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function main() {
const key = PrivateKey.fromString(process.env.ACTIVE_WIF || ""); // from the environment, never from a file
const { id } = await client.broadcast.sendOperations([
["witness_update", {
owner: ACCOUNT,
url: URL,
block_signing_key: SIGNING_PUB,
props: { account_creation_fee: "20.000 PIXA", maximum_block_size: 2097152, pxs_interest_rate: 0 },
fee: "0.000 PIXA",
}],
], key);
console.log("accepted, transaction", id);
await sleep(9000);
const w = await client.call("condenser_api", "get_witness_by_account", [ACCOUNT]);
console.log("on chain:", w.owner, "signs with", w.signing_key, "url", w.url);
}
main().catch((e) => { console.error(e.message); process.exit(1); });
Run it with the active key read from the keyboard, so the key stays out of your shell history:
Shellread -rsp 'Active key: ' ACTIVE_WIF && export ACTIVE_WIF && echo
node register-witness.js
unset ACTIVE_WIF
Expected output:
accepted, transaction <40 hexadecimal characters>
on chain: your-account signs with PIX… url https://pixagram.com/…
| Field | What to put |
|---|---|
url |
Your witness page. It cannot be empty. |
block_signing_key |
The pub_key from step 1 |
account_creation_fee |
Your vote for the fee to open an account. The median was 20.000 PIXA on 2026-10-05; the floor is 0.001 PIXA. |
maximum_block_size |
Your vote for the block size, from 131072 to 2097152 bytes. Every witness voted 2097152 on 2026-10-05. |
pxs_interest_rate |
0. The chain rejects any other value. |
fee |
0.000 PIXA |
Your votes for the fee and the block size count through the median of the scheduled witnesses (account fee, block size).
5. Publish the price feed
PXS promises no price. Its feed is each witness's statement of how many PIXA one Big Mac costs. The chain takes the middle feed of the scheduled witnesses once an hour, conversions use the median of the last 84 samples, and a feed older than 7 days stops counting (the rules). How the feed is computed, and why, is on Oracle and Price Feed.
The bigmac-feed service in the repository runs bigmac-feed v1.3.0. Every hour it:
- prices one Big Mac in the local currency of the country your server runs in, from The Economist's index, or uses the country or the price you set;
- values PIXA at the fair value of the PIXA/USDT order book on Coinstore, not at its last trade;
- converts that value into the Big Mac's currency at the day's rate, when the two differ;
- publishes
1.000 PXS = <Big Mac ÷ PIXA> PIXAthrough your own node, over Docker's internal network, which is whyconfig.iniloadsdatabase_api,condenser_apiandnetwork_broadcast_api.
It needs no placeholder price. If the market gives no fair value, it publishes nothing that hour, and the chain keeps your previous feed.
Check the version
In the witness directory:
Shellgrep 'bigmac-feed:' docker-compose.yml
ls docker-compose.override.yml
Expected output: the line image: pixarex/bigmac-feed:v1.3.0, indented, then ls: cannot access 'docker-compose.override.yml': No such file or directory. Until 2026-10-30 the repository pinned v1.0.2 with a fixed 0.12 USD price for PIXA.
- An older version means an older copy of the repository: update it as in step 2, with
git pull. - An override file may hold a feed pinned by an earlier version of this page,
pixadock/bigmac-feed:v1.0.3orbigmac-feed-agnostic:local, and it overrides the repository. Delete itsbigmac-feedsection, or the whole file if that is all it holds.
Try it without a key
Shelldocker run --rm pixarex/bigmac-feed:v1.3.0 --dry-run --once --witness=your-account
A dry run computes everything, broadcasts nothing and needs no key. Expected output from a server in the United States on 2026-11-03. Each line starts with the date and time, and the numbers follow the order book and the day's exchange rate:
Big Mac country: USA (detected from public IP)
Big Mac Feed — witnesses=your-account rpc=https://api.pixagram.com markets=[coinstore PIXAUSDT] impact-size=1000 PIXA max-spread=25% interval=1h0m0s dry-run=true
Big Mac in United States = 6.22 USD (as of 2026-07-01) | PIXA = 0.138513 USDT [coinstore PIXAUSDT: bid 0.1349 / ask 0.142, 1000-PIXA impact 0.13402 / 0.143006] → 1 PXS = 44.906 PIXA
Dry run: would publish 1.000 PXS = 44.906 PIXA for your-account
Read the third line before going further: it names the Big Mac your feed will use and the market it read.
Choose your Big Mac
The detected country is your server's, which may not be yours: a server rented abroad reports the Big Mac of its data centre's country. Two settings change that:
| Setting | Effect | Example |
|---|---|---|
--country=CH |
The local price of that country in the latest survey of the index. Two- or three-letter ISO codes; the euro area is EUZ, and any member country maps to it. A country missing from the survey makes the feed publish nothing. |
Switzerland: 7.30 CHF |
--bigmac-price="7.30 CHF" |
A price you observe yourself, with its currency. It overrides --country. Nobody can check it on chain, so say on your witness page where it comes from. |
7.30 CHF |
Try a setting before you keep it:
Shelldocker run --rm pixarex/bigmac-feed:v1.3.0 --dry-run --once --witness=your-account --bigmac-price="7.30 CHF"
Expected output on 2026-11-03, after the first two lines:
Big Mac = 7.3 CHF (--bigmac-price) | PIXA = 0.138513 USDT [coinstore PIXAUSDT: bid 0.1349 / ask 0.142, 1000-PIXA impact 0.13402 / 0.143006] = 0.113529 CHF | FX USD→CHF 0.81963 (ECB 2026-11-02) → 1 PXS = 64.301 PIXA
Dry run: would publish 1.000 PXS = 64.301 PIXA for your-account
To keep it, create docker-compose.override.yml in the witness directory; git pull leaves that file alone. Compose replaces the whole command list, so repeat the repository's three lines before yours:
YAMLservices:
bigmac-feed:
command:
- --witness=${WITNESS_ACCOUNT}
- --rpc=http://pixagram:7777
- --interval=1h
- --country=CH
Start it
Shelldocker compose up -d bigmac-feed
docker compose logs -f bigmac-feed
After any change to its settings, run docker compose up -d bigmac-feed again; docker compose restart keeps the old command. Expected output, once an hour, with your Big Mac and that hour's numbers:
Big Mac in United States = 6.22 USD (as of 2026-07-01) | PIXA = 0.138513 USDT [coinstore PIXAUSDT: …] → 1 PXS = 44.906 PIXA
Feed published by your-account: 1.000 PXS = 44.906 PIXA
Until your registration in step 4 is in a block, every publish fails with an error from the node. When the feed publishes nothing, it says why:
| Log line | Meaning | What to do |
|---|---|---|
WARNING: no fair value: coinstore PIXAUSDT: spread … is wider than --max-spread 25.0%, no fair value |
The book is too thin to read | Nothing. The chain keeps your previous feed for up to 7 days. |
ERROR: no PIXA fair value from any of the 1 market(s); nothing published, the chain keeps the previous feed |
No market gave a fair value this hour | The same. If it lasts for days, raise it with the other witnesses before you set --token-price. |
ERROR: fx USD→…: … |
No exchange rate for your Big Mac's currency, and no saved rate younger than 96 hours | Check the server's outgoing connections. |
ERROR: … is not in the latest Big Mac survey |
The country set with --country is not in the index |
Choose another country, or declare a price. |
Keep it healthy
-
When your witness is scheduled, which needs votes from 1% of all Pixa Power (below),
condenser_api.get_witness_schedulelists it incurrent_shuffled_witnesses, and the node logsGenerated block #<n> with timestamp <time> at time <time>at each of its turns. If it logsWon't produce block because I don't have the private key for PIX…instead, the key inconfig.inidoes not match the one you registered: fix one of them and rundocker compose restart pixagram. -
Look daily at the witness status page: your missed blocks, your feed's age, your version against the majority.
-
Let a machine watch. The status project's check exits 0 when everything is healthy, 1 on a warning, 2 on a critical finding and 3 if it cannot fetch. It judges the whole network, so a warning may concern another witness. Run it on a machine that holds no keys, pinned to a version you have read, with Node.js 20 or newer. A cron line can alert you:
Shell
*/10 * * * * npx github:pixagram-blockchain/witness-status#a376149 --check >/dev/null 2>&1 || <your alert command> -
Your own numbers, from any machine:
Shell
curl -s -X POST https://api.pixagram.com -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","method":"condenser_api.get_witness_by_account","params":["your-account"],"id":1}'total_missedshould stay flat;running_versionshould match the other witnesses. -
Short maintenance. Restart right after your node has produced a block, with
docker compose restart pixagram, so it is back within seconds. -
Longer maintenance: a standby node. Run a second synced node with its own signing key. Move to it by running
register-witness.jswith the standby's public key, do the work, and move back the same way. Never let two nodes sign with the same key at the same time: that can harm the chain, not just you. -
Upgrades follow Run a Node. Running the new version is how your node signals support for its hardfork.
-
Back up keys, not the chain.
config.ini,.envand your key store cannot be downloaded again; the chain can.
Before you announce
- <input class="task-list-item-checkbox" type="checkbox">
get_witness_by_accountshows your signing key and URL, and once you are scheduled, your missed count stays flat. - <input class="task-list-item-checkbox" type="checkbox"> Your feed is published, less than an hour old, and its log line names the Big Mac and the market you expect.
- <input class="task-list-item-checkbox" type="checkbox">
config.iniand.envarechmod 600, backed up, and in no git commit. - <input class="task-list-item-checkbox" type="checkbox">
timedatectlreports a synchronized clock, and the offset is under 50 ms. - <input class="task-list-item-checkbox" type="checkbox"> Your witness URL says who runs the node, where, and how to reach you.
- <input class="task-list-item-checkbox" type="checkbox"> Something other than you checks the node every ten minutes.
Then ask for votes: tell stakeholders why you run a witness and what you run besides it. They vote as shown in Witnesses and DPoS. Since hardfork 30 your witness is scheduled only once accounts holding at least 1% of all Pixa Power have voted for it, so registering alone produces no blocks (Witnesses and DPoS).
Sources
- Repository
pixagram-blockchain/witnessat commit704a68f, 2026-10-30:README.md(keys, clock incident, verification, the 1.30.1 upgrade),docker-compose.yml,pixagram/config.ini. Since commite3b8841, which the first version of this page described, only the image versions, the feed's settings and the README changed. - Feed:
bigmac-feedv1.3.0 at commit11f8ad1, 2026-10-30: README andmain.gofor the settings and log lines,chain.go:280-290for the line logged after each publish. The dry runs above were made on 2026-11-03 between 08:49 and 08:58 UTC with the program built from that commit, which its README calls v1.3.0, and reachedapi.pixagram.com, Coinstore and the exchange-rate services read-only. Docker Hub refused anonymous pulls from the test machine that day, so the image itself was not run. The supersededbigmac-feed-agnosticis at commit96b9d4a. - Registration: the
witness_updateabove, signed by dpixa, matched the public node's serialization (condenser_api.get_transaction_hex) on 2026-10-05. Validation of the properties:hive_operations.cpp:312-336at commit48f75a2. - Log lines:
witness_plugin.cpp:342, 427 and 625. - Live values, 2026-10-05:
condenser_api.get_witnesses_by_voteandcondenser_api.get_chain_properties; the number of scheduled witnesses,condenser_api.get_witness_scheduleat block 1,722,911 on 2026-11-03. - Monitoring:
witness-statusat commita376149. - The same procedure on the developer site: pixa.org, Run a Pixagram node, node operator guide version 1.0, 2026-09-05.