Skip to main content

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 SideMaker NeedsTaker Needs
SELLBase asset (e.g., MON)Quote asset (e.g., AUSD)
BUYQuote 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.) 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.

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"
  }
}

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

ParameterTypeDescription
strategyIdstringStrategy 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/balances
Query available balances for multiple accounts. Useful for checking balances before submitting a strategy.

Query Parameters

ParameterTypeRequiredDescription
symbolstringYesTrading pair symbol (e.g., “MON/AUSD”)
accountsstringYesComma-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 StateEventTo State
pendingSubmitmaker_placing
pendingCancelcancelled
maker_placingMakerOrderPlacedwaiting_trigger
maker_placingMakerOrderRejectedfailed
maker_placingCancelsafe_exiting
waiting_triggerTriggerSatisfiedtaker_triggered
waiting_triggerCancelsafe_exiting
waiting_triggerTimeoutsafe_exiting
waiting_triggerShutdownsafe_exiting
taker_triggeredTakerOrderFilledreconciling
taker_triggeredTakerOrderFailedsafe_exiting
reconcilingNo discrepancycompleted
reconcilingHas discrepancyrecovering
safe_exitingMakerCancelled (no fills)cancelled / timed_out / failed*
safe_exitingMakerCancelled (has fills)recovering
safe_exitingMakerCancelFailed (no fills)requires_intervention
safe_exitingMakerCancelFailed (has fills)recovering
recoveringRecoveryOrderFilled (net > 0)completed_with_recovery
recoveringRecoveryOrderFilled (net = 0)exited_with_recovery
recoveringRecoveryOrderFailedfailed
recoveringMakerCancelFailedrequires_intervention
*Terminal state depends on exit reason: UserCancel → cancelled, Timeout → timed_out, TakerFailed → failed

Status Descriptions

StatusTerminalDescription
pendingNoStrategy submitted, waiting to place maker order
maker_placingNoPlacing maker order on the maker exchange
waiting_triggerNoMaker order placed, waiting for trigger conditions to be satisfied
taker_triggeredNoTrigger conditions met, placing taker order
reconcilingNoTaker executed, in reconciliation waiting period to check for fill discrepancies
safe_exitingNoIntermediate state - safely exiting strategy (handling maker fills before cancel/timeout/failure)
recoveringNoExecuting recovery order to close position discrepancy
completedYesStrategy completed successfully with matched fills
completed_with_recoveryYesStrategy completed with recovery order executed. Net position change > 0 (actual trading effect achieved)
exited_with_recoveryYesStrategy exited with recovery order but net position change = 0 (no trading effect, safe exit only)
failedYesStrategy failed (see error field for details)
cancelledYesStrategy was cancelled by user request
timed_outYesStrategy timed out waiting for trigger conditions
requires_interventionYesManual 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 ReasonDescription
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:
  1. From Reconciliation: After taker executes, if maker has additional fills that weren’t matched, a recovery order closes the excess maker position
  2. 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:
ScenarioMaker FilledTaker FilledRecoveryNet ChangeTerminal State
Perfect match1001000100completed
Partial taker100802080completed_with_recovery
Overfill taker501005050completed_with_recovery
Taker failed10001000exited_with_recovery
Cancel with fills10001000exited_with_recovery
Maker 0, Taker filled01001000exited_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:
  1. Server monitors the orderbook on the taker exchange
  2. Calculates cumulative volume from current market price to maker price
  3. 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

  1. Check Balances First: Use /strategy/balances to verify sufficient funds before submitting
  2. Set Appropriate Timeouts: Balance between waiting for conditions and avoiding stale strategies
  3. Use Buffer Volume: Add takerBufferVolume for safety margin in volatile markets
  4. Enable Reconciliation: Keep reconciliation enabled to automatically handle partial fills
  5. Monitor Status: Poll the status endpoint to track execution progress