Typeclass Architecture
The core abstraction in Binance4s is the BinanceEndpoint[Req, Resp] typeclass. It statically links a request type to its response type, HTTP method, path, security model, and query parameters.
The Typeclass
trait BinanceEndpoint[Req, Resp]:
def method: HttpMethod // GET, POST, PUT, DELETE
def prefix: ApiPrefix // Api (/api) or Sapi (/sapi)
def version: ApiVersion // V1, V2, V3
def path: Vector[String] // e.g. Vector("ticker", "price")
def security: SecurityType // None, UserStream, MarketData, Trade, UserData
def queryParams(req: Req): QueryString // builds the query string from the request
def decoder: Decoder[Resp] // Circe decoder for the response
def weight: Int = 1 // API weight for rate limiting
Defining an Endpoint
Each Binance API endpoint is defined as a given instance:
case class AvgPriceReq(symbol: String)
given BinanceEndpoint[AvgPriceReq, AvgPrice] with
def method = HttpMethod.GET
def prefix = ApiPrefix.Api
def version = ApiVersion.V3
def path = Vector("avgPrice")
def security = SecurityType.None
def queryParams(req: AvgPriceReq) =
QueryString.empty.add("symbol", req.symbol)
def decoder = Decoder[AvgPrice]
This generates GET /api/v3/avgPrice?symbol=BTCUSDT and decodes the response as AvgPrice.
How It Works
When you call client.execute(AvgPriceReq("BTCUSDT")), the compiler:
- Resolves the
given BinanceEndpoint[AvgPriceReq, AvgPrice]instance at compile time - Infers the return type as
F[AvgPrice] - At runtime, the client:
- Calls
queryParams(req)to build the query string - Adds timestamp + HMAC signature if
securityrequires it - Constructs the full URI from
prefix,version,path, and query - Executes the HTTP request with appropriate headers
- Decodes the JSON response using
decoder
- Calls
Two API Styles
// Explicit — you see the request type and the compiler resolves the response
val avgPrice: F[AvgPrice] = client.execute(AvgPriceReq("BTCUSDT"))
// Syntax extension — ergonomic shorthand
val avgPrice: F[AvgPrice] = client.avgPrice("BTCUSDT")
Both compile to the same code. The syntax extensions are defined as:
extension [F[_]](client: BinanceClient[F])
def avgPrice(symbol: String): F[AvgPrice] =
client.execute(AvgPriceReq(symbol))
Extensibility
Adding a new endpoint requires only a case class for the request and a given instance. No client modifications needed:
// Your custom request type
case class MyCustomReq(symbol: String, interval: String)
// Your custom endpoint definition
given BinanceEndpoint[MyCustomReq, io.circe.Json] with
def method = HttpMethod.GET
def prefix = ApiPrefix.Api
def version = ApiVersion.V3
def path = Vector("myCustomEndpoint")
def security = SecurityType.None
def queryParams(req: MyCustomReq) =
QueryString.empty.add("symbol", req.symbol).add("interval", req.interval)
def decoder = Decoder[io.circe.Json]
// Use it
client.execute(MyCustomReq("BTCUSDT", "1h"))
This is the key advantage of the typeclass approach: the client is open for extension without modification.