The relayer market lets users withdraw from the privacy pool without submitting the withdrawal from their connected wallet. A relayer bids on a job, receives an encrypted payload, submits the Soroban transaction, and earns the escrowed fee.
For the MVP demo, a relayer is already running on testnet at https://g-stelar-relayer.opaque.cash. Operators can run additional relayers and connect them to the shared gateway.
The intended production shape is a shared public gossip hub:
wallet -> public gateway hub -> relayer nodes
Wallets publish and subscribe through a gateway URL from the deployment manifest or frontend environment. Relayer nodes connect to the hub and bid on jobs. This means users do not have to paste a manual relayer URL.
The testnet manifest currently points wallets at the MVP demo gateway:
https://g-stelar-relayer.opaque.cash
The gateway topic is:
opaque/stellar/jobs/v1
The current implementation exposes this over the relayer HTTP gateway. The gossip transport is structured so a libp2p or pubsub backend can replace the in-memory transport later.
- Operator registers a relayer on-chain with stake, endpoint, and X25519 public key.
- Wallet creates an escrowed job in
relayerRegistry. - Wallet advertises the job to the gateway.
- Registered relayers inspect the on-chain job and submit signed bids.
- Wallet validates bids against on-chain registration and free stake.
- Wallet encrypts the withdrawal payload to the selected relayer.
- Relayer decrypts, submits the privacy-pool withdrawal, and earns the fee.
The relayer fee must be less than or equal to the relayer's free stake. A relayer with 0.1 XLM free stake cannot validly bid on a 30 XLM fee.
| Requirement | Notes |
|---|---|
| Node.js 20+ | Required for the relayer workspace. |
| Stellar testnet XLM | Used for stake and transaction fees. |
| Dedicated operator key | Do not reuse the pool admin or deployer key. |
| HTTPS endpoint | Required when the wallet is served over HTTPS. |
| Reverse proxy | Caddy or nginx is recommended. |
git clone https://github.com/collinsadi/opauque-stellar.git
cd opauque-stellar/relayer
npm cistellar keys generate opaque-relayer --network testnet --fund
stellar keys address opaque-relayer
stellar keys show opaque-relayerKeep the S... secret private.
Withdrawal payloads are encrypted to the relayer's X25519 public key. Derive a deterministic 32-byte secret from the operator secret:
node -e "const {createHash}=require('crypto');const s='YOUR_S_SECRET';console.log(createHash('sha256').update(s).update('opaque-relayer-x25519-v1').digest('hex'))"Store the 64-character hex value as RELAYER_X25519_SECRET.
Create stellar/.env at the repo root:
cp .env.example .env
chmod 600 .env
nano .envMinimum config:
RELAYER_OPERATOR_SECRET=S...
RELAYER_X25519_SECRET=abcdef0123...
RELAYER_ENDPOINT=https://relayer.example.com
RELAYER_PORT=8787Optional overrides:
STELLAR_RPC_URL=https://soroban-testnet.stellar.org
RELAYER_REGISTRY_ID=C...
RELAYER_MIN_FEE=100000Register the operator before expecting bids:
cd ~/stellar/relayer
set -a
source ../.env
set +a
npm run registerThe smoke test also registers if needed and verifies the market control plane:
npm run smoke:marketStandalone mode runs one process that includes a local hub, relayer engine, and HTTP gateway:
cd ~/stellar/relayer
npm run relayerHealthy startup:
Opaque relayer gateway listening on https://relayer.example.com
operator=G...
x25519=0x...
registry=C...
Verify:
curl https://relayer.example.com/healthRun one public gateway hub:
cd ~/stellar/relayer
RELAYER_GATEWAY_ENDPOINT=https://gateway.example.com RELAYER_PORT=8787 npm run hubRun each node with outbound hub connection:
# in stellar/.env
RELAYER_HUB_URL=https://gateway.example.com
RELAYER_ENDPOINT=https://operator-1.example.com
cd ~/stellar/relayer
npm run relayerOnly the hub needs to be the public wallet gateway. Each relayer still registers its operator endpoint on-chain as metadata.
Set the public gateway once for the frontend:
# frontend/.env
VITE_RELAYER_GATEWAY_URL=https://gateway.example.comFor a repository-wide default, update the manifest:
"gatewayUrls": [
"https://gateway.example.com"
]The wallet reads gateway URLs through frontend/src/contracts/relayerConfig.ts, then uses the first healthy gateway when creating jobs and fetching bids.
[Unit]
Description=Opaque Stellar relayer
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=YOUR_LINUX_USER
WorkingDirectory=/home/YOUR_LINUX_USER/stellar/relayer
EnvironmentFile=/home/YOUR_LINUX_USER/stellar/.env
ExecStart=/usr/bin/npm run relayer
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetStart it:
sudo systemctl daemon-reload
sudo systemctl enable opaque-relayer
sudo systemctl start opaque-relayer
sudo journalctl -u opaque-relayer -fCaddy example:
relayer.example.com {
reverse_proxy localhost:8787
}
nginx must disable buffering for streaming endpoints:
location / {
proxy_pass http://127.0.0.1:8787;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off;
}| Symptom | Likely cause | Fix |
|---|---|---|
tsx: not found |
Dependencies missing. | Run cd relayer && npm ci. |
| Node starts but never bids | Not registered, wrong X25519 key, wrong endpoint, or fee above free stake. | Run npm run register, compare startup x25519, and lower fee or increase stake. |
| Wallet says no valid bids | Gateway reached, but no registered relayer produced a valid bid. | Check relayer logs, stake, RELAYER_MIN_FEE, and registry record. |
| Payload delivered but no success message | Relayer accepted the payload but submission is still pending or status polling lagged. | Check relayer logs and on-chain job status. |
| Mixed content browser error | HTTPS wallet is calling HTTP gateway. | Use an HTTPS gateway URL. |
fee > freeStake behavior |
Registry validation rejects bids that cannot be backed by stake. | Top up relayer stake or offer a smaller fee. |
The relayer operator key is a hot key with staked XLM. Use a dedicated account, keep .env locked down, run as a dedicated Linux user, and rotate immediately if leaked.