Maker-Taker Arbitrage
Cross-exchange arbitrage strategy with conditional execution
Maker-Taker Arbitrage Strategy
The Maker-Taker Arbitrage strategy allows you to place a maker (limit) order on one exchange and automatically execute a taker order on another exchange when trigger conditions are met.
How It Works
+-------------------------------------------+
| Strategy Execution |
+-------------------------------------------+
| |
| 1. Submit Strategy |
| | |
| v |
| 2. Place Maker Order (GTX/Post-Only) |
| | |
| v |
| 3. Wait for Trigger Conditions |
| - Price spread threshold |
| - Order book depth |
| | |
| v |
| 4. Execute Taker Order |
| | |
| v |
| 5. Reconciliation (optional) |
| | |
| v |
| 6. Strategy Complete |
| |
+-------------------------------------------+Use Cases
Cross-Exchange Arbitrage
Place a maker buy order on Exchange A, sell on Exchange B when spread is profitable.
Same-Exchange Hedging
Place a maker order with one account, hedge with another account on the same exchange.
Typical Workflow
A typical strategy execution follows these steps:
1. Check Account Balances
Before submitting, verify both accounts have sufficient funds:
GET /v2/all/strategy/balances?symbol=MON/AUSD&accounts=kuru:acc_A,kuru:acc_BBalance Requirements by Side:
| Maker Side | Maker Needs | Taker Needs |
|---|---|---|
| SELL | Base asset (e.g., MON) | Quote asset (e.g., AUSD) |
| BUY | Quote asset (e.g., AUSD) | Base asset (e.g., MON) |
2. Determine Maker Price
Fetch the orderbook and choose a maker price. The maker order uses GTX (Post-Only) mode, which has strict price requirements:
GTX Price Rules (to avoid PostOnlyError):
- BUY orders: Price must be below best ask (cannot cross the spread)
- SELL orders: Price must be above best bid (cannot cross the spread)
3. Calculate Depth Trigger (Optional)
If using depthCondition, calculate takerBufferVolume:
takerBufferVolume = cumulative_depth_at_maker_price - order_amountThis triggers the taker when orderbook depth thins to this level.
Example:
- Maker buy @ 0.010, order amount = 100 MON
- Cumulative bid depth at 0.010 = 150 MON
takerBufferVolume= 150 - 100 = 50 MON- Taker triggers when bid depth falls below 150 MON
4. Submit Strategy
POST /v2/all/strategy/submit/maker-taker-arbitrage5. Monitor Strategy Progress
Option A: REST Polling
GET /v2/all/strategy/status/maker-taker-arbitrage/{strategyId}Poll until status is terminal (completed, failed, timed_out, etc.)
Option B: WebSocket Subscription (Recommended)
Subscribe to real-time strategy updates via WebSocket for lower latency and reduced API calls.
Endpoint: wss://api.hypereth.io/v2/all/ws?apiKey=YOUR_API_KEY
Subscribe to all strategies:
{
"jsonrpc": "2.0",
"id": 1,
"method": "subscribe",
"params": ["strategy_updates", {}]
}Subscribe to specific strategy:
{
"jsonrpc": "2.0",
"id": 1,
"method": "subscribe",
"params": ["strategy_updates", {"strategy_ids": ["mta_550e8400-e29b-41d4-a716-446655440000"]}]
}Example update message:
{
"jsonrpc": "2.0",
"method": "subscription",
"params": {
"subscription": "0x8b9c0d1e2f3a...",
"channel": "strategy_updates",
"result": {
"strategyId": "mta_550e8400-e29b-41d4-a716-446655440000",
"status": "waiting_trigger",
"createdAt": 1710000000000,
"updatedAt": 1710000001000,
"stateHistory": ["pending", "placing_maker", "waiting_trigger"],
"makerLeg": {
"orderId": "12345",
"status": "open",
"filledSize": "0",
"remainingSize": "100",
"avgFillPrice": null,
"placeTxHash": "0x1234...",
"cancelTxHash": null,
"recovery": null
},
"takerLeg": null,
"error": null
}
}
}Unsubscribe:
{
"jsonrpc": "2.0",
"id": 2,
"method": "unsubscribe",
"params": ["0x8b9c0d1e2f3a..."]
}6. Alternate Sides (Optional)
To maintain low net position, alternate between buy/sell after each successful strategy.
API Endpoints
Submit Strategy
POST /v2/all/strategy/submit/maker-taker-arbitrageSubmit a new maker-taker arbitrage strategy.
Example Request
{
"symbol": "MON/AUSD",
"amount": "100",
"maker": {
"exchange": "kuru",
"accountId": "acc_A",
"side": "buy",
"price": "0.01"
},
"taker": {
"exchange": "kuru",
"accountId": "acc_B",
"side": "sell"
}
}{
"symbol": "MON/AUSD",
"amount": "100",
"maker": {
"exchange": "kuru",
"accountId": "acc_A",
"side": "buy",
"price": "0.01"
},
"taker": {
"exchange": "kuru",
"accountId": "acc_B",
"side": "sell",
"slippage": "0.005"
},
"takerTriggerConfig": {
"priceCondition": {
"spreadThreshold": "0.001",
"condition": "above"
},
"depthCondition": {
"takerBufferVolume": "10"
},
"conditionLogic": "AND"
},
"timeout": 30000,
"clientStrategyId": "my_strategy_001",
"reconciliation": {
"enabled": true,
"timeoutMs": 30000,
"maxSlippageBps": 100
}
}Response
{
"strategyId": "maker_taker_550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"createdAt": 1710000000000,
"requestId": "req_123456789"
}Get Strategy Status
GET /v2/all/strategy/status/maker-taker-arbitrage/{strategyId}Get the current status of a submitted strategy.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
strategyId | string | Strategy ID from submit response |
Response
{
"strategyId": "mta_550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"createdAt": 1710000000000,
"updatedAt": 1710000005000,
"stateHistory": ["pending", "placing_maker", "waiting_trigger", "taker_triggered", "reconciling", "completed"],
"makerLeg": {
"orderId": "12345",
"status": "closed",
"filledSize": "100",
"remainingSize": "0",
"avgFillPrice": "0.01",
"placeTxHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"cancelTxHash": null,
"recovery": null
},
"takerLeg": {
"orderId": "67890",
"status": "closed",
"filledSize": "100",
"avgFillPrice": "0.0099",
"txHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
"recovery": null
},
"error": null,
"request": {
"symbol": "MON/AUSD",
"amount": "100",
"maker": {
"exchange": "kuru",
"accountId": "acc_A",
"side": "buy",
"price": "0.01"
},
"taker": {
"exchange": "kuru",
"accountId": "acc_B",
"side": "sell"
}
}
}Get Strategy Balances
GET /v2/all/strategy/balancesQuery available balances for multiple accounts. Useful for checking balances before submitting a strategy.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
symbol | string | Yes | Trading pair symbol (e.g., "MON/AUSD") |
accounts | string | Yes | Comma-separated "exchange:account_id" pairs |
Example
GET /v2/all/strategy/balances?symbol=MON/AUSD&accounts=kuru:acc_A,kuru:acc_BResponse
{
"symbol": "MON/AUSD",
"accounts": [
{
"exchange": "kuru",
"accountId": "acc_A",
"account": "0x1234567890abcdef1234567890abcdef12345678",
"baseAvailable": "10000.5",
"quoteAvailable": "5000.0"
},
{
"exchange": "kuru",
"accountId": "acc_B",
"account": "0xabcdef1234567890abcdef1234567890abcdef12",
"baseAvailable": "8000.0",
"quoteAvailable": "12000.0"
}
],
"timestamp": 1710000000000,
"requestId": "req_123456789"
}Strategy Status Lifecycle
The strategy engine uses an explicit state machine with well-defined transitions. Each state represents a distinct execution phase.
State Transition Diagram
+-------------+
| pending |---- Cancel ----> cancelled
+------+------+
| Submit
v
+---------------+
+-----------+ maker_placing +---- Rejected --> failed
| +------+--------+
| Cancel | Placed
v v
+--------------+ +-----------------+
| safe_exiting |<--+ waiting_trigger |
+-------+------+ +-------+---------+
| ^ |
| Cancel --+ | Trigger / Timeout / Shutdown
| Timeout v
| +-----------------+
| | taker_triggered |
| +-------+---------+
| |
| +-----------+-----------+
| | TakerFilled | TakerFailed
| v v
| +-------------+ +--------------+
| | reconciling | | safe_exiting |
| +------+------+ +------+-------+
| | |
| +------+------+ |
| | Discrepancy?| |
| +------+------+ |
| No | Yes |
| v v |
| completed recovering <----------+ (if has fills)
| |
| | RecoveryFilled
| v
| +--------------------------------+
| | net_change > 0? |
| +------+-------------------------+
| Yes | No
| v v
| completed_with_recovery
| exited_with_recovery
|
| (RecoveryFailed -> failed / requires_intervention)
|
| MakerCancelled
v
+--------------------------------------+
| No fills: cancelled/timed_out/failed |
| Has fills: recovering ---------------+--> (see net_change logic above)
+--------------------------------------+State Transitions by Event
| From State | Event | To State |
|---|---|---|
pending | Submit | maker_placing |
pending | Cancel | cancelled |
maker_placing | MakerOrderPlaced | waiting_trigger |
maker_placing | MakerOrderRejected | failed |
maker_placing | Cancel | safe_exiting |
waiting_trigger | TriggerSatisfied | taker_triggered |
waiting_trigger | Cancel | safe_exiting |
waiting_trigger | Timeout | safe_exiting |
waiting_trigger | Shutdown | safe_exiting |
taker_triggered | TakerOrderFilled | reconciling |
taker_triggered | TakerOrderFailed | safe_exiting |
reconciling | No discrepancy | completed |
reconciling | Has discrepancy | recovering |
safe_exiting | MakerCancelled (no fills) | cancelled / timed_out / failed* |
safe_exiting | MakerCancelled (has fills) | recovering |
safe_exiting | MakerCancelFailed (no fills) | requires_intervention |
safe_exiting | MakerCancelFailed (has fills) | recovering |
recovering | RecoveryOrderFilled (net > 0) | completed_with_recovery |
recovering | RecoveryOrderFilled (net = 0) | exited_with_recovery |
recovering | RecoveryOrderFailed | failed |
recovering | MakerCancelFailed | requires_intervention |
*Terminal state depends on exit reason: UserCancel → cancelled, Timeout → timed_out, TakerFailed → failed
Status Descriptions
| Status | Terminal | Description |
|---|---|---|
pending | No | Strategy submitted, waiting to place maker order |
maker_placing | No | Placing maker order on the maker exchange |
waiting_trigger | No | Maker order placed, waiting for trigger conditions to be satisfied |
taker_triggered | No | Trigger conditions met, placing taker order |
reconciling | No | Taker executed, in reconciliation waiting period to check for fill discrepancies |
safe_exiting | No | Intermediate state - safely exiting strategy (handling maker fills before cancel/timeout/failure) |
recovering | No | Executing recovery order to close position discrepancy |
completed | Yes | Strategy completed successfully with matched fills |
completed_with_recovery | Yes | Strategy completed with recovery order executed. Net position change > 0 (actual trading effect achieved) |
exited_with_recovery | Yes | Strategy exited with recovery order but net position change = 0 (no trading effect, safe exit only) |
failed | Yes | Strategy failed (see error field for details) |
cancelled | Yes | Strategy was cancelled by user request |
timed_out | Yes | Strategy timed out waiting for trigger conditions |
requires_intervention | Yes | Manual intervention required (e.g., maker order could not be cancelled, remote node down) |
Terminal States: completed, completed_with_recovery, exited_with_recovery, failed, cancelled, timed_out, requires_intervention
Success States: completed, completed_with_recovery
Safe Exit States: exited_with_recovery, cancelled, timed_out
Exit Reasons
When a strategy enters the safe_exiting intermediate state, it includes a reason for why it's exiting. Note that safe_exiting itself is NOT a terminal state - the strategy will transition to a terminal state after handling maker fills:
- No maker fills:
cancelled,timed_out, orfailed(based on exit reason) - Has maker fills: Goes to
recovering, thenexited_with_recovery(net = 0) since taker didn't execute
| Exit Reason | Description |
|---|---|
user_cancel | User requested cancellation via cancel endpoint |
timeout | Strategy timed out waiting for trigger (includes waited_ms and timeout_ms) |
taker_failed | Taker order execution failed (includes error message) |
shutdown | Engine shutdown initiated |
Recovery Flow
Recovery orders are placed when there's a position discrepancy between maker and taker fills:
- From Reconciliation: After taker executes, if maker has additional fills that weren't matched, a recovery order closes the excess maker position
- From Safe Exit: If cancellation/timeout occurs while maker has fills, a recovery order closes those fills
Calculating Net Position Change:
Net position change = min(makerLeg.filledSize, takerLeg.filledSize)Terminal State Based on Net Change:
| Scenario | Maker Filled | Taker Filled | Recovery | Net Change | Terminal State |
|---|---|---|---|---|---|
| Perfect match | 100 | 100 | 0 | 100 | completed |
| Partial taker | 100 | 80 | 20 | 80 | completed_with_recovery |
| Overfill taker | 50 | 100 | 50 | 50 | completed_with_recovery |
| Taker failed | 100 | 0 | 100 | 0 | exited_with_recovery |
| Cancel with fills | 100 | 0 | 100 | 0 | exited_with_recovery |
| Maker 0, Taker filled | 0 | 100 | 100 | 0 | exited_with_recovery |
Key Distinction:
completed_with_recovery: Net change > 0 — strategy achieved some trading effect (success)exited_with_recovery: Net change = 0 — no actual trading effect, just safe exit (not success)
Check makerLeg.filledSize, takerLeg.filledSize, and recovery.filledSize in the response to verify actual position change.
Trigger Conditions
Price Condition
Checks the spread between maker price and taker exchange's current price.
Spread Calculation:
- Maker BUY + Taker SELL:
spread = taker_bestBid - maker_price - Maker SELL + Taker BUY:
spread = maker_price - taker_bestAsk
Condition Types:
above: Trigger when spread > threshold (cross-exchange arbitrage)below: Trigger when spread < threshold (price convergence)
Depth Condition
Checks order book depth on the taker exchange to ensure sufficient liquidity for the taker to "eat through" to the maker price.
How it works:
- Server monitors the orderbook on the taker exchange
- Calculates cumulative volume from current market price to maker price
- Triggers when:
cumulative_volume >= order_amount + takerBufferVolume
Calculating takerBufferVolume:
If you want the taker to trigger when your maker order is near the front of the queue:
takerBufferVolume = cumulative_depth_at_maker_price - order_amountConcrete Example:
Suppose you're placing a maker BUY at 0.010 for 100 MON:
Orderbook Bids:
Price | Size | Cumulative
---------|--------|------------
0.0105 | 50 | 50
0.0103 | 30 | 80
0.0100 | 70 | 150 ← Your maker price
0.0098 | 40 | 190- Cumulative depth at 0.010 = 150 MON
- Your order = 100 MON
takerBufferVolume= 150 - 100 = 50 MON
The taker will trigger when someone sells enough to bring the cumulative bid depth down to 150 MON (meaning your order is about to get filled).
Buffer Recommendations:
- Conservative: 20-50% of order amount (larger buffer, triggers earlier)
- Moderate: 10-20% of order amount
- Aggressive: 0 or small value (triggers when depth is thin)
Condition Logic
AND(default): Both conditions must be metOR: Either condition triggers execution
Error Handling
Failed Status
When a strategy fails, the response includes an error message:
{
"strategyId": "mta_550e8400...",
"status": "failed",
"error": "Taker order rejected: insufficient balance",
"makerLeg": { ... },
"takerLeg": null
}Common failure reasons:
- Maker order rejected (insufficient balance, invalid price)
- Taker order failed (rejected, timeout, network error)
- Recovery order failed
Requires Intervention Status
When status is requires_intervention, manual action is needed:
{
"strategyId": "mta_550e8400...",
"status": "requires_intervention",
"error": "Failed to cancel maker order after 3 attempts",
"uncancelledOrderId": "12345",
"makerLeg": {
"orderId": "12345",
"status": "open",
"filledSize": "50",
"remainingSize": "50"
}
}This occurs when:
- Maker order could not be cancelled (network issues, exchange errors)
- Position discrepancy cannot be automatically resolved
Manual resolution: Use the uncancelledOrderId to manually cancel the order on the exchange.
Best Practices
- Check Balances First: Use
/strategy/balancesto verify sufficient funds before submitting - Set Appropriate Timeouts: Balance between waiting for conditions and avoiding stale strategies
- Use Buffer Volume: Add
takerBufferVolumefor safety margin in volatile markets - Enable Reconciliation: Keep reconciliation enabled to automatically handle partial fills
- Monitor Status: Poll the status endpoint to track execution progress
Strategy Engine Overview
Server-side strategy execution for low-latency trading
Get All Balances (Cross-DEX) GET
Get balances aggregated across all supported DEXes (Hyperliquid, Aster, Lighter). **Note**: This endpoint uses the `/v2/all` base URL instead of a platform-specific URL. **Endpoint**: `GET https://api.hypereth.io/v2/all/balances`