Build an operator-facing UI: registry status, queue depth, fee stats, exit controls.
If you’re running a relayer, your operator dashboard is the control panel: register, monitor, top up bond, change fee, request exit. The SDK ships every primitive needed — this page assembles them.
What to surface
Active / cooldown / offline / unregistered. Bond, fee, registered-at.
Pending authorize orders the matcher is holding. From /api/info.
Trades settled per hour, fees collected. Subscribe to
PrivateSettledAuth events.
Current bond, headroom over minBond. (No slashing history —
the registry does not slash; reputation events live in the
Dispute Registry once it ships.)
Read everything in one shot
import {
loadOperatorRow,
RelayerClient,
} from "@zkscatter/sdk/relayer";
const [row, info] = await Promise.all([
loadOperatorRow(
network.contracts.relayerRegistry,
operatorAddress,
readProvider,
),
new RelayerClient("https://my-relayer.example.com").getInfo(),
]);
// row.status: "active" | "cooldown" | "offline" | "unregistered"
// row.bondEth: human-readable bond
// info.orderCount: pending queue depthDrive the UI off row.status:
| Status | UI affordances |
|---|---|
unregistered | Register form (URL + fee + bond) |
active | Update info / Add bond / Request exit |
cooldown | Show cool-down timer; disable updates |
offline | Show “re-register” CTA |
Settlement event feed
import { ethers } from "ethers";
import { PRIVATE_SETTLEMENT_IFACE } from "@zkscatter/sdk";
const contract = new ethers.Contract(
network.contracts.privateSettlement,
PRIVATE_SETTLEMENT_IFACE,
readProvider,
);
// PrivateSettledAuth indexes 3 fields: makerNullifier, takerNullifier,
// makerRelayer. Filter by the relayer address (5th positional, 3rd
// indexed) to scope to your own settlements where you matched as
// the maker-side relayer.
const filter = contract.filters.PrivateSettledAuth(null, null, operatorAddress);
contract.on(filter, (makerNull, takerNull, claimsRootMaker, claimsRootTaker, makerRelayer, takerRelayer, submitter, feeTokenMaker, feeTokenTaker) => {
// append to UI
});For settlements where you were the taker relayer, scan and
filter post-hoc on takerRelayer (non-indexed by Solidity’s
3-indexed cap) or maintain your own secondary index keyed on
submitter.
Fee adjustments
import { updateRelayerInfo } from "@zkscatter/sdk/relayer";
await updateRelayerInfo(
network.contracts.relayerRegistry,
{ url: row.url, feeBps: 25 }, // 0.25 %
signer,
);Fee changes propagate to apps on their next loadRelayersWithApiInfo
refresh — typically within 60s.
Bond posture widget
function BondPosture({ row, minBond }: { row: OperatorRow; minBond: bigint }) {
const ratio = Number((row.bond * 1000n) / minBond) / 1000;
return (
<div>
<div>Current bond: {row.bondEth} (registry bond asset)</div>
<div>Minimum: {ethers.formatEther(minBond)} (registry bond asset)</div>
<div>Headroom: {(ratio - 1).toFixed(2)}× over minimum</div>
{ratio < 1.5 && <Warning>Bond is close to `minBond` — apps doing operator filtering may rank operators with thinner bond margins lower</Warning>}
</div>
);
}Exit flow UI
import { requestRelayerExit, executeRelayerExit, EXIT_COOLDOWN_SECONDS } from "@zkscatter/sdk/relayer";
function ExitFlow({ row }: { row: OperatorRow }) {
if (row.status === "active") {
return <button onClick={() => requestRelayerExit(/* ... */)}>Request exit (7-day cool-down)</button>;
}
if (row.status === "cooldown") {
const unlockAt = row.exitRequestedAt + EXIT_COOLDOWN_SECONDS;
const remaining = unlockAt - Math.floor(Date.now() / 1000);
if (remaining > 0) {
return <div>Bond unlocks in {Math.ceil(remaining / 86400)} days</div>;
}
return <button onClick={() => executeRelayerExit(/* ... */)}>Withdraw bond</button>;
}
return null;
}Reference implementation
The Tokamak-shipped operator dashboard lives in apps/operators/
under the monorepo. Patterns to copy:
OperatorIdentityBar— header showing status / address / bonduseOperator— hook wrappingloadOperatorRowwith auto-refreshuseRegistryWrite— abstracts the four write operations with toast feedback
If you build your own, point at it via the registry’s url field;
apps don’t care which dashboard implementation an operator uses.