Maker-Taker Arbitrage Strategy
Supported Exchanges : Currently only Kuru is supported. Support for Hyperliquid, Aster, and Lighter is coming soon.
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_B
Balance 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_amount
This 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-arbitrage
5. 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-arbitrage
Submit a new maker-taker arbitrage strategy.
Field Type Required Description symbolstring Yes Trading pair symbol (e.g., “MON/AUSD”) amountstring Yes Order amount for both legs makerobject Yes Maker leg configuration takerobject Yes Taker leg configuration takerTriggerConfigobject No Trigger conditions for taker execution timeoutnumber No Strategy timeout in milliseconds clientStrategyIdstring No Custom strategy ID reconciliationobject No Position reconciliation configuration
Show Maker Leg Configuration
Field Type Required Description exchangestring Yes Exchange name (e.g., “kuru”, “hyperliquid”) accountIdstring Yes Account ID for this leg sidestring Yes Order side: “buy” or “sell” pricestring Yes Limit price for the maker order
Show Taker Leg Configuration
Field Type Required Description exchangestring Yes Exchange name accountIdstring Yes Account ID for this leg sidestring Yes Order side (must be opposite of maker) pricestring No Limit price (if omitted, uses market order) slippagestring No Slippage tolerance for market orders
Show Taker Trigger Configuration
Field Type Required Description priceConditionobject No Price spread trigger condition depthConditionobject No Order book depth trigger condition conditionLogicstring No ”AND” (default) or “OR” timeoutnumber No Trigger timeout in milliseconds
Price Condition :Field Type Description spreadThresholdstring Spread threshold to trigger conditionstring ”above” or “below”
Depth Condition :Field Type Description takerBufferVolumestring Additional volume buffer beyond order amount
Show Reconciliation Configuration
Field Type Default Description enabledboolean true Enable position reconciliation timeoutMsnumber 30000 Wait time before checking discrepancies discrepancyThresholdstring ”0” Minimum discrepancy to trigger recovery maxSlippageBpsnumber 100 Max slippage for recovery orders (basis points)
Example Request
Basic
With Trigger Conditions
{
"symbol" : "MON/AUSD" ,
"amount" : "100" ,
"maker" : {
"exchange" : "kuru" ,
"accountId" : "acc_A" ,
"side" : "buy" ,
"price" : "0.01"
},
"taker" : {
"exchange" : "kuru" ,
"accountId" : "acc_B" ,
"side" : "sell"
}
}
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 strategyIdstring 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"
}
}
}
Show Response with Recovery
When a strategy completes with recovery (position discrepancy was handled): {
"strategyId" : "mta_550e8400-e29b-41d4-a716-446655440001" ,
"status" : "completed_with_recovery" ,
"createdAt" : 1710000000000 ,
"updatedAt" : 1710000010000 ,
"stateHistory" : [ "pending" , "placing_maker" , "waiting_trigger" , "taker_triggered" , "reconciling" , "recovering" , "completed_with_recovery" ],
"makerLeg" : {
"orderId" : "12345" ,
"status" : "canceled" ,
"filledSize" : "100" ,
"remainingSize" : "0" ,
"avgFillPrice" : "0.01" ,
"placeTxHash" : "0x1234..." ,
"cancelTxHash" : "0x5678..." ,
"recovery" : {
"orderId" : "99999" ,
"txHash" : "0xaaaa..." ,
"status" : "closed" ,
"filledSize" : "20" ,
"remainingSize" : "0" ,
"avgFillPrice" : "0.0098"
}
},
"takerLeg" : {
"orderId" : "67890" ,
"status" : "closed" ,
"filledSize" : "80" ,
"avgFillPrice" : "0.0099" ,
"txHash" : "0xabcd..." ,
"recovery" : null
},
"error" : null ,
"request" : { ... }
}
In this example:
Maker filled 100, taker only filled 80
Recovery order closed the 20 unit discrepancy
Net position change = min(100, 80) = 80 → completed_with_recovery
Show Response with Exited (No Net Change)
When a strategy exits with recovery but no net position change (e.g., taker failed): {
"strategyId" : "mta_550e8400-e29b-41d4-a716-446655440002" ,
"status" : "exited_with_recovery" ,
"createdAt" : 1710000000000 ,
"updatedAt" : 1710000010000 ,
"stateHistory" : [ "pending" , "placing_maker" , "waiting_trigger" , "taker_triggered" , "safe_exiting" , "recovering" , "exited_with_recovery" ],
"makerLeg" : {
"orderId" : "12345" ,
"status" : "canceled" ,
"filledSize" : "100" ,
"remainingSize" : "0" ,
"avgFillPrice" : "0.01" ,
"placeTxHash" : "0x1234..." ,
"cancelTxHash" : "0x5678..." ,
"recovery" : {
"orderId" : "99999" ,
"txHash" : "0xaaaa..." ,
"status" : "closed" ,
"filledSize" : "100" ,
"remainingSize" : "0" ,
"avgFillPrice" : "0.0098"
}
},
"takerLeg" : {
"orderId" : null ,
"status" : "rejected" ,
"filledSize" : "0" ,
"avgFillPrice" : null ,
"txHash" : null ,
"recovery" : null
},
"error" : null ,
"request" : { ... }
}
In this example:
Maker filled 100, taker failed (filled 0)
Recovery order closed all 100 units
Net position change = min(100, 0) = 0 → exited_with_recovery
Strategy safely exited but achieved no trading effect
Get Strategy Balances
GET /v2/all/strategy/balances
Query available balances for multiple accounts. Useful for checking balances before submitting a strategy.
Query Parameters
Parameter Type Required Description symbolstring Yes Trading pair symbol (e.g., “MON/AUSD”) accountsstring Yes Comma-separated “exchange:account_id” pairs
Example
GET /v2/all/strategy/balances?symbol=MON/AUSD&accounts=kuru:acc_A,kuru:acc_B
Response
{
"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 pendingSubmit maker_placingpendingCancel cancelledmaker_placingMakerOrderPlaced waiting_triggermaker_placingMakerOrderRejected failedmaker_placingCancel safe_exitingwaiting_triggerTriggerSatisfied taker_triggeredwaiting_triggerCancel safe_exitingwaiting_triggerTimeout safe_exitingwaiting_triggerShutdown safe_exitingtaker_triggeredTakerOrderFilled reconcilingtaker_triggeredTakerOrderFailed safe_exitingreconcilingNo discrepancy completedreconcilingHas discrepancy recoveringsafe_exitingMakerCancelled (no fills) cancelled / timed_out / failed*safe_exitingMakerCancelled (has fills) recoveringsafe_exitingMakerCancelFailed (no fills) requires_interventionsafe_exitingMakerCancelFailed (has fills) recoveringrecoveringRecoveryOrderFilled (net > 0) completed_with_recoveryrecoveringRecoveryOrderFilled (net = 0) exited_with_recoveryrecoveringRecoveryOrderFailed failedrecoveringMakerCancelFailed requires_intervention
*Terminal state depends on exit reason: UserCancel → cancelled, Timeout → timed_out, TakerFailed → failed
Status Descriptions
Status Terminal Description pendingNo Strategy submitted, waiting to place maker order maker_placingNo Placing maker order on the maker exchange waiting_triggerNo Maker order placed, waiting for trigger conditions to be satisfied taker_triggeredNo Trigger conditions met, placing taker order reconcilingNo Taker executed, in reconciliation waiting period to check for fill discrepancies safe_exitingNo Intermediate state - safely exiting strategy (handling maker fills before cancel/timeout/failure) recoveringNo Executing recovery order to close position discrepancy completedYes Strategy completed successfully with matched fills completed_with_recoveryYes Strategy completed with recovery order executed. Net position change > 0 (actual trading effect achieved) exited_with_recoveryYes Strategy exited with recovery order but net position change = 0 (no trading effect, safe exit only) failedYes Strategy failed (see error field for details) cancelledYes Strategy was cancelled by user request timed_outYes Strategy timed out waiting for trigger conditions requires_interventionYes 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, or failed (based on exit reason)
Has maker fills : Goes to recovering, then exited_with_recovery (net = 0) since taker didn’t execute
Exit Reason Description user_cancelUser requested cancellation via cancel endpoint timeoutStrategy timed out waiting for trigger (includes waited_ms and timeout_ms) taker_failedTaker order execution failed (includes error message) shutdownEngine 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 completedPartial taker 100 80 20 80 completed_with_recoveryOverfill taker 50 100 50 50 completed_with_recoveryTaker failed 100 0 100 0 exited_with_recoveryCancel with fills 100 0 100 0 exited_with_recoveryMaker 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_amount
Concrete 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 met
OR: 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/balances to verify sufficient funds before submitting
Set Appropriate Timeouts : Balance between waiting for conditions and avoiding stale strategies
Use Buffer Volume : Add takerBufferVolume for 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