Configuration
One YAML file, written by matrixd -init and read at start. Every field below is real.
The generated secure baseline
matrixd -init writes a safe starting point: ACLs stay on, browser access is limited to local development origins, public reads and signed writes stay off, no maintainer account is invented, provider emissions are zero, and the bridge is disabled. It is a baseline to review, not a claim that a production launch profile has been installed.
network:
listen_addr: /ip4/127.0.0.1/tcp/9000 # libp2p multiaddr; 0.0.0.0 to accept peers
bootstrap_peers: [] # multiaddrs to dial on start
storage:
engine: pebble
path: ./data
security:
enable_acls: true # when true, every RPC needs a key
allow_unsigned_agents: false
api_keys:
- key: <64 hex chars> # generated by `matrixd -init`
role: admin # admin | operator | viewer
name: local-admin
account: "" # on-chain account this key spends from
admin:
addr: 0.0.0.0:9090 # gRPC: deploy, health, logs
market:
addr: 0.0.0.0:9091 # gRPC: matrix.market.v1.MarketService
inference:
addr: 0.0.0.0:9092 # gRPC: matrix.inference.v1.InferenceService
echo_provider: demo-inference-provider # GPU-free backend; "" registers none
backends: # what this node actually serves
- id: my-gpu-box
kind: local-http # echo | openai | local-http
base_url: http://127.0.0.1:8000
models: [llama-3.3-70b] # what a model request routes on
capacity: 1000
price_per_unit: 3
connect:
addr: 0.0.0.0:9093 # HTTP/JSON + /v1/chat/completions; "off" disables
public_reads: false # open Get*/List* to keyless callers
signed_writes: false # open the signature-authorised writes
rate_limit_per_minute: 600 # per caller; 0 disables
rate_limit_burst: 120
allowed_origins: # browser origins; empty = no browser may call
- http://127.0.0.1:5173
- http://localhost:5173
consensus:
membership_mode: bonded-open # self-signed admission after a positive bond
participate_in_open_set: true # this node bonds and requests admission
validators: [] # empty creates the secure solo/dev genesis set
epoch_length: 100 # blocks between set changes taking effect; must match everywhere
approved_changes: [] # ignored by bonded-open membership
eject_equivocators: true # objectively proven double-votes are removed and slashed
stake:
enabled: true # required by bonded-open membership
min_bond: 1000000000000000 # positive admission floor in native base units
unbonding_period: 0 # 0 = 1000 blocks after leaving before withdrawal
bond: 1000000000000000 # this node's target; fund its consensus account first
fee_basis_points: 100 # 1% of each transfer to validators; code cap is 100
maintainer_account: "" # paid a standing cut of the fee; empty pays nobody
maintainer_fee_share_basis_points: 0 # that cut, in hundredths of a percent OF THE FEE; capped at 5000
rewards:
per_block: 0 # pool payout per block, shared by the providers it paid
half_life: 0 # 0 = halve every 1,000,000 blocks
approved_providers: [] # "add:<account id>"; needs a quorum of operators
genesis:
allocations: [] # [{account, amount}] credited once, at first start
reward_pool: 1000000000000000000 # native base units held for funding accounts
bridge:
contract: "" # verified WrappedMatrix address; empty leaves bridge off
attestor_keystore: "" # only for a fixed contract attestor; not every validator
chain_id: 0 # 84532 rehearsal or 8453 Base production
watch:
enabled: false
rpc_url: ""
start_block: 0
confirmations: null # null = 12
poll_interval: 0s # 0 = default
max_block_span: 0A missing section takes its defaults, so this is also a valid config:
network:
listen_addr: /ip4/127.0.0.1/tcp/9000
storage:
engine: pebble
path: ./dataExplicit public Base overlay
A public deployment must opt in deliberately and identically on every node. This example keeps bonded-open membership, a positive stake target, the 100-basis-point protocol fee, a real 64-hex maintainer account with a 5000-basis-point share of that fee, and zero provider emissions. Replace every example value and verify the WrappedMatrix deployment before use.
# Overlay on the generated baseline for the public web/API deployment.
# The maintainer value below is a syntactically valid EXAMPLE; replace it with
# the real 64-lowercase-hex account controlled by the launch maintainer.
connect:
addr: 0.0.0.0:9093
public_reads: true
signed_writes: true
rate_limit_per_minute: 600
rate_limit_burst: 120
allowed_origins:
- https://matrix.example.org # exact deployed web origin; NEVER "*"
consensus:
membership_mode: bonded-open
participate_in_open_set: true
stake:
enabled: true
min_bond: 1000000000000000 # positive; all nodes must agree
bond: 1000000000000000 # fund this node's consensus account
fee_basis_points: 100 # 1%; all nodes must agree
maintainer_account: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
maintainer_fee_share_basis_points: 5000 # 50% OF the 1% fee, not 50% of value
rewards:
per_block: 0 # launch policy: zero provider emissions
bridge:
contract: "<verified WrappedMatrix address>"
chain_id: 84532 # Base Sepolia rehearsal; use 8453 for Base production
attestor_keystore: "<only on a node whose fixed EVM attestor is registered>"
watch:
enabled: true
rpc_url: "<credential-free or server-only Base HTTPS RPC>"
confirmations: 12The web origin and node CORS origin are a pair: list the exact HTTPS site origin on every endpoint, never *. CORS only decides which browser origin may call the endpoint. public_reads: true opens credential-free read methods, while signed_writes: true opens only writes carrying the client's valid signature. Neither setting exposes admin methods, funding, provider registration, private keys, or an unsigned spending path.
network
How the node reaches peers.
listen_addrdefault /ip4/127.0.0.1/tcp/9000- A libp2p multiaddr. Loopback keeps a dev node private; use /ip4/0.0.0.0/tcp/9000 to accept peers.
bootstrap_peersdefault []- Multiaddrs dialled at start. A failure is logged, not fatal.
storage
One embedded Pebble store holds the ledger, the token chain and the committed blocks.
enginedefault pebble- The only engine.
pathdefault ./data- Directory for the store. Back this up; it is the node state.
security
Authentication for every surface.
enable_aclsdefault true- With it on, admin, market, inference and the HTTP endpoint all require a key. With it off they are open to anyone who can reach the port.
api_keysdefault one generated key- A list of {key, role, name, account}. `matrixd -init` writes one admin key, which is why the file is mode 0600. An unset role means admin; an unrecognised one falls to viewer. `account` is the on-chain account the key spends from, which is what makes it usable on /v1/chat/completions: that protocol carries no buyer field, so the key has to say whose balance to charge. The balance stays on the ledger; the node must hold that account's signing key to settle for it.
allow_unsigned_agentsdefault false- Whether unsigned agent code may be loaded.
admin / market / inference
The gRPC listeners, one port each.
admin.addrdefault 0.0.0.0:9090- Deploy, health and log services.
market.addrdefault 0.0.0.0:9091- The marketplace: providers, jobs, balances, transfers.
inference.addrdefault 0.0.0.0:9092- LLM inference jobs.
inference.echo_providerdefault demo-inference-provider- Registers a deterministic GPU-free backend under this provider id at start, so the inference path works with no models. Empty registers none.
inference.backendsdefault none- The backends this node serves. Each entry needs an id, a kind (echo, openai for any OpenAI-compatible vendor, or local-http for a model server on this machine), a capacity and a price_per_unit; base_url and api_key_env configure the upstream, and models lists what it serves. Each entry is registered twice at start: in the inference registry as the thing that fulfills the job, and on the order book as a provider that can reserve capacity and be paid.
inference.backends[].modelsdefault none- The model identifiers this backend serves, lowercased and de-duplicated. A request naming a model is routed to the cheapest provider advertising it that still has capacity. A backend that declares no model is still reachable by naming its provider id.
connect
The HTTP surface a browser can use. Raw gRPC needs HTTP/2 trailers no browser can produce.
addrdefault 0.0.0.0:9093- Serves the market and inference services as JSON. Set to "off" to disable it entirely.
public_readsdefault false- Opens the read methods (Get*, List*) to callers with no API key. Writes still require one, so this cannot move money or reserve capacity. Off by default and it stays that way: chain data is public information, but turning this on by default would widen what an unauthenticated caller can see on every node that already exists. Turn it on for a PUBLIC endpoint, because a browser cannot hold a secret and the alternatives - shipping a key to everyone who loads the page, or not reading the chain from a page at all - are both worse.
signed_writesdefault false- Opens the three write methods whose authority is a client signature rather than an API key: SubmitSignedTransfer, SettleInferenceJob and RunInferenceJob. It is what a self-custody page needs, because a browser cannot keep a key secret. Turning it on ALSO makes RunInferenceJob require the buyer's signed run authorization, and that is not a separate switch on purpose: without it `buyer` is just a string, so an open RunInferenceJob would let anyone name someone else's funded account, have a provider do the work, and never sign for it. Nothing else is opened - FundAccount, RegisterProvider, SubmitJob, CompleteJob and CancelJob carry no signature that could stand in for a credential.
rate_limit_per_minutedefault 600- How many requests one caller may make per minute, counted per credential where one is presented and per remote address otherwise. Nothing bounded this before, on an endpoint where every call reserves capacity, reads a ledger or runs a model. A refusal is a 429 with Retry-After, in the envelope the caller's protocol expects. Zero disables it. Note the limitation: the remote address is the TCP peer, not X-Forwarded-For, so behind a reverse proxy every unauthenticated request looks like one caller and the proxy should limit too.
rate_limit_burstdefault 120- How many requests a caller may make back to back before the sustained rate applies. Zero means one minute's worth. CORS preflights are not counted, since a browser sends one per request.
allowed_originsdefault the Console dev origins- Empty means no browser may call the endpoint. The generated baseline lists only the Console development origins. A public endpoint must replace those with the exact HTTPS origin of the deployed web app on every validator endpoint; never use "*" in a public profile. CORS is not authentication: non-browser callers send no Origin, public_reads controls credential-free reads, and signed_writes still verifies client signatures.
consensus
The genesis validator set and admission policy. Every node must agree on genesis ids, epoch_length, membership_mode and stake policy or they derive different leader schedules and quorums. Bonded-open candidates self-sign admission after bonding; operator-approved networks use approved_changes. The set in force is chain state and resumes from committed history.
membership_modedefault bonded-open- The generated baseline uses bonded-open membership: a candidate self-signs admission after its positive minimum bond commits, and may self-sign a voluntary exit. operator-approved preserves the private allow-list model. In bonded-open mode approved_changes does not admit or remove honest validators; objectively proven equivocation is still slashable.
participate_in_open_setdefault true- Whether this node should bond and request admission in bonded-open mode. False makes an active validator submit a voluntary exit and keeps a candidate passive.
validatorsdefault []- The GENESIS set, as hex account ids. A non-empty list is authoritative and must match for every node. An empty list creates the generated solo/dev set; a later bonded-open candidate starts as a passive follower, replays that genesis history, bonds, then self-signs admission.
epoch_lengthdefault 100- How many committed blocks make an epoch. A set change carried by a committed block takes effect at the next height that is a multiple of this, so every node switches at the SAME height - nodes that disagree about it would switch at different heights, which is a fork. Zero means 100.
approved_changesdefault []- Set changes this operator approves in operator-approved mode, written as add:<64-hex public key> or remove:<64-hex account id>. Bonded-open admission and voluntary exit are self-signed and ignore this list; objective equivocation evidence remains the only path to force-removing a validator there.
stake.enableddefault true- Turns bonded stake on and is required by bonded-open membership. Voting power becomes an account’s bonded native MATRIX, admission requires stake.min_bond, and a validator proven to have equivocated loses its whole bond to the reward pool. operator-approved networks may turn it off for a permissioned set of equals, but bonded-open refuses to start without stake and a positive minimum.
stake.min_bonddefault 1e15 base units- What an account must have bonded before bonded-open admission. The generated baseline writes the positive default explicitly. Every node must agree on the floor; lowering it changes the economic cost of admission. This validator minimum is separate from the bridge lock minimum of 100 MATRIX.
stake.unbonding_perioddefault 0 (1000 blocks)- How many blocks after LEAVING the validator set an account waits before it may withdraw its bond. A sitting validator may not withdraw at all. The delay is why a bond deters anything: without it a validator could equivocate, be ejected, and withdraw before the network committed the slash, so it has to be long enough for evidence to be gossiped, voted on and committed. A withdrawal submitted early waits in the mempool and lands by itself.
stake.bonddefault 1e15 base units- How much of its own native MATRIX this node targets as bonded stake in the generated bonded-open baseline. The node bonds the shortfall itself and keeps topping it up; fund the printed consensus account first. A private operator-approved deployment may set zero, but bonded-open admission requires at least the positive minimum.
fee_basis_pointsdefault 100- The protocol fee taken from every value transfer a committed block carries, in hundredths of a percent, paid to the validator set pro rata by voting power. It is what makes a bond worth posting: without it stake is a pure cost. Taken OUT of the amount, so a 10,000 transfer at 1% credits the recipient 9,900 - adding it to the sender would make a transfer that was affordable at proposal time unaffordable at apply time. Capped at 100 IN CODE, so a mistyped 1000 fails at startup instead of taking ten times the cut. A generated config sets it to 100 (1%), the code cap; set it to 0 for a fee-free network. Every node must agree on the rate. It reaches ALL consensus-settled value: marketplace settlement AND a plain `matrix wallet transfer` are signed transfers that settle through consensus, so both pay it - there is no path that escapes the fee.
maintainer_accountdefault ""- An account paid a standing cut of the protocol fee, taken before the rest is split among validators. Empty pays nobody, which is the default. It exists for whoever starts a network and then keeps operating and developing it, because neither alternative covers that: a validator share DILUTES as the set grows, so it shrinks exactly as the project succeeds, and a genesis allocation funds a moment rather than an ongoing obligation. It must be a real account id (64 lowercase hex), not a reserved name - the node refuses to start on anything else, because a typo would pay the fee into an account nobody holds a key for, every block, forever, and look exactly like it was working.
maintainer_fee_share_basis_pointsdefault 0- That cut, in hundredths of a percent OF THE FEE rather than of the transfer. 2000 is a fifth of the fee; at the 1% fee that is 0.2% of transferred value. Capped at 5000 (half the fee) IN CODE, because the fee is what makes a bond worth posting by a third party and a maintainer taking most of it would be spending the budget that buys the network its security. Every node must agree on it exactly as with the rate - which is also why an operator cannot quietly set it to zero and keep the network: a node using a different share computes different balances from the same block and forks itself off. It is a tax on users, so the node prints it at startup with both the share of the fee and the share of transferred value.
rewards.per_blockdefault 0- What the genesis reward pool pays per committed block. The generated baseline and public launch overlay set this to zero: providers earn user-paid MATRIX only, with no provider token emissions. matrixd can run a nonzero, supply-tracked reward schedule for a separate network, but that is not the launch policy.
rewards.half_lifedefault 0 (1,000,000 blocks)- How many blocks halve the emission. The schedule is a right shift, so it reaches exactly zero rather than trailing off. Total ever paid is about 1.44 × per_block × half_life, so a target spend T over a half-life H wants per_block near T / (1.44 × H).
rewards.approved_providersdefault []- This operator’s allow-list of registry changes, as add:<64-hex account id> or remove:<id>. A registration needs a QUORUM of operators to have listed it, the same as admitting a validator, and the node offers what it has listed until the change commits. Without a registry the emission would pay whoever happened to receive a transfer. Registration takes effect from the next block, so a block cannot register an account and pay it in the same breath.
eject_equivocatorsdefault true- Vote to remove a validator this node holds proof equivocated - two votes for different blocks at one height, round and phase, both signed by that validator’s key - with no entry in approved_changes. The evidence proves itself and every node checks it rather than trusting a peer, so there is no operator judgement to make. False keeps detection, recording and gossip and leaves the removal to you.
genesis
Applied once, on first start, and idempotent afterwards. Nothing is minted past the 1,000,000,000 MATRIX cap.
allocationsdefault []- A list of {account, amount} in native base units.
reward_pooldefault 1e18 (the whole cap)- Held in the reserved account native/reward-pool. `matrix fund` moves coins out of it; it never mints. On a validator SET, `fund` refuses: reward-pool funding is not consensus-ordered, so it would move value on one node and nowhere else, diverging each node's pool and forking the provider emission. Set the allocation in every node's genesis instead, or move value with a signed transfer.
agent
The WebAssembly agent runtime and what it charges. Every price defaults to zero, so a node runs agents for free until an operator decides otherwise.
addrdefault 0.0.0.0:9094- Listen address for matrix.agent.v1.AgentService, distinct from the admin (9090), market (9091), inference (9092) and connect (9093) ports.
max_module_bytesdefault 0 (32 MiB)- Largest wasm module accepted. A larger one is refused before it is stored or run.
run_pricedefault 0- Native MATRIX base units charged per agent run, settled from the deploying account to run_price_recipient through consensus. Zero disables metering: nothing is charged and no signing key is needed. A deploy whose payer cannot afford the charge, or has no signing key, is refused rather than run for free.
run_price_recipientdefault ""- The account both the run price and the storage rent are paid to. Required when either price is set.
storage_pricedefault 0- Native MATRIX base units per MiB per day for a stored module. Zero disables rent. The rate a deployment is billed at is fixed when deployed, so raising this price never re-prices modules already stored.
storage_rent_intervaldefault 0 (1h)- How often rent is swept. The interval does not change the price: a sub-credit sweep charges nothing and advances no clock, so the remainder keeps accruing until it crosses one credit.
storage_rent_gracedefault 0 (72h)- How long a deployment whose rent went unpaid survives before its module bytes are EVICTED. Eviction is what makes rent a bound on disk rather than an unpayable debt that grows, so it cannot be turned off - only lengthened. Paying again clears the clock. A deployment with no recorded owner is skipped and never evicted, so an upgrade cannot delete an operator's own agents.
default_deployerdefault ""- The account charged when a deploy names none. On an authenticated node the payer is the account on the CALLER's own api key and a request naming a different account is refused, so this is the unauthenticated fallback.
allow_senddefault false- Turns on the runtime's inter-agent send(). Off by default: every send is refused and reported to the guest's stderr. It does nothing alone - send_allowlist must also name who may be addressed.
send_allowlistdefault []- Deployment ids on this node a running module may address. Empty refuses every send even with allow_send true. There is deliberately no wildcard.
bridge
The lock-and-mint bridge to wMATRIX on Base. Off unless an exact contract address and chain are configured. Before every browser lock, a fresh challenge-response gate proves a live registered attestor threshold for that deployment; this still does not replace source verification or post-lock reconciliation.
contractdefault ""- Exact deployed WrappedMatrix address. Empty leaves the subsystem off. Base Sepolia rehearsal is chain 84532; Base production is 8453. Verify code and deployment records independently before funding it.
chain_iddefault 0- EVM chain id bound into every mint attestation: 84532 for Base Sepolia rehearsal or 8453 for Base production.
watch.enableddefault false- Runs the burn-to-unlock watcher inside matrixd and polls confirmed Burned events exactly once. Burn release is always a native-consensus operation: on a validator set, each watcher submits its observation and escrow releases only in the committed block where native voting power crosses quorum. A standalone bridge-watch process uses a throwaway diagnostic ledger and is not this production path. The contract’s fixed EVM attestor committee is separate from these dynamic native validators.
watch.rpc_urldefault ""- Ethereum JSON-RPC endpoint for the watcher.
attestor_keystoredefault ""- Path to a fixed deployment attestor’s encrypted secp256k1 key, generated by `matrix bridge attestor-new`. The address must be one of the immutable WrappedMatrix attestors chosen when that contract was deployed. This EVM committee does not follow bonded-open validator joins or exits automatically; only operators assigned to the fixed committee configure a key. The passphrase comes from MATRIX_ATTESTOR_PASSPHRASE and is never stored in YAML.
watch.confirmationsdefault null (12)- Blocks to wait behind head. Set 0 only on a chain with instant finality, such as a local hardhat node.
watch.start_blockdefault 0- Where to begin scanning. A persisted cursor takes over after the first run, so a restart does not rescan.
Joining an existing network
Two things have to match: the bootstrap peers you dial, and the validator set. Every node derives its leader schedule from that list, so a node with a different list is on a different network even if it can reach the others.
consensus:
validators:
- 3f9a... # every node in the set lists the SAME ids
- 7c21...
- b0e4...
epoch_length: 100
approved_changes:
- add:9f2c1e4b... # a new validator's consensus identity (its public key, hex)
- remove:7c21... # an account id to ejectEnvironment
MATRIX_ADMIN_API_KEY adds one admin key on top of whatever the config lists, for deployments that keep secrets out of files. It is additive, not a replacement.
Next
- Network setup - running more than one node
- Architecture - what each listener belongs to