hpyhacking/peatio · error · APIv2::CreateOrderError
2002
2002
Error message
Failed to create order. Reason: #{e} What it means
Error code 2002 (CreateOrderError) is raised by APIv2::Helpers#create_order (POST /api/v2/orders). build_order constructs an OrderBid/OrderAsk (market taken from params[:market], ord_type defaulting to 'limit') and Ordering#submit executes it; a bare rescue catches EVERY exception and re-raises it wrapped as 'Failed to create order. Reason: <original exception>'. The real cause lives only in the Reason text and the server's 'Failed to create order:' log line.
Source
Thrown at app/api/api_v2/helpers.rb:53
ask: current_market.base_unit,
bid: current_market.quote_unit,
currency: current_market.id,
ord_type: attrs[:ord_type] || 'limit',
price: attrs[:price],
volume: attrs[:volume],
origin_volume: attrs[:volume]
)
end
def create_order(attrs)
order = build_order attrs
Ordering.new(order).submit
order
rescue
Rails.logger.info "Failed to create order: #{$!}"
Rails.logger.debug order.inspect
Rails.logger.debug $!.backtrace.join("\n")
raise CreateOrderError, $!
end
def create_orders(multi_attrs)
orders = multi_attrs.map {|attrs| build_order attrs }
Ordering.new(orders).submit
orders
rescue
Rails.logger.info "Failed to create order: #{$!}"
Rails.logger.debug $!.backtrace.join("\n")
raise CreateOrderError, $!
end
def order_param
params[:order_by].downcase == 'asc' ? 'id asc' : 'id desc'
end
def format_ticker(ticker)
{ at: ticker[:at],View on GitHub (pinned to dab8641137)
Solutions
- Read the Reason in the 2002 message plus the matching 'Failed to create order:' server log line — it names the actual exception; fix that root cause first
- Pre-flight the order: GET /api/v2/markets to validate the market id and its precision, then check available (not total) balance covers price*volume
- Normalize price and volume to plain decimal strings at market precision and require both to be positive before submitting
Example fix
# before — fire and hope
post '/api/v2/orders', params: auth_params.merge(market: 'btcusd', side: 'buy', volume: 1, price: 4000)
# after — validate then submit, decode the wrapped reason
market = markets.find { |m| m['id'] == 'btcusd' } or raise 'unknown market'
raise 'insufficient funds' unless available_quote >= 4000 * 1
begin
post '/api/v2/orders', params: signed(market: 'btcusd', side: 'buy', volume: '1.0', price: '4000.0')
rescue CreateOrderError => e
reason = e.message.gsub('Failed to create order. Reason: ', '')
retry_after_top_up if reason.include?('balance')
end Defensive patterns
Strategy: try-catch
Validate before calling
# Pre-flight checks mirroring build_order + Ordering requirements
def order_submittable?(attrs, markets, accounts)
m = markets.find { |x| x['id'] == attrs[:market] } or return false
return false unless %w[buy sell].include?(attrs[:side])
price = attrs[:price].to_d; volume = attrs[:volume].to_d
return false unless price.positive? && volume.positive?
cost = attrs[:side] == 'buy' ? price * volume : volume
funds = attrs[:side] == 'buy' ? accounts[m['quote']] : accounts[m['base']]
funds && funds >= cost
end Try / catch
rescue the 2002 body, strip the 'Failed to create order. Reason: ' prefix, and branch on the underlying reason: balance errors trigger a top-up/queue path, validation errors mark the order template invalid, infrastructure reasons go to a bounded retry with fresh tonce and re-signature.
Prevention
- Check available (unlocked) balance, not total balance, before submitting
- Fetch /api/v2/markets at startup and validate id, precision, and min volume against it
- Send numbers as plain decimal strings to avoid float/locale surprises
- Never assume 2002 means one thing — always decode the wrapped Reason
When it happens
Trigger: Insufficient or locked balance for price*volume on a bid (volume on an ask); an unknown :market id (Market.find fails before the order is built); nil, zero, negative, or wrong-precision price/volume; ordering-service, AMQP, or database failures — all of these surface identically as 2002.
Common situations: Open orders already locked the funds so available balance is lower than total; market string mismatch such as 'btcusd' vs 'btcusdt'; price sent as a localized string ('1.234,5'); precision/lot-size rules changed after the client was written; a NoMethodError from nil price masked by the blanket rescue.
Related errors
AI-assisted analysis of hpyhacking/peatio@dab8641137 (2026-08-23).
Data as JSON: /api/errors/7d40a3124d1ee457.
Report an issue: GitHub.