Official Go library for the Gemini Exchange REST and WebSocket APIs.
- Zero Core Dependencies: The REST, authentication, decimal, and order-book packages use only the Go standard library. The optional Gorilla adapter is a separate module.
- Predictable Concurrency: Authenticated requests are nonce-serialized, WebSocket feeds apply backpressure, and order-book updates are atomic.
- Accurate Financial Math: Fixed-precision decimal arithmetic without floating-point errors.
- Safe Retries: Automatic backoff for idempotent requests with
Retry-Afterheader support. - Self-Healing WebSockets: Automatic reconnection with exponential backoff and feed resumption.
- Smart Quote Reconciler: Preserves queue priority and minimizes exchange round-trips.
- Secret Redaction: Credentials never leak in
fmt.Printf,%#v,slog, or JSON output.
Benchmark numbers depend on the Go version, compiler, CPU, operating system, and workload. Run the suite on the target environment instead of relying on fixed measurements in documentation:
go test -bench=. -benchmem ./...The repository includes benchmarks for authentication, transport, decimal arithmetic, WebSocket dispatch, and order-book operations.
Install the core library:
go get github.com/gemini/developer-platform/packages/sdk-goOptional: Install the Gorilla WebSocket adapter:
go get github.com/gemini/developer-platform/packages/sdk-go/websocket/gorillaThis repository is a monorepo, but packages/sdk-go is the published Go module
root. The release-smoke target stages the package as a standalone module and
compiles the documented import paths, generated packages, service facade, demo,
and optional Gorilla module.
Go modules are published from Git tags; there is no package upload step. The canonical module roots are:
packages/sdk-go→github.com/gemini/developer-platform/packages/sdk-gopackages/sdk-go/websocket/gorilla→github.com/gemini/developer-platform/packages/sdk-go/websocket/gorilla
The scripts and cmd/demo directories are repository-only modules and are
not released independently. Because these modules live below the repository
root, their Git tags must include the directory prefix required by Go's module
versioning rules. Consumers still use ordinary semantic versions:
go get github.com/gemini/developer-platform/packages/sdk-go@v0.1.0
go get github.com/gemini/developer-platform/packages/sdk-go/websocket/gorilla@v0.1.0After the release commit has been merged to main, a maintainer should create
signed, annotated tags on that commit and push only the module(s) being
released. The core module must be released before the Gorilla module when both
are being released, because the Gorilla module depends on the core module:
git fetch origin main
release_commit="$(git rev-parse origin/main)"
git tag -s packages/sdk-go/v0.1.0 \
-m "sdk-go v0.1.0" "$release_commit"
git verify-tag packages/sdk-go/v0.1.0
git push origin packages/sdk-go/v0.1.0After the core module's release workflow succeeds, release the optional Gorilla module if it changed:
git tag -s packages/sdk-go/websocket/gorilla/v0.1.0 \
-m "sdk-go/websocket/gorilla v0.1.0" "$release_commit"
git verify-tag packages/sdk-go/websocket/gorilla/v0.1.0
git push origin packages/sdk-go/websocket/gorilla/v0.1.0The Go SDK release workflow
accepts only new, signed annotated, v0/v1, and on-main tags. It runs the
complete test, race, vet, generation, standalone-consumer, and security
suites, and then verifies that the tagged module is available through
proxy.golang.org. Release tag rules in GitHub should also prevent tag
deletion or updates. Never move or reuse a published version tag; publish a
new semantic version for every release. A GitHub Release is optional and is
only for human-readable notes—the Git tag is the canonical Go release
artifact.
Before the first release, repository administrators must protect both
packages/sdk-go/v* and packages/sdk-go/websocket/gorilla/v* tag patterns:
restrict tag creation to release maintainers and disallow updates and
deletion. The workflow has read-only GitHub permissions and never creates or
moves release tags.
If a future major version requires v2 or later, the module path must first
gain the corresponding /v2 suffix and the release workflow and tag path must
be updated together, as required by Go's major-version module rules.
To verify a release from the same public path used by consumers:
GOPROXY=https://proxy.golang.org \
go list -m github.com/gemini/developer-platform/packages/sdk-go@v0.1.0
GOPROXY=https://proxy.golang.org \
go list -m github.com/gemini/developer-platform/packages/sdk-go/websocket/gorilla@v0.1.0See Go's module source management and module publishing guide for the underlying tag and proxy behavior.
Use Production or Sandbox explicitly when selecting an environment. For
applications that must reject invalid configuration during startup, use the
error-returning constructor:
client, err := gemini.NewClientWithError(
gemini.WithEnvironment(gemini.Sandbox),
)
if err != nil {
log.Fatal(err)
}
defer client.Close()Custom REST endpoints must use https; custom WebSocket endpoints must use the
wss scheme. Endpoints may include a path prefix but cannot include userinfo,
a query string, or a fragment. NewClientWithError validates both before
returning a client. The low-level HTTP transport also rejects every non-HTTPS
request, including public requests; use a TLS test server or in-memory
RoundTripper for isolated tests.
The library supports two authentication modes.
client := gemini.NewClient(
gemini.WithAPIKey("your-api-key", "your-api-secret"),
)WithAPIKey uses strictly increasing millisecond nonces for REST-only API-key
clients. If the same API key must authenticate private WebSocket traffic, it
must be a Gemini time-based key; configure it with
gemini.WithTimeBasedAPIKey, which uses epoch-second nonces on both surfaces.
For startup validation, NewClientWithError rejects blank API keys or secrets
with gemini.ErrInvalidHMACCredentials.
Static token:
client := gemini.NewClient(
gemini.WithBearerToken("oauth-access-token"),
)Dynamic token refresh with a TokenSource:
tokenSource := auth.TokenFunc(func(ctx context.Context) (string, error) {
return tokenManager.GetValidToken(ctx)
})
client := gemini.NewClient(
gemini.WithTokenSource(tokenSource),
)The token source is application-owned. It must be safe for concurrent calls,
honor the request context, and return a current non-expired access token. The
SDK calls it for each authenticated HTTP attempt and each WebSocket connection
or reconnect; it does not force-refresh after a 401 response. The optional
github.com/gemini/developer-platform/packages/sdk-go/oauth package provides PKCE authorization-code
and refresh-token helpers without making interactive login part of the core
client.
The OAuth package keeps token persistence application-owned. Its
Config.Login convenience uses a fixed loopback callback such as
http://localhost:8787/callback; that is the only HTTP URL permitted by the
package. Authorization and token endpoints must use HTTPS. Applications that
already have their own browser flow can use Config.AuthCodeURL and
Config.Exchange directly, then pass the result to oauth.NewTokenSource:
oauthConfig := oauth.Config{
ClientID: os.Getenv("GEMINI_OAUTH_CLIENT_ID"),
Endpoint: oauth.Endpoint{
AuthURL: "https://exchange.gemini.com/auth",
TokenURL: "https://exchange.gemini.com/auth/token",
},
RedirectURL: "http://localhost:8787/callback",
Scopes: []string{"account:read", "orders:create"},
}
token, err := oauthConfig.Login(ctx, openBrowser)
if err != nil {
log.Fatal(err)
}
source, err := oauth.NewTokenSource(
oauthConfig,
*token,
oauth.WithTokenUpdate(func(ctx context.Context, updated oauth.Token) error {
return secureTokenStore.Save(ctx, updated)
}),
)
if err != nil {
log.Fatal(err)
}
client := gemini.NewClient(gemini.WithTokenSource(source))oauth.Source refreshes once for concurrent callers, honors cancellation
while waiting for another refresh, and preserves a refresh token when the
provider omits it from a rotation response. It does not write credentials to
disk or a keychain. Applications that need durable refresh-token rotation can
pass oauth.WithTokenUpdate; the callback receives the complete refreshed
token before it becomes visible to callers. If persistence fails, the refresh
fails and the previous in-memory token remains in place.
If a token source fails during an automatic WebSocket reconnect, the SDK stops
that reconnect loop, reports the underlying error through connection events,
and leaves the client disconnected so the source can be repaired before a
caller explicitly reconnects.
For applications that need startup validation, NewClientWithError rejects a
nil or empty bearer configuration with gemini.ErrInvalidTokenSource.
For private REST calls, the SDK encodes the request path and endpoint
parameters in Gemini's X-GEMINI-PAYLOAD header and sends no request body, as
required by Gemini OAuth. Callers only provide the generated request model.
Private REST methods fail locally with gemini.ErrAuthenticationRequired when
the client has no authentication strategy; no unauthenticated private request
is sent to the network.
To revoke the active OAuth token, configure bearer authentication and call
client.Account.RevokeOAuthToken(ctx). The endpoint revokes the token used by
that request; it is not available through API-key authentication.
OAuth tokens can also authenticate private WebSocket streams and RFQ quote methods. Gemini enforces the token's account capabilities server-side, so the token must be authorized for the requested feed or operation.
An opt-in integration test validates one bearer REST request and one private WebSocket handshake against the selected Gemini environment. It is skipped when no token is supplied and never runs as part of the default test target:
cd websocket/gorilla && \
GEMINI_OAUTH_ACCESS_TOKEN="..." \
GEMINI_OAUTH_ENVIRONMENT=sandbox \
go test -tags=integration ./...The live demo uses the Markets CLI's public OAuth client ID by default, opens the production consent page, and keeps the resulting tokens in memory only:
cd cmd/demo
GEMINI_DEMO_OAUTH_LOGIN=1 go run .To arm one live RFQ quote submission, also provide an explicit confirmation and the quote parameters. The demo submits at most one quote, and only for an open RFQ observed during its bounded window:
GEMINI_DEMO_OAUTH_LOGIN=1 \
GEMINI_DEMO_RFQ_SUBMIT=1 \
GEMINI_DEMO_RFQ_CONFIRM=I_UNDERSTAND_THIS_SUBMITS_A_LIVE_RFQ_QUOTE \
GEMINI_DEMO_RFQ_PRICE="0.55" \
GEMINI_DEMO_RFQ_QUANTITY="100" \
go run .Use GEMINI_OAUTH_CLIENT_ID to override the default client ID and
GEMINI_OAUTH_CLIENT_SECRET only when the OAuth application requires one.
The loopback callback is HTTP on localhost by OAuth convention; all Gemini
authorization and token endpoints remain HTTPS.
Verify incoming Gemini HMAC-SHA384 webhook signatures in constant time:
isValid := gemini.VerifySignature(secret, b64Payload, signatureHeader)
if !isValid {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}package main
import (
"context"
"fmt"
"log"
"github.com/gemini/developer-platform/packages/sdk-go"
)
func main() {
client := gemini.NewClient()
ctx := context.Background()
ticker, err := client.MarketData.GetTicker(ctx, "BTCUSD")
if err != nil {
log.Fatalf("failed to fetch ticker: %v", err)
}
fmt.Printf("BTC/USD Bid: %s, Ask: %s, Last: %s\n", gemini.Val(ticker.Bid), gemini.Val(ticker.Ask), gemini.Val(ticker.Last))
}Place maker post-only or limit orders with exact decimals:
amount := gemini.MustDecimal("0.05")
price := gemini.MustDecimal("65000.00")
// Guaranteed Maker (Post-Only)
order, err := client.Trading.PostOnlyBid(ctx, "BTCUSD", amount, price)
// Standard Limit Buy
order, err := client.Trading.LimitBuy(ctx, "BTCUSD", amount, price)
// Immediate-or-Cancel Sell
order, err := client.Trading.ImmediateOrCancelSell(ctx, "BTCUSD", amount, price)// Import the generated request models used by typed service methods:
// "github.com/gemini/developer-platform/packages/sdk-go/generated/account"
// 1. Account balances and subaccounts
balances, err := client.Account.GetBalances(ctx, &account.GetAvailableBalancesJSONBody{Account: "primary"})
accounts, err := client.Account.ListAccounts(ctx, nil)
// 2. Staking lifecycle (provider ID is required by the API)
stkBalances, err := client.Staking.GetStakingBalances(ctx, nil)
stakeTx, err := client.Staking.Stake(ctx, &account.StakeCryptoFundsJSONBody{ProviderId: "provider-id", Currency: "ETH", Amount: "1.5"})
unstakeRes, err := client.Staking.Unstake(ctx, &account.UnstakeCryptoFundsJSONBody{ProviderId: "provider-id", Currency: "ETH", Amount: "0.5"})
// 3. Multichain transfers and fee estimates
feeEst, err := client.Transfers.GetWithdrawalFeeEstimateV2(ctx, "solana", "sol", "address", "10.0")
withdrawRes, err := client.Transfers.WithdrawCryptoV2(ctx, "solana", "sol", "address", "10.0")
// Pass a generated *account.ListPastTransfersJSONBody for typed filters;
// nil requests the endpoint defaults.
pastTransfers, err := client.Transfers.GetTransfers(ctx, nil)Synchronize a market-making ladder. The reconciler diffs your desired orders against open orders, keeps matching orders in the exchange queue, and sends only required cancels and new orders:
// Create reconciler with a 0.5 bps tolerance band
reconciler := client.NewQuoteReconciler("BTCUSD",
gemini.WithToleranceBps(0.5),
gemini.WithQuantization(gemini.MustDecimal("0.01"), gemini.MustDecimal("0.0001")),
)
// StartStreaming subscribes before the initial REST hydration and replays
// order events received during that handoff.
errChan, err := reconciler.StartStreaming(ctx)
if err != nil {
log.Fatalf("stream error: %v", err)
}
// Define target quotes and sync
mid := gemini.MustDecimal("65000")
size := gemini.MustDecimal("0.05")
desired := []gemini.DesiredQuote{
{Side: "buy", Price: mid.SubBps(5.0), Amount: size},
{Side: "sell", Price: mid.AddBps(5.0), Amount: size},
}
result, err := reconciler.Sync(ctx, desired)
if err != nil {
log.Fatalf("reconciliation could not start: %v", err)
}
if err := result.Err(); err != nil {
log.Printf("reconciliation completed with partial failures: %v", err)
}
log.Printf("Sync: Kept=%d, Cancelled=%d, Placed=%d",
result.Kept, result.Cancelled, result.Placed)Each reconciler supports one active stream; cancel its context before starting another stream. Reconciler cleanup removes only its own WebSocket order-event subscription, so other subscribers on the same private WebSocket remain active.
The generated packages are regenerated from Gemini's deployed REST contracts
and contain request and response models for every documented REST operation.
The hand-written services facade intentionally exposes a smaller, curated
set of core trading, market-data, account, transfer, staking, margin,
perpetuals, clearing, and prediction-market operations. The supported subset
is tracked by an operation-to-method coverage test in scripts/; a spec change
cannot silently add an unclassified endpoint.
For operations not yet surfaced by a high-level service, use the generated
models with transport.Client.Request or wait for a typed service method. The
prediction-market facade includes typed batch order/cancel, order history,
positions, settled positions, combos, volume metrics, maker-rebate, and
liquidity-rewards operations. Native Go iterators are available for the
paginated event, order, position, combo, and liquidity-rewards collections.
Some less common REST and clearing/reporting operations remain available through
the generated models and transport client.
Prediction-market event responses preserve the complete sports metadata model:
Event.SportsMarket includes sport, market type, subject, scope, and metric,
and PredictionsService.GetEvents accepts all corresponding repeated filters.
For higher-level sports discovery, services.ClusterSportsEvents groups raw
events by contest root and services.ResolveSportsContest resolves a contest
from an already-fetched event set without adding network behavior to the REST
client.
The SDK keeps public and private WebSocket traffic on separate client instances and separate connections:
client.PublicWebSocket()is unauthenticated and is for public market data such as depth, trades, book ticker, and contract status.client.PrivateWebSocket()is the authenticated connection for order, balance, position, and settlement feeds. ConfigureWithAPIKey,WithTimeBasedAPIKey,WithBearerToken, or another auth option first.WithAPIKeyuses strict-increasing REST nonces and is intended for REST-only API-key clients; useWithTimeBasedAPIKeywhen the same key must authenticate both REST and private WebSocket traffic.
client := gemini.NewClient(
gemini.WithEnvironment(gemini.Sandbox),
gemini.WithTimeBasedAPIKey("your-api-key", "your-api-secret"),
)
defer client.Close()
publicWS := client.PublicWebSocket()
depth, err := publicWS.SubscribeDepth(ctx, "BTCUSD")
if err != nil {
log.Fatal(err)
}
privateWS := client.PrivateWebSocket()
orders, err := privateWS.SubscribeOrderEvents(ctx)
if err != nil {
log.Fatal(err)
}
// UnsubscribeOrderEvents removes every order-event subscriber. To remove only
// this subscriber, use UnsubscribeOrderEventsChannel(ctx, orders).
go func() {
for update := range depth {
log.Printf("public depth update: %d", update.LastUpdateID)
}
}()
go func() {
for update := range orders {
log.Printf("private order %d: %s", update.OrderID, update.OrderStatus)
}
}()Typed stream options mirror the supported AsyncAPI variants. Use
SubscribeDepthWithOptions for the 100ms differential stream,
SubscribePartialDepth with DepthLevel5, DepthLevel10, or DepthLevel20
for top-of-book snapshots, SubscribeOrderEventsWithScope for account versus
session orders, and SubscribeBalancesWithOptions or
SubscribePositionsWithOptions with Interval: time.Second for throttled
snapshots. Settlements currently have only the documented account stream;
the SDK does not invent a session variant that is absent from the spec.
The WebSocket control plane and authenticated order methods are typed as well:
use ConnInfo, Time, ListSubscriptions, SubscribeStreams, and
UnsubscribeStreams for protocol control requests, and PlaceOrder,
CancelOrder, CancelAllOrders, and CancelSessionOrders for trading
requests. Raw SubscribeStreams calls are direct protocol operations and are
not replayed automatically after reconnect; private stream names fail closed
on a public client. Use RequestAuthenticated for dynamically named private
methods, and use the typed feed subscription methods when feed resumption is
required. PlaceOrder accepts LIMIT and MARKET. A stop-limit order uses
Type: "LIMIT" with both Price and StopPrice; Gemini reports the resulting
order as STOP_LIMIT on the order-event stream. stopPrice is not valid with
MARKET. The WebSocket contract allows stopPrice == Price; the legacy REST
TradingService.NewOrder contract requires a strict inequality (stopPrice < Price for buys and stopPrice > Price for sells). Account-wide cancellation requires
CancelAllOptions{Confirm: true} over WebSocket or
CancelAllOrdersOptions{Confirm: true} through TradingService so a
destructive request cannot be issued by omitting a parameter accidentally.
Partial-depth subscriptions use one underlying connection per symbol because
their snapshot envelope may not include a symbol. Differential depth
subscriptions remain multiplexed because their snapshots include the market
symbol. The root SDK configures this behavior automatically. Low-level clients
that need the same behavior can use
websocket.WithIsolatedPartialSnapshots(); the older
websocket.WithIsolatedSnapshots() remains available when both feed types
must be isolated.
An unauthenticated client still exposes PrivateWebSocket() so applications
can construct clients uniformly, but private subscriptions fail immediately
with gemini.ErrAuthenticationRequired; no unauthenticated connection is
silently upgraded or reused. The low-level websocket.NewPublicClient and
websocket.NewPrivateClient constructors provide the same separation when the
root gemini.Client facade is not used.
The Go SDK exposes the public requestForQuote discovery stream, authenticated
requestForQuote@account/@session delivery streams, and typed
SubmitRFQQuote, WithdrawRFQQuote, and ConfirmRFQQuote methods. RFQ
deliveries are at-least-once; deduplicate them by DeliveryID before applying
lifecycle transitions. Quote methods require an authenticated WebSocket
client and preserve the API's explicit Confirm boolean—no action is taken on
the caller's behalf. Each RFQLeg includes its contract ID and outcome, plus
the optional leg-specific InstrumentSymbol; this is distinct from the
combo-level RFQPublicEvent.Symbol.
rfqs, err := client.PublicWebSocket().SubscribeRFQEvents(ctx)
if err != nil {
return err
}
for rfq := range rfqs {
if rfq.State != websocket.RFQStateOpen {
continue
}
// Insert application-specific pricing here. The SDK does not choose a
// price or submit a quote automatically.
quote, err := client.PrivateWebSocket().SubmitRFQQuote(ctx, websocket.RFQSubmitQuoteParams{
RFQID: rfq.RFQID, Price: "0.55", Quantity: "100",
})
if err != nil {
return err
}
_ = quote.QuoteID
}The public and private WebSocket clients are intentionally separate. Use the private client for RFQ quote methods and authenticated delivery streams; it must be created with the same account credentials used for the relevant capabilities.
Inbound WebSocket messages are limited to 1 MiB by default. Configure a
different limit with websocket.WithMaxMessageSize, or pass a non-positive
value only when the transport is trusted and an unbounded payload is required.
Malformed JSON frames are reported through ConnectionEvent.Err as
websocket.ErrMalformedFrame; the connection remains alive so callers can
continue receiving valid frames. For unattended processes, opt into
application-level liveness checks with
websocket.WithLiveness(interval, timeout). A failed check is reported as
websocket.ErrLivenessFailed and follows the normal reconnect policy.
This example uses the optional github.com/gemini/developer-platform/packages/sdk-go/websocket/orderbook
package. Always drain subscription channels; the client applies backpressure
to preserve every update. Feed channels are bounded. If a consumer falls
behind far enough to fill the client's inbound dispatch queue, the client
reports websocket.ErrSlowConsumer through its lifecycle event channel and
reconnects when automatic reconnect is enabled. Treat that event as a data
recovery boundary: rebuild order-book state from a snapshot and reconcile
private order state with REST before continuing.
liveBook := orderbook.NewLiveOrderBook("BTCUSD")
// Book returns a read-only view. Apply snapshots and diffs through liveBook
// so sequence and recovery state remains synchronized.
bookView := liveBook.Book()
_ = bookView.LastUpdateID()
// Callback triggers only when the Top-of-Book changes
liveBook.OnBBOChanged(func(bbo orderbook.BBO) {
fmt.Printf("Bid: %s | Ask: %s | Mid: %.2f | Spread: %.2f bps\n",
bbo.BestBid, bbo.BestAsk, bbo.Mid, bbo.SpreadBps)
})
depthStream, err := client.PublicWebSocket().SubscribeDepth(ctx, "BTCUSD")
if err != nil {
log.Fatalf("subscribe error: %v", err)
}
for diff := range depthStream {
if err := liveBook.IngestDiff(diff); err != nil {
log.Printf("Sequence gap detected, resyncing: %v", err)
break
}
}The top-level SDK requests a full order-book snapshot when it connects the
public WebSocket. The first snapshot frame is marked internally and can be
passed directly to IngestDiff; subsequent frames are differential updates.
The live book intentionally does not infer snapshot state from U == u, since
that is also valid for a normal differential update. If you construct a
low-level WebSocket client directly, opt into the same behavior with
websocket.WithSnapshot(-1).
Prediction-market orders are sent to Gemini immediately. If the backend
returns gemini.ErrAcceptTermsRequired, explicitly call
client.Predictions.AcceptTerms(ctx) and retry the order. The SDK never checks
terms in advance or accepts them on the caller's behalf.
- Private REST requests using HMAC authentication are serialized per client so retries cannot send a lower nonce after a later request.
- Call
client.Close()when the SDK client is no longer needed to stop WebSocket pumps and release idle connections from the SDK-owned HTTP transport. A caller-providedWithHTTPClientremains caller-owned. - WebSocket
RequestandPingcalls wait for their correlated server response; subscription methods return protocol errors instead of treating a write as success. Useerrors.Is(err, websocket.ErrRequestFailed)to classify a rejected request. - WebSocket lifecycle events are buffered and coalesced if the consumer falls
behind; use
State()as the authoritative current state. - WebSocket subscription channels are flow-controlled. If a consumer fills the
bounded inbound queue, the client closes feed channels and emits
websocket.ErrSlowConsumer; applications must resync state before subscribing again. - Order-book snapshots and diffs reject malformed or negative levels without
partially mutating the book. Sequence gaps return
gemini.ErrResyncRequired. LiveOrderBook.Resetclears both sequence state and price levels. Call it before applying a fresh snapshot after a disconnect or sequence gap.
From this directory, the package checks are:
gofmt -l .
go test ./...
go test -race ./...
go vet ./...The scripts module fetches the allowlisted deployed OpenAPI/AsyncAPI contracts,
verifies their SHA-256 hashes, and contains contract-drift tests:
(cd scripts && go test ./...)Keep an active trading session alive in the background:
session := client.Heartbeat.Start(ctx, 5*time.Second)
defer session.Stop()
go func() {
for err := range session.Errors() {
log.Printf("Heartbeat error: %v", err)
}
}()types.Decimal preserves the quoted-string representation used by string
decimal fields. Generated models for OpenAPI numeric decimal fields use
types.DecimalNumber, which accepts either quoted or numeric input but emits
an exact JSON number without converting through float64.
price := gemini.MustDecimal("65000.00")
// Add and subtract basis points
ask := price.AddBps(10.0) // 65065.00
bid := price.SubBps(10.0) // 64935.00
// Measure difference in basis points
diffBps := ask.BpsDiff(bid) // 20.0 bps
// Quantize to market tick and lot rules
tickSize := gemini.MustDecimal("0.50")
lotSize := gemini.MustDecimal("0.001")
quantizedPrice := gemini.MustDecimal("65000.37").QuantizePrice(tickSize) // 65000.00
quantizedQty := gemini.MustDecimal("0.1237").QuantizeAmount(lotSize) // 0.123All errors returned by the SDK seamlessly unwrap to typed sentinels. You can inspect errors using standard errors.Is() or the top-level helper functions in a single flat switch:
order, err := client.Trading.PostOnlyBid(ctx, "BTCUSD", amount, price)
if err != nil {
// Optional: Extract request ID for Gemini Support logs
if reqID := gemini.RequestIDFromError(err); reqID != "" {
log.Printf("Gemini Request ID: %s", reqID)
}
switch {
case gemini.IsInsufficientFunds(err):
log.Println("Domain: Insufficient balance to place order")
case gemini.IsMarketClosed(err):
log.Println("Domain: Market or trading pair is halted")
case gemini.IsRateLimit(err):
log.Println("API: Rate limit exceeded (automatic backoff engaged)")
case gemini.IsSelfCrossPrevented(err):
log.Println("Domain: Self-trade prevention triggered")
case gemini.IsAuthError(err):
log.Fatalf("Auth: Invalid keys, signature, or nonce")
case gemini.IsNotFound(err):
log.Println("API: Order or symbol not found")
case gemini.IsResyncRequired(err):
log.Println("Stream: Sequence gap detected; resyncing book...")
default:
log.Printf("Unhandled error: %v", err)
}
}switch {
case gemini.IsDomainError(err):
// Exchange matching engine business rejections (do not retry)
log.Printf("Business logic rejection: %v", err)
case gemini.IsAPIError(err):
// Gateway / HTTP 4xx / 5xx responses
if apiErr, ok := gemini.AsAPIError(err); ok {
log.Printf("HTTP %d (%s): %s", apiErr.StatusCode, apiErr.Reason, apiErr.Message)
}
case gemini.IsTimeout(err):
// Client-side deadline exceeded
log.Println("Request timed out")
}Use the local mock server for testing without network requests:
server := geminitest.NewMockServer("test-key", "test-secret")
defer server.Close()
client := gemini.NewClient(
gemini.WithCustomRESTURL(server.URL()),
gemini.WithAPIKey("test-key", "test-secret"),
)Apache 2.0. See the Apache 2.0 license for details.