This broker uses a JSON policy file to restrict:
- Withdrawals: permitted exchanges, networks, and destination address whitelist
- Orders: permitted exchanges/pairs, plus optional directional conversion limits
The policy is loaded at startup. If you start the broker with a policy file path, the file is watched and policies are reloaded when the file changes.
- Example policy:
policy/policy.json - CLI usage:
bun run start-broker --policy policy/policy.json --port 8086 --whitelist 127.0.0.1Top-level keys (all required):
| Key | Type | Description |
|---|---|---|
withdraw |
{ rule: WithdrawRuleEntry[] } |
Withdrawal restrictions (at least one rule entry required) |
deposit |
{} |
Placeholder object — not enforced today |
order |
{ rule: OrderRule } |
Order/conversion restrictions |
Canonical type: PolicyConfig in src/types.ts.
{
"withdraw": {
"rule": [
{ "exchange": "BINANCE", "network": "ARBITRUM", "whitelist": [] }
]
},
"deposit": {},
"order": { "rule": { "markets": [], "limits": [] } }
}When started with a policy file path:
- the policy file is watched for changes
- when changed, the broker reloads the policy and restarts the gRPC server to apply it
Operational note: this restart may briefly interrupt in-flight requests.
Withdraw requests are rejected unless all of the below pass.
Required. Must contain at least one entry.
Each entry scopes a set of withdrawal permissions to an exchange + network combination. When a withdraw request arrives, the broker finds the highest-priority matching rule entry and validates the destination address against that entry.
When multiple entries match a request, the broker selects the single highest-priority entry:
| Priority | exchange |
network |
Description |
|---|---|---|---|
| 4 (highest) | exact match | exact match | Fully specific rule |
| 3 | exact match | "*" |
Exchange-specific, any network |
| 2 | "*" |
exact match | Network-specific, any exchange |
| 1 (lowest) | "*" |
"*" |
Global catch-all |
If no entry matches, the request is rejected.
| Type | string |
| Required | Yes |
| Normalisation | Trimmed, uppercased |
Accepted values:
- An exchange identifier — e.g.
"BINANCE","KRAKEN","BYBIT". Must correspond to a supported CCXT exchange. "*"— wildcard; matches any exchange.
| Type | string |
| Required | Yes |
| Normalisation | Trimmed, uppercased |
Accepted values:
- A network/chain identifier — e.g.
"ARBITRUM","BEP20","ETH","SOL". The value must match what the exchange uses for that chain. "*"— wildcard; matches any network.
Even if the policy allows a network, the selected exchange must also support that network for the currency or the request will still fail at execution time.
| Type | string[] |
| Required | Yes (may be empty, but that would reject all addresses) |
| Normalisation | Each entry is trimmed and lowercased |
Accepted values:
- An array of destination addresses (e.g.
"0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"). - Matching is exact after both the policy value and the incoming address are lowercased.
- Recommendation: store all addresses in lowercase in the JSON for readability and diffs.
{
"withdraw": {
"rule": [
{
"exchange": "BINANCE",
"network": "ARBITRUM",
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
},
{
"exchange": "BINANCE",
"network": "*",
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
},
{
"exchange": "*",
"network": "BEP20",
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
},
{
"exchange": "*",
"network": "*",
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
}
]
}
}In this example, a BINANCE + ARBITRUM withdraw uses the first rule (priority 4). A BINANCE + SOL withdraw falls to the second rule (priority 3). A KRAKEN + BEP20 withdraw uses the third rule (priority 2). Everything else hits the global catch-all (priority 1).
Common rejection reasons:
- no matching exchange + network entry
- address not whitelisted
Order creation is rejected unless all of the below pass:
- Market rule match:
marketscontains at least one pattern that matches the requested exchange + pair - Exchange symbol support: the exchange supports either
FROM/TOorTO/FROM - Limits (optional): if
limitsis present and non-empty, the requested conversion direction exists and the requested amount is within min/max
| Type | string[] |
| Required | Yes |
| Normalisation | Each entry is trimmed, uppercased |
Market pattern formats:
| Pattern | Example | Matches |
|---|---|---|
"*" |
"*" |
Any exchange, any pair |
"<EXCHANGE>:*" |
"BINANCE:*" |
Any pair on the specified exchange |
"*:<BASE>/<QUOTE>" |
"*:BTC/ETH" |
The specified pair on any exchange |
"<EXCHANGE>:<BASE>/<QUOTE>" |
"BINANCE:ETH/USDT" |
The specified pair on the specified exchange |
Matching behaviour:
- Case-insensitive (both the pattern and the request are uppercased before comparison).
- Pair matching is symmetric:
BINANCE:BTC/ETHmatches requests for bothBTC→ETHandETH→BTC. - Any pattern that does not contain a
:separator (other than the bare"*") is ignored.
Examples:
{ "markets": ["*"] }{ "markets": ["BINANCE:*", "KRAKEN:ETH/USDT"] }{ "markets": ["*:BTC/ETH"] }| Type | Array<{ from: string, to: string, min: number, max: number }> |
| Required | No (defaults to [] if omitted) |
Optional directional conversion limits.
Each object in the array:
| Field | Type | Required | Normalisation | Description |
|---|---|---|---|---|
from |
string |
Yes | Uppercased | Source token symbol, e.g. "USDT" |
to |
string |
Yes | Uppercased | Destination token symbol, e.g. "ETH" |
min |
number |
Yes | — | Inclusive lower bound on the from amount |
max |
number |
Yes | — | Inclusive upper bound on the from amount |
Key behaviour:
- If omitted or empty, no amount or direction restrictions are applied — any matched market is allowed.
- If non-empty, each order must match a
from/toentry for the requested direction. Unmatched directions are rejected. - Limits are directional:
USDT→ETHandETH→USDTare separate entries. You must add both if you want to allow both directions. - Limits apply to the request's from amount (the amount of
fromTokenthe caller is converting), even if the broker flips the symbol direction for execution. - If the exchange only supports the reverse symbol, the broker computes a base amount via
amount / price; this requiresprice > 0.
Example:
{
"order": {
"rule": {
"markets": ["BINANCE:*"],
"limits": [
{ "from": "USDT", "to": "ETH", "min": 1, "max": 100000 },
{ "from": "ETH", "to": "USDT", "min": 0.5, "max": 5 }
]
}
}
}| Type | {} (empty object) |
| Required | Yes |
deposit is required in the current policy schema, but it is not enforced today.
At present, deposits are effectively always allowed; this field is reserved for future deposit rule support. Set it to {}.
The broker validates the policy JSON against a Joi schema when loading it.
- All top-level keys (
withdraw,deposit,order) must be present. withdraw.rulemust be a non-empty array (at least one entry).- Every required field must be present and of the correct type.
- If validation fails, the policy load fails and the broker will not start (or will log an error on reload).
- Withdraw address rejected: ensure the address is in the matching
withdraw.rule[].whitelistentry (lowercase recommended). - Withdraw exchange/network rejected: ensure there is a
withdraw.rule[]entry whoseexchangeandnetworkmatch (or wildcard-match) the request. - Order rejected (market): ensure
order.rule.marketscontains a matching pattern for the exchange + pair. - Order rejected (limits): if
limitsis non-empty, ensure there's an entry for the exactfrom→todirection and the amount is within bounds.