Cryptographically signed attestations of Bisq pricenode data, published as Nostr events.
Bisq pricenodes serve market reference prices over Tor but do not sign their responses. This script periodically fetches prices from a pricenode and publishes them as signed Nostr events, creating a verifiable trail that trading parties can reference to agree on a price.
Two parties trading P2P can use these attestations to agree on a reference price:
- Alice publishes an offer: "selling BTC at market price + 7%"
- Bob wants to accept. He queries the bot for the latest attestation, gets event
abc123with USD price 62,610 - Bob sends a TakeOffer to Alice including event
abc123as the price reference. The agreed price is 62,610 * 1.07 = 66,992 USD - Alice fetches event
abc123from any relay, verifies the signature, timestamps, and confirms the price
Given an event, any party can independently verify it:
- Recompute the event ID:
SHA-256(json_encode([0, pubkey, created_at, kind, tags, content])) - Verify the Schnorr signature against the public key and event ID
- Parse the
contentfield to extract the original pricenode response
- Fetches
/getAllMarketPricesfrom a randomly selected Bisq pricenode via Tor - Builds a Nostr event (kind
1078) with the raw JSON response as content and individual price tags for filtering - Signs the event with a BIP-340 Schnorr signature using the
secp256k1_nostrPHP extension - Publishes the signed event to several Nostr relays
The raw pricenode response is embedded byte-for-byte in the event's content field.
- PHP >= 8.1 with extensions:
curl,sockets,mbstring,secp256k1_nostr - Tor running with a SOCKS5 proxy on
127.0.0.1:9050 - Composer
The secp256k1_nostr extension can be installed with pie: pie install uma/secp256k1-nostr
composer install
A Nostr private key is a random 32-byte value, hex-encoded (64 characters). Generate one with:
php -r 'echo bin2hex(random_bytes(32)) . "\n";'
Or with OpenSSL:
openssl rand -hex 32
Or rana, if you want a vanity npub:
cargo run --release -- --vanity-n-prefix=n0de
To derive the corresponding public key (which identifies your bot on Nostr):
php -r 'echo secp256k1_nostr_derive_pubkey($argv[1]) . "\n";' <private_key>
Keep the private key secret. Share the public key with anyone who needs to verify your attestations.
# Dry run (build and sign the event, print it to stdout, don't publish)
NOSTR_PRIVATE_KEY=<hex> php attest.php --dry-run
# Publish to default relays
NOSTR_PRIVATE_KEY=<hex> php attest.php
# Publish and print the signed event to stdout
NOSTR_PRIVATE_KEY=<hex> php attest.php --verbose
# Publish to additional relays (merged with the built-in list)
NOSTR_PRIVATE_KEY=<hex> NOSTR_RELAYS=wss://custom.relay.example php attest.php
By default, only relay status messages are printed (to stderr). Use --verbose to also print the signed event JSON to stdout. --dry-run implies --verbose.
The recommended way to run this periodically is a systemd timer with a dedicated service user. This avoids exposing the private key in crontabs, environment listings or process tables.
Create a credentials file readable only by the service user:
sudo mkdir -p /etc/pricenode-attestations
echo 'NOSTR_PRIVATE_KEY=<hex>' | sudo tee /etc/pricenode-attestations/env > /dev/null
sudo chmod 600 /etc/pricenode-attestations/env
sudo chown pricenode-attestations: /etc/pricenode-attestations/env
Create the service unit (/etc/systemd/system/pricenode-attestations.service):
[Unit]
Description=Pricenode attestation publisher
Wants=network-online.target tor.service
After=network-online.target tor.service
[Service]
Type=oneshot
User=pricenode-attestations
EnvironmentFile=/etc/pricenode-attestations/env
ExecStart=/usr/bin/php /opt/pricenode-attestations/attest.php
# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadOnlyPaths=/opt/pricenode-attestationsCreate the timer (/etc/systemd/system/pricenode-attestations.timer):
[Unit]
Description=Run pricenode attestation every 5 minutes
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
RandomizedDelaySec=30s
[Install]
WantedBy=timers.targetEnable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now pricenode-attestations.timer
If you prefer simplicity over hardening:
*/5 * * * * NOSTR_PRIVATE_KEY=<hex> php /path/to/attest.php >> /var/log/pricenode-attestations.log 2>&1
Note that the private key will be visible in the crontab file and in /proc/*/environ of the running process.
Each event uses kind 1078 (regular, non-replaceable) to preserve full history on relays.
| Tag | Purpose |
|---|---|
["t", "pricenode"], ["t", "bisq"], ["t", "bitcoin"] |
Hashtags for discoverability |
["L", "io.bisq.pricenode"] |
NIP-32 label namespace |
["l", "attestation", "io.bisq.pricenode"] |
NIP-32 label: marks the event as a pricenode attestation |
["pricenode", "<onion address>"] |
Which pricenode was queried |
["price", "<currency>", "<price>", "<timestamp>"] |
One tag per Bisq-Aggregate price, filterable by relay queries |
The content field contains the raw JSON response from the pricenode, byte-for-byte identical to what was received.
From a Nostr relay, using the bot's public key:
// Latest attestation
["REQ", "sub1", {"authors": ["<pubkey>"], "kinds": [1078], "limit": 1}]
// Latest USD price
["REQ", "sub2", {"authors": ["<pubkey>"], "kinds": [1078], "#price": ["USD"], "limit": 1}]
// Attestations within a time window
["REQ", "sub3", {"authors": ["<pubkey>"], "kinds": [1078], "since": 1700000000, "until": 1700000300}]
// Specific event by ID
["REQ", "sub4", {"ids": ["<event_id>"]}]| NPub | Operator |
|---|---|
npub1n0deulgn46wl2jq2kckvexlmc9qd3ghpa0a6nz3tsgaemktyztes9wfzmh |
Barcelona Bitcoin Only |