This broker uses a JSON policy file to restrict:
- Withdrawals: permitted exchanges, networks, tokens, and destination address whitelist
- Deposits: permitted exchanges, networks, and tokens (gates deposit address fetching)
- 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.example.json - Backtest policy:
policy/policy.backtest.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 |
{ rule?: DepositRuleEntry[] } |
Deposit restrictions (optional; omitting rule or using {} allows all) |
order |
{ rule: OrderRule } |
Order/conversion restrictions |
Canonical type: PolicyConfig in src/types.ts.
{
"withdraw": {
"rule": [
{ "exchange": "BINANCE", "network": "ARBITRUM", "coins": ["ETH", "USDT"], "whitelist": ["0x..."] }
]
},
"deposit": {
"rule": [
{ "exchange": "BINANCE", "network": "ARBITRUM", "coins": ["ETH", "USDT"] }
]
},
"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.
| Type | string[] |
| Required | No |
| Normalisation | Each entry is trimmed, uppercased |
Optional array of CEX ticker symbols that are allowed for withdrawal under this rule.
Accepted values:
- An array of ticker symbols — e.g.
["ETH", "USDT", "USDC", "ARB"]. Only these tokens may be withdrawn when this rule matches. ["*"]— wildcard; allows any token (same as omitting the field).- Omitted / not present — allows any token (backward compatible with rules written before
coinswas added).
Matching is case-insensitive — both the policy value and the request ticker are uppercased before comparison.
{
"withdraw": {
"rule": [
{
"exchange": "BINANCE",
"network": "ARBITRUM",
"coins": ["ETH", "USDT", "USDC", "ARB"],
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
},
{
"exchange": "BINANCE",
"network": "*",
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
},
{
"exchange": "*",
"network": "BEP20",
"coins": ["BNB", "USDT"],
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
},
{
"exchange": "*",
"network": "*",
"whitelist": ["0x9d467fa9062b6e9b1a46e26007ad82db116c67cb"]
}
]
}
}In this example, a BINANCE + ARBITRUM withdraw uses the first rule (priority 4) and only allows ETH, USDT, USDC, and ARB. A BINANCE + SOL withdraw falls to the second rule (priority 3), which has no coins restriction — any token is allowed. A KRAKEN + BEP20 withdraw uses the third rule (priority 2) and is restricted to BNB and USDT. Everything else hits the global catch-all (priority 1), which also allows any token.
Common rejection reasons:
- no matching exchange + network entry
- address not whitelisted
- token not in
coinsfor the matched rule
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 | { rule?: DepositRuleEntry[] } |
| Required | Yes (the deposit key must be present) |
Deposit validation gates the FetchDepositAddresses action — a request to fetch a deposit address is rejected if the policy does not permit deposits for the given exchange, network, and token. The actual deposit confirmation is not gated by the policy (only the address fetch is).
"deposit": {}— norulekey or empty rule array: all deposits are allowed. This is backward compatible with policies written before deposit rules were added."deposit": { "rule": [...] }— only deposits matching a rule are allowed.
Optional. When omitted or empty, all deposits are permitted.
Each entry scopes deposit permissions to an exchange + network combination, optionally restricted to specific tokens. When a FetchDepositAddresses request arrives, the broker finds the highest-priority matching rule and validates the token against it.
Same priority scheme as withdraw rules:
| 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 rules are present but no entry matches, the request is rejected.
| Type | string |
| Required | Yes |
| Normalisation | Trimmed, uppercased |
Same as withdraw.rule[].exchange — an exchange identifier (e.g. "BINANCE") or "*" for wildcard.
| Type | string |
| Required | Yes |
| Normalisation | Trimmed, uppercased |
Same as withdraw.rule[].network — a network/chain identifier (e.g. "ARBITRUM") or "*" for wildcard.
| Type | string[] |
| Required | No |
| Normalisation | Each entry is trimmed, uppercased |
Optional array of CEX ticker symbols allowed for deposit under this rule. Behaves identically to withdraw.rule[].coins:
- An array of ticker symbols — e.g.
["ETH", "USDT"]. Only these tokens may be deposited. ["*"]— wildcard; allows any token (same as omitting the field).- Omitted / not present — allows any token.
Matching is case-insensitive.
{
"deposit": {
"rule": [
{
"exchange": "BINANCE",
"network": "ARBITRUM",
"coins": ["ETH", "USDT", "USDC", "ARB"]
},
{
"exchange": "*",
"network": "*"
}
]
}
}In this example, a BINANCE + ARBITRUM deposit address request is matched by the first rule (priority 4) and is restricted to ETH, USDT, USDC, and ARB. Any other exchange/network combination hits the catch-all rule, which allows all tokens.
Common rejection reasons:
- deposit rules are present but no matching exchange + network entry
- token not in
coinsfor the matched rule
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. - Withdraw token rejected: ensure the matched
withdraw.rule[].coinsincludes the token ticker (or omitcoinsto allow all). - Deposit address fetch rejected: ensure
deposit.rulecontains a matching entry for the exchange + network + token, or use"deposit": {}to allow all. - 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.