-
Notifications
You must be signed in to change notification settings - Fork 0
Recipes
Practical, copyable patterns for the cryptohopper gem. Every snippet runs as-is — paste into a .rb file and execute with ruby file.rb (after gem install cryptohopper --pre). They use only the public SDK surface, never internals.
Hash responses use string keys (raw JSON.parse output), so access is hopper["id"], not hopper[:id].
- Use the client and clean up cleanly
- Wait for a backtest to finish
- Find every open position across all your hoppers
- Detect new fills since the last poll
- Pattern-match on Cryptohopper::Error codes
- Fail fast on auth errors, retry on transient ones
- Read your remaining backtest quota
- Run multiple SDK calls in parallel with Concurrent::Promises
- Tighten timeouts for short-lived jobs (Sidekiq, cron)
- Disable the SDK's built-in retry and handle 429 yourself
- Stub the SDK in tests with WebMock
The Ruby SDK uses Ruby's stdlib Net::HTTP — there's no long-lived connection pool to close. Just instantiate the client and use it.
require "cryptohopper"
ch = Cryptohopper::Client.new(api_key: ENV.fetch("CRYPTOHOPPER_TOKEN"))
me = ch.user.get
puts me["email"]Within a single Sidekiq job or short-lived script, there's nothing extra to manage.
Backtests run async on the server. create returns immediately with an ID; you poll get until status is terminal.
def run_backtest(ch, hopper_id:, from_date:, to_date:)
bt = ch.backtest.create(
hopper_id: hopper_id,
start_date: from_date,
end_date: to_date,
)
loop do
cur = ch.backtest.get(bt["id"])
return cur if %w[completed failed].include?(cur["status"])
sleep 5
end
endThe backtest rate bucket is separate (1 request per 2 seconds). 5-second polling stays well clear.
ch.hoppers.list.each do |h|
ch.hoppers.positions(h["id"]).each do |p|
puts "#{h['name']} (##{h['id']}): #{p['amount']} #{p['coin']} @ #{p['rate']}"
end
endThis is sequential — one request per hopper. With 50+ hoppers, parallelise with Concurrent::Promises (recipe below).
seen = Set.new
loop do
ch.hoppers.orders(hopper_id).each do |o|
next if seen.include?(o["id"])
next unless o["status"] == "filled"
seen << o["id"]
puts "Fill: #{o['market']} #{o['type']} #{o['amount']} @ #{o['price']}"
end
sleep 10
endFor production-grade fill notifications, configure the webhooks resource — push beats poll for event delivery.
Ruby 3.0+ pattern matching makes the typed-error surface very pleasant.
require "cryptohopper"
begin
ch.hoppers.get("999999999")
rescue Cryptohopper::Error => e
case e.code
in "NOT_FOUND"
puts "no such hopper"
in "UNAUTHORIZED" | "FORBIDDEN"
puts "auth problem; check token / scopes / IP whitelist"
puts " IP we sent: #{e.ip_address}" if e.ip_address
in "RATE_LIMITED"
puts "rate limited; retry after #{e.retry_after_ms}ms"
else
raise
end
ende.code is a stable string — compare with ==, never substring-match.
The SDK auto-retries 429s. For 5xx and network errors you may want a tighter retry. Auth errors should never be retried.
def with_retry(max_attempts: 3)
attempts = 0
begin
attempts += 1
yield
rescue Cryptohopper::Error => e
raise if %w[UNAUTHORIZED FORBIDDEN NOT_FOUND VALIDATION_ERROR].include?(e.code)
raise if attempts >= max_attempts
sleep(0.5 * (2 ** (attempts - 1)))
retry
end
end
me = with_retry { ch.user.get }retry re-runs the whole begin block — exactly what you want for idempotent SDK calls.
limits = ch.backtest.limits
puts "Backtests remaining: #{limits['remaining']} of #{limits['limit']}"For the normal and order buckets there's no explicit quota endpoint — the only signal is Retry-After on a 429 (read it via error.retry_after_ms).
Add concurrent-ruby to your Gemfile, then:
require "concurrent-ruby"
# A single client is reentrant — share it across threads.
client = Cryptohopper::Client.new(api_key: ENV.fetch("CRYPTOHOPPER_TOKEN"))
hoppers = client.hoppers.list
# Issue every positions call in parallel, capped at 10 in flight.
pool = Concurrent::FixedThreadPool.new(10)
futures = hoppers.map do |h|
Concurrent::Promises.future_on(pool) { [h, client.hoppers.positions(h["id"])] }
end
futures.each do |f|
hopper, positions = f.value!
puts "#{hopper['name']}: #{positions.size} positions"
end
pool.shutdown
pool.wait_for_terminationEach in-flight call counts against the normal bucket (30 req/min). With many concurrent calls, expect 429s — the SDK retries transparently.
Default timeout is 30 seconds. Inside a Sidekiq job with a 5-minute wall-clock budget the default is fine. Inside an AWS Lambda (15s) or Heroku request boundary, drop it.
ch = Cryptohopper::Client.new(
api_key: ENV.fetch("CRYPTOHOPPER_TOKEN"),
timeout: 8, # ~half your job's budget
max_retries: 1, # leave headroom for one retry within the lifetime
)A Cryptohopper::Error with code == "TIMEOUT" is much easier to handle than a process kill.
ch = Cryptohopper::Client.new(
api_key: ENV.fetch("CRYPTOHOPPER_TOKEN"),
max_retries: 0,
)
begin
ch.hoppers.list
rescue Cryptohopper::Error => e
if e.code == "RATE_LIMITED"
puts "rate limited; server says wait #{e.retry_after_ms}ms"
# your custom queue / circuit breaker / etc.
else
raise
end
endUseful when you're inside something that already does retries (Sidekiq's exponential backoff, ActiveJob retry_on, etc.) and don't want two layers of retry.
The SDK is built on Net::HTTP, so any stdlib-aware stubber works. The test suite uses WebMock.
require "webmock/rspec"
RSpec.describe "user.get" do
let(:client) { Cryptohopper::Client.new(api_key: "test") }
it "returns the parsed user payload" do
stub_request(:get, "https://api.cryptohopper.com/v1/user/get")
.with(headers: { "Authorization" => "Bearer test" })
.to_return(
status: 200,
body: { data: { id: 42, email: "alice@example.com" } }.to_json,
headers: { "Content-Type" => "application/json" },
)
me = client.user.get
expect(me["id"]).to eq(42)
end
it "retries on 429 and respects Retry-After" do
stub_request(:get, %r{/v1/user/get}).to_return(
{ status: 429, headers: { "Retry-After" => "0" } },
{ status: 200, body: { data: { id: 42 } }.to_json },
)
expect(client.user.get["id"]).to eq(42)
end
endThe SDK pulls data out of the envelope automatically — your stub returns {"data": ...}, your assertion sees the inner value.
Pages
Other SDKs
Resources