コンテンツへスキップ

Running an OpenFiat node

Node operators maintain the decentralized marketplace that sits above the blockchain. Solana validators secure the chain and execute the OpenFiat programs; nodes carry the listings, reputation, messaging and discovery.

Commands below use the reference deployment: the openfiat-node service, /etc/openfiat for configuration and /var/lib/openfiat for data.

Hardware

Minimum

4 CPU cores · 16 GB RAM · 250 GB NVMe SSD · Stable broadband connection

Recommended for production

8–16 CPU cores · 32 GB RAM or more · 1 TB NVMe SSD · High-bandwidth, low-latency network · UPS power protection · Redundant internet connectivity

The node is a single Rust binary. libp2p (QUIC, Noise, Yamux) and RocksDB are compiled in — there is nothing to install separately.

Where to run it

AllenHark leads initial development of the protocol and sells the two things a node needs: a server and Solana RPC access. Node operators get 50% off both, as part of the OpenFiat program.

Server hosting

NVMe storage on 10 Gbps uplinks, in Frankfurt, Amsterdam and Chicago. Size it against the hardware above — storage is what a full node grows into, so say what you are running and it can be specified for you.

See pricing

Solana RPC and gRPC

Staked connections, so transactions your node submits are not queued behind everyone else's. Access is granted by IP allowlist rather than an API key. Yellowstone gRPC is available for streaming account and slot updates.

See pricing

Claiming the discount

There is no code to enter. Ask on Discord or in the chat on allenhark.com, say you are running an OpenFiat node, and the discount is applied to your order.

Serve the network, not just yourself

A node that only reads is a guest. A node the public can reach is infrastructure — wallets, explorers and web apps have to talk to *somebody*, and today too few of those somebodies exist. Every operator who puts their node behind TLS and sets --public-rpc-url widens that set, which is the difference between a network with several independent front doors and one with a single point of failure that everyone quietly depends on. It costs a certificate and a reverse proxy. It is also paid: a reachable node is one peers can challenge and reward, and OpenFiat's own applications will use yours alongside every other.

Two things worth knowing before you do it. Serving the public means real traffic, so size the machine for it and watch the metrics endpoint. And a public node is a public commitment — people will build against the URL you publish, so take it down deliberately rather than quietly.

Installing it

Build from source — for production, today

No version has been tagged yet, so this is currently the only way to get a node binary. Needs the Rust toolchain and a C toolchain for RocksDB. Build it, drop it in /usr/local/bin, and run it under systemd — that is the deployment this page documents, and the one to use for a node others rely on.

shell
# Rust toolchain, then build the node binary
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
apt install -y build-essential pkg-config libssl-dev clang cmake

git clone https://github.com/OpenFiat-org/openfiat-core.git
cd openfiat-core
cargo build --release --bin openfiat-node

install -m 0755 target/release/openfiat-node /usr/local/bin/

Prebuilt binary — once a version is tagged

Pushing a v* tag runs the release workflow, which builds openfiat-node on native runners and publishes linux-x86_64 and windows-x86_64 archives to GitHub Releases. Nothing is tagged yet, so that page is currently empty. Note these are ordinary release builds, not static or signed — verify what you download against the workflow that produced it.

GitHub Releases
shell
useradd --system --create-home --home-dir /var/lib/openfiat --shell /usr/sbin/nologin openfiat
install -m 755 openfiat-node /usr/local/bin/openfiat-node
mkdir -p /etc/openfiat
chown -R openfiat:openfiat /var/lib/openfiat

systemctl daemon-reload
systemctl enable --now openfiat-node
journalctl -u openfiat-node -f

Docker — for testing only

Use the image to try a node locally or to bring up a throwaway multi-node cluster — not to run a node the network depends on. It exists for reproducible local testing; production runs the binary under systemd.

compose.yaml
# /opt/openfiat/compose.yaml
# TESTING ONLY. Use this to try a node locally or to bring up a throwaway
# cluster — run a production node from the binary under systemd instead
# (see the unit file further down this page).
services:
  node:
    image: ghcr.io/openfiat-org/openfiat-node:latest
    restart: unless-stopped
    # QUIC needs UDP; the same TCP port serves JSON-RPC, WebSocket, REST,
    # health, and metrics together — there is no separate metrics port.
    ports:
      - "4001:4001/udp"
      - "7080:7080/tcp"
    volumes:
      - /var/lib/openfiat:/data
      - /etc/openfiat/wallet.json:/data/wallet.json:ro
    command:
      - --ledger=/data
      - --identity=/data/wallet.json
      - --rpc-bind-address=0.0.0.0:7080
      - --gossip-bind-address=/ip4/0.0.0.0/udp/4001/quic-v1
      # Static multiaddr — DNS bootstrap does not resolve (see this
      # file's own top comment). Repeat the flag for several.
      - --entrypoint=/ip4/84.32.223.111/udp/4001/quic-v1/p2p/12D3KooWK9hQ7TwbfvFiaAxUbRFCkdhS7iEpAJDnewNL1anyREQ1
      # Omit to stay GossipOnly, the safe default. Any value opts into
      # real Solana devnet connectivity (OFS-4300 §4).
      - --solana-rpc-url=https://api.devnet.solana.com
    stop_grace_period: 60s
    ulimits:
      nofile: 65536
shell
install -d -m 0750 /var/lib/openfiat
docker compose -f /opt/openfiat/compose.yaml up -d
docker compose -f /opt/openfiat/compose.yaml logs -f node

Ports

  • 4001/UDPPeer-to-peer traffic. QUIC is the primary transport, so this UDP port must be reachable — the one people most often forget to open.Must be reachable
  • 7080/TCPJSON-RPC, WebSocket, REST, health, and metrics — one real port serves all of it, there is no separate port per surface.Must be reachable
  • 7080/TCPSame port as the API above (GET /metrics). Firewall it to loopback/a private network if you don't want it public.Keep private

Bringing a node online

Work through these in order. Each step below carries the command it needs.

  1. 01

    Prepare the machine

    A current Debian or Ubuntu server, a firewall, and an accurate clock. The clock matters more than it looks: records carry timestamps and expiry times, and a drifting clock makes a node reject valid data.

    shell
    # Debian 12 / Ubuntu 24.04, as root
    apt update && apt install -y ca-certificates curl gnupg ufw chrony
    
    # Clock drift breaks signature and expiry checks — keep NTP running.
    systemctl enable --now chrony
  2. 02

    Install the node

    Build from source and run the result under systemd — no version has been tagged yet, so that is the only way to get a binary today, and the rest of this runbook assumes it. Reach for Docker only when testing locally or standing up a throwaway cluster.

  3. 03

    Generate the node's wallet

    There is no separate "node identity" format — a node's identity is a real Solana CLI wallet.json, the same file solana-keygen produces. Its seed is reused for both the node's gossip/peer identity and its Solana signing key. Keep the file — if you lose it the node rejoins as a stranger and starts building reputation again from zero.

    shell
    # The node's identity IS a Solana CLI-format wallet.json — the same
    # file "solana-keygen new" produces (its own seed is reused for the
    # node's gossip/P2P keypair too, see crates/cli's load_or_generate_wallet).
    # There is no separate "node identity" format or command.
    solana-keygen new --outfile /etc/openfiat/wallet.json
    chmod 600 /etc/openfiat/wallet.json
    solana-keygen pubkey /etc/openfiat/wallet.json
  4. 04

    Set the environment

    openfiat-node has no config file of its own — every setting is an environment variable, read once at startup: where data lives, which address to listen on, which peers to dial on start, and (optionally) which Solana RPC endpoint to use. Bootstrap peers must be a static multiaddr/IP, not a hostname — DNS bootstrap doesn't resolve.

    shell
    # There is no config file and no environment variables. Every
    # setting is a flag on the systemd unit's ExecStart below, so
    # "systemctl cat openfiat-node" shows exactly what a running node was
    # given, and "openfiat-node --help" is the whole surface.
    #
    # Try it in the foreground first — the node prints the addresses it is
    # reachable at and whether it reached Solana:
    
    openfiat-node \
      --ledger /var/lib/openfiat \
      --identity /etc/openfiat/wallet.json \
      --rpc-bind-address 0.0.0.0:7080 \
      --gossip-bind-address /ip4/0.0.0.0/udp/4001/quic-v1 \
      --entrypoint /ip4/84.32.223.111/udp/4001/quic-v1/p2p/12D3KooWK9hQ7TwbfvFiaAxUbRFCkdhS7iEpAJDnewNL1anyREQ1 \
      --solana-rpc-url https://api.devnet.solana.com
    
    # Omit --solana-rpc-url to stay GossipOnly, the safe default: the node
    # still serves the marketplace, but its on-chain answers come
    # second-hand from peers. Never put a real endpoint or API key anywhere
    # version controlled — it belongs in the unit file on the server.
    #
    # Add --ipfs-api-url http://127.0.0.1:5001 to pin protocol content and
    # earn the full reward share, and --retention archival to keep the whole
    # history instead of a rolling 30 days.
  5. 05

    Open the right ports

    Peers reach the node over UDP because QUIC is the primary transport — that is the port people most often forget. One TCP port serves JSON-RPC, WebSocket, REST, health, and metrics together; keep it private if you don't want to serve clients publicly.

    shell
    ufw default deny incoming
    ufw allow 22/tcp                 # keep your own access
    ufw allow 4001/udp               # libp2p, QUIC — the port people most often forget
    ufw allow 443/tcp                # once nginx terminates TLS in front (see below)
    ufw allow 7080/tcp               # only if the node is exposed directly, without a proxy
    ufw enable && ufw status verbose
  6. 06

    Serve the network: make your node reachable

    This is the step that turns a node from something that watches the network into something that carries it. Put nginx in front and get a certificate, then tell the node its public URL with --public-rpc-url and it advertises itself so wallets, explorers and OpenFiat's own web app can use it. The order matters: nginx over plain HTTP first, then certbot adds TLS — a config that already names a certificate cannot start, so certbot fails before it can issue the one that would have fixed it. A certificate is not optional decoration: a page served over HTTPS cannot open a plain-HTTP connection, so a node without one is invisible to every browser however healthy it is.

    /etc/nginx/sites-available/openfiat-node
    # /etc/nginx/sites-available/openfiat-node
    #
    # HTTP only at this stage, with NO TLS directives. certbot --nginx works
    # by running "nginx -t" and editing this file, so a config that already
    # points at a certificate cannot load, nginx will not start, and certbot
    # fails before it can issue the certificate that would have fixed it.
    server {
        listen 80;
        listen [::]:80;
        server_name openfiat.example.com;
    
        location / {
            proxy_pass http://127.0.0.1:7080;
            proxy_http_version 1.1;
    
            # The node serves its WebSocket event stream on the same port;
            # without these the stream downgrades to a plain request and
            # subscriptions silently never deliver.
            proxy_set_header Upgrade    $http_upgrade;
            proxy_set_header Connection "upgrade";
    
            proxy_set_header Host              $host;
            proxy_set_header X-Real-IP         $remote_addr;
            proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
    
            # Do NOT add CORS headers here — the node already sets them, and
            # two Access-Control-Allow-Origin headers on one response make
            # browsers reject it while curl sees a clean 200.
            proxy_read_timeout 300s;
        }
    }
    shell
    # The A record must already resolve for the world — certbot proves
    # control of the name over port 80. Check a public resolver, not your
    # own machine, whose cache can be stale:
    dig +short @8.8.8.8 openfiat.example.com
    
    ln -s /etc/nginx/sites-available/openfiat-node /etc/nginx/sites-enabled/
    nginx -t && systemctl reload nginx
    curl -s http://openfiat.example.com/health    # ok, before any certificate
    
    apt install -y certbot python3-certbot-nginx
    certbot --nginx -d openfiat.example.com
    
    # certbot edits the server block in place: it adds listen 443 ssl, the
    # certificate paths, and a redirect from :80. The location block and its
    # proxy headers carry over. Renewal installs itself as a systemd timer.
    # Do not hand-write a 443 block afterwards — certbot manages that one.
    
    # Then tell the network this node can be reached directly:
    #   openfiat-node --public-rpc-url https://openfiat.example.com
  7. 07

    Run it as a service

    Under systemd the node restarts after a crash or reboot, and gets a long enough shutdown grace period that the database flushes cleanly instead of being killed mid-write.

    openfiat-node.service
    # /etc/systemd/system/openfiat-node.service
    [Unit]
    Description=OpenFiat node
    Documentation=https://docs.openfiat.network
    After=network-online.target
    Wants=network-online.target
    
    [Service]
    Type=simple
    User=openfiat
    Group=openfiat
    ExecStart=/usr/local/bin/openfiat-node \
        --ledger /var/lib/openfiat \
        --identity /etc/openfiat/wallet.json \
        --rpc-bind-address 0.0.0.0:7080 \
        --gossip-bind-address /ip4/0.0.0.0/udp/4001/quic-v1 \
        --entrypoint /ip4/84.32.223.111/udp/4001/quic-v1/p2p/12D3KooWK9hQ7TwbfvFiaAxUbRFCkdhS7iEpAJDnewNL1anyREQ1 \
        --solana-rpc-url https://api.devnet.solana.com
    WorkingDirectory=/var/lib/openfiat
    Restart=on-failure
    RestartSec=5s
    
    # openfiat-node handles SIGTERM itself (graceful shutdown) rather than
    # needing systemd to force-kill it; this gives that shutdown path a
    # reasonable window before systemd escalates to SIGKILL.
    TimeoutStopSec=30s
    
    NoNewPrivileges=true
    ProtectSystem=strict
    ProtectHome=true
    ReadWritePaths=/var/lib/openfiat
    PrivateTmp=true
    
    # AF_NETLINK is required, not optional: binding a wildcard address makes
    # libp2p enumerate this host's interfaces, and that goes over a netlink
    # socket. Without it the QUIC listener fails and the gossip actor panics
    # while the HTTP thread survives — so systemd reports the unit active
    # and the node looks healthy while serving nothing.
    RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK
    
    [Install]
    WantedBy=multi-user.target
    shell
    useradd --system --create-home --home-dir /var/lib/openfiat --shell /usr/sbin/nologin openfiat
    install -m 755 openfiat-node /usr/local/bin/openfiat-node
    mkdir -p /etc/openfiat
    chown -R openfiat:openfiat /var/lib/openfiat
    
    systemctl daemon-reload
    systemctl enable --now openfiat-node
    journalctl -u openfiat-node -f
  8. 08

    Let it catch up

    Rather than replaying all history, a new node can discover and import a peer-announced snapshot of current marketplace state (OFS-1300) — real JSON-RPC methods, not a separate tool: getLatestSnapshot, getCheckpointHeight. Signature, protocol version, and state root all have to match before it's trusted.

    shell
    # Snapshot sync (OFS-1300) is real JSON-RPC, not a separate CLI —
    # a new node discovers and imports a peer-announced snapshot instead of
    # replaying all history:
    curl -s -X POST http://localhost:7080/rpc -H 'content-type: application/json' \
      -d '{"jsonrpc":"2.0","id":1,"method":"getLatestSnapshot","params":{}}'
    curl -s -X POST http://localhost:7080/rpc -H 'content-type: application/json' \
      -d '{"jsonrpc":"2.0","id":1,"method":"getCheckpointHeight","params":{}}'
  9. 09

    Check that it is healthy

    GET /health confirms the process is up; getChainStatus over JSON-RPC tells you whether it's GossipOnly or RpcConnected, and its current blockhash if the latter.

    shell
    # Is it up, and which mode is it in?
    curl -s http://localhost:7080/health
    # ok
    
    curl -s -X POST http://localhost:7080/rpc -H 'content-type: application/json' \
      -d '{"jsonrpc":"2.0","id":1,"method":"getChainStatus","params":{}}'
    # {"jsonrpc":"2.0","id":1,"result":{"mode":"GossipOnly","blockhash":null,"slot":null,"age_ms":null}}
    # ("RpcConnected" with a real blockhash once --solana-rpc-url is set)
    
    # The addresses your node is reachable at are in its own log — give one
    # of these to another operator as their --entrypoint:
    journalctl -u openfiat-node | grep "reachable at a new address"
  10. 10

    It's already part of the network

    There's no separate "announce" step — once a node has bootstrap peers, it gossips and is gossiped about automatically; nobody approves it. Staking, publishing service-registry metadata, joining disputes, and casting governance votes are all separate, wallet-driven actions a client performs against the running node — see the relevant participate guide for each.

    A node stakes OPEN to become an active participant. Stake does not buy reputation, though: an operator with poor performance cannot compensate by staking more. Effective priority combines reputation, stake and measured network performance.

  11. 11

    Watch it

    The signals that actually predict trouble are connected peers, chain mode (GossipOnly vs RpcConnected), and blockhash age. Alert on those and you will know before your users do.

    prometheus.yml
    # /etc/prometheus/prometheus.yml — same port as everything else,
    # no separate metrics listener to configure on the node's own side.
    scrape_configs:
      - job_name: openfiat-node
        static_configs:
          - targets: ["127.0.0.1:7080"]
  12. 12

    Keep it current

    Stop, replace the binary, start. Nodes upgrade one at a time, so the network never needs a coordinated outage, and missed gossip events replay on startup.

    shell
    systemctl stop openfiat-node        # SIGTERM, lets RocksDB flush
    # docker: docker compose pull node && docker compose up -d node
    install -m 0755 ./openfiat-node /usr/local/bin/
    systemctl start openfiat-node
    
    # Missed gossip events replay automatically on start.
    journalctl -u openfiat-node -f
  13. 13

    Back up what cannot be regenerated

    Marketplace state can always be re-synced from a snapshot. The wallet cannot be regenerated. Balances and escrow live on Solana, not on your disk.

    shell
    # Back up what cannot be re-derived. Marketplace state can always be
    # re-synced from a snapshot; the wallet cannot be regenerated.
    tar czf openfiat-wallet-$(date +%F).tar.gz /etc/openfiat/wallet.json
    
    # Authoritative financial state lives on Solana, not on the node's own
    # RocksDB volume.

When something is wrong

No peers connect

UDP 4001 is blocked, or --entrypoint points at an address peers can't actually reach — it must be a static multiaddr/IP, not a hostname (DNS bootstrap doesn't resolve). Your own node logs the addresses it is reachable at once it is listening.

shell
ss -lunp | grep 4001 && journalctl -u openfiat-node -n 100

Synchronization never completes

Snapshot import (OFS-1300) hasn't finished, or no peer has announced a recent-enough snapshot yet — check getLatestSnapshot/getCheckpointHeight.

shell
journalctl -u openfiat-node -n 200 --no-pager | grep -i snapshot

Disk usage keeps growing

RocksDB has not compacted.

shell
du -sh /var/lib/openfiat/*

Signatures or expiries rejected

Clock drift. Records carry timestamps and expiry, so the host clock has to be correct.

shell
chronyc tracking

What a node should expose

That's everything the node exports today — peer count, chain mode, and sync progress aren't Prometheus metrics yet, only values you can poll over JSON-RPC (getChainStatus, getLatestSnapshot, getCheckpointHeight).

Infrastructure

CPU · Memory · Disk · Network throughput

Protocol (GET /metrics)

rpc_requests_total · rpc_errors_total

Hosting the user interface

OpenFiat separates the protocol from its interfaces. Anyone may host a web interface, a mobile gateway, an enterprise portal or a regional marketplace, and every one of them reaches the same marketplace.

Regardless of which interface is used, every participant interacts with the same decentralized marketplace.

Official web interface · Community interfaces · Merchant interfaces · Regional marketplaces · Enterprise portals

What an interface connects to

Interfaces do not talk to the chain directly. They connect to one or more nearby OpenFiat nodes, and fail over to another node if one becomes unavailable.

Node API surface

  • MarketplaceSearch advertisements · Create advertisements · Update advertisements · Remove advertisements
  • TradingReserve advertisements · Synchronize trade sessions · Submit payment confirmations
  • InfrastructurePeer discovery · Gateway discovery · Snapshot information · Risk intelligence
  • GovernanceProposal discovery · Vote submission · Treasury information

Every compliant node exposes the same API surface, so an interface written against one node works against all of them.

Nodes never custody user funds. All financial operations remain under the control of Solana smart contracts.