Available Hire Me
← All Writing Betfair

Working with Runner Metadata, SP, and BSP in the Betfair API

How to extract runner metadata, interpret Starting Price fields, and use BSP data from the Betfair API in Java — with practical examples for pre-race and post-race analysis.

Every Betfair market has two layers of runner data: the catalogue layer that identifies who is running, and the market book layer that carries live prices, volumes, and the Starting Price fields. Conflating the two — or not knowing where BSP data actually lives — is one of the most common sources of confusion when building against the Betfair API for the first time.

This post covers both layers, explains the difference between SP and BSP, and shows how to work with all of it in Java.

Two Objects, One Runner

The Betfair API models each runner across two separate objects that you join by selection ID.

RunnerCatalog comes back from listMarketCatalogue when you request the RUNNERS market projection. It carries the stable, pre-race metadata:

public class RunnerCatalog {
    private long selectionId;     // stable ID — same runner across all markets
    private String runnerName;
    private double handicap;
    private String sortPriority;  // cloth number / stall draw
    private Map<String, String> metadata; // COLOURS_DESCRIPTION, TRAINER_NAME, etc.
}

Runner comes back from listMarketBook. It carries the live state: best available prices, matched volume, and SP fields. The selectionId on both objects is the join key.

RunnerCatalog catalogue = catalogueRunners.stream()
    .filter(r -> r.getSelectionId() == runner.getSelectionId())
    .findFirst()
    .orElseThrow();

String name = catalogue.getRunnerName();
double bsp  = runner.getSp().getActualSP();

The RunnerCatalog Metadata Map

The metadata field on RunnerCatalog is underused. It can contain:

Key Value
COLOURS_DESCRIPTION Jockey silks description
COLOURS_FILENAME Silks image filename
TRAINER_NAME Trainer name
JOCKEY_NAME Jockey name
AGE Age of the horse
FORM Recent form string
OFFICIAL_RATING Official Racing Post rating
WEIGHT_VALUE / WEIGHT_UNITS Carried weight
RUNNER_ID Racing Post runner ID
Map<String, String> meta = runner.getMetadata();
String trainer = meta.getOrDefault("TRAINER_NAME", "Unknown");
String form    = meta.getOrDefault("FORM", "-");
String rating  = meta.getOrDefault("OFFICIAL_RATING", "N/A");

Not every runner has every field — always use getOrDefault. Fields are absent rather than null when not populated, so a missing key is normal rather than an error.

SP vs BSP

These are different things and the terminology is inconsistent across the industry.

SP (traditional Starting Price) is the price returned by the racing press (Racing Post, Timeform) based on bookmaker prices at the off. It is calculated off-exchange, arrives as historical data, and is not available through the Betfair API directly.

BSP (Betfair Starting Price) is Betfair’s own mechanism. It matches unmatched BSP bets at a single algorithmically-determined price at the moment the market is turned in-play. BSP is native to the exchange and fully accessible via the API.

When Betfair documentation or third-party tools say “SP” in the context of the exchange, they almost always mean BSP.

The StartingPrices Object

Every Runner from listMarketBook carries an sp field of type StartingPrices:

public class StartingPrices {
    private Double nearPrice;         // BSP projection during pre-race betting
    private Double farPrice;          // removed — ignore this field
    private List<PriceSize> backStakeTaken;   // bets submitted as BSP backs
    private List<PriceSize> layLiabilityTaken; // bets submitted as BSP lays
    private Double actualSP;          // final BSP — only populated after the off
}

nearPrice is the model’s best estimate of where BSP will settle, updated continuously during pre-race. It is available before the off and is useful as a late market indicator — it tends to converge toward the final BSP as the market approaches the jump.

actualSP is the final reconciled price. It is null until the market turns in-play (or is suspended at the off). Never assume it is populated — check for null before using it.

StartingPrices sp = runner.getSp();

if (sp.getActualSP() != null) {
    // race is off — final BSP available
    double bsp = sp.getActualSP();
} else if (sp.getNearPrice() != null) {
    // pre-race — use projection
    double nearBsp = sp.getNearPrice();
}

BSP Volume Fields

The backStakeTaken and layLiabilityTaken lists tell you how much money has been placed as BSP bets. These are cumulative and update throughout the pre-race window.

double totalBspBackStake = sp.getBackStakeTaken().stream()
    .mapToDouble(PriceSize::getSize)
    .sum();

double totalBspLayLiability = sp.getLayLiabilityTaken().stream()
    .mapToDouble(PriceSize::getSize)
    .sum();

High backStakeTaken relative to layLiabilityTaken suggests strong back interest — the BSP mechanism will need to find lay money at or near the near price to match it, which can pull the BSP down. The imbalance is a secondary market signal.

Reading nearPrice Through the Pre-Race Window

nearPrice is one of the more useful signals for detecting late money. Plotting it over the final five minutes before the off gives you a BSP trend:

record BspSnapshot(Instant at, long selectionId, Double nearPrice) {}

List<BspSnapshot> snapshots = new ArrayList<>();

// poll every 5 seconds in the pre-race window
for (Runner runner : marketBook.getRunners()) {
    Double near = runner.getSp().getNearPrice();
    if (near != null) {
        snapshots.add(new BspSnapshot(
            Instant.now(),
            runner.getSelectionId(),
            near
        ));
    }
}

A nearPrice that shortens consistently from 6.0 to 4.2 in the final minute is a steam signal — the BSP mechanism is seeing more back money coming in and adjusting the projection downward.

Combining Catalogue and Book Data

A practical pattern: build a RunnerView that joins the two sources once and passes the combined object through your system.

record RunnerView(
    long selectionId,
    String name,
    String trainer,
    String form,
    String officialRating,
    double bestBackPrice,
    double bestLayPrice,
    Double nearBsp,
    Double actualBsp,
    double totalMatched
) {}

static RunnerView build(RunnerCatalog cat, Runner book) {
    Map<String, String> meta = cat.getMetadata();
    ExchangePrices ex = book.getEx();
    StartingPrices sp = book.getSp();

    double bestBack = ex.getAvailableToBack().stream()
        .mapToDouble(PriceSize::getPrice).max().orElse(0);
    double bestLay = ex.getAvailableToLay().stream()
        .mapToDouble(PriceSize::getPrice).min().orElse(0);

    return new RunnerView(
        cat.getSelectionId(),
        cat.getRunnerName(),
        meta.getOrDefault("TRAINER_NAME", ""),
        meta.getOrDefault("FORM", ""),
        meta.getOrDefault("OFFICIAL_RATING", ""),
        bestBack,
        bestLay,
        sp.getNearPrice(),
        sp.getActualSP(),
        book.getTotalMatched()
    );
}

This keeps the BSP and catalogue concerns out of wherever you use the data.

Post-Race: Extracting BSP from Historical Data

If you are working with Betfair’s historical data files (the Streaming API NDJSON format), BSP appears in the RunnerChange delta once the market turns in-play:

{ "id": 12345678, "sp": { "p": 4.2 } }

The p field in the streaming sp object is actualSP. b and l are the back and lay BSP volumes. The n field is nearPrice. This maps directly to the StartingPrices object above — the REST API and the Streaming API use the same underlying model.

BSP vs Exchange Price: Which to Use?

BSP is a settlement mechanism, not a trading price. For analysis:

  • If you want to know what price casual punters got on the horse, use BSP — it is the price that unmatched SP bets cleared at.
  • If you want to know where the market was trading 60 seconds before the off, use the best available back price from getAvailableToBack().
  • If you want a steam signal, watch nearPrice trend in the final minutes.
  • If you want to calculate expected value against the going or historical performance, you typically want exchange price rather than BSP — BSP includes the SP back/lay imbalance and can distort in illiquid markets.

The two converge in liquid races (big fields, major meetings) and diverge in illiquid ones (small NH fields, Monday evening). That divergence itself is a signal worth modelling.

If you’re building Betfair tooling and need to work through the SP / BSP data model or integrate runner metadata into a trading signal, get in touch.

Samuel Jackson

Samuel Jackson

Senior Java Back End Developer & Contractor

Senior Java Back End Developer — Betfair Exchange API specialist, Spring Boot, AWS, and event-driven architecture. 20+ years delivering high-performance systems across betting, finance, energy, retail, and government. Available for Java contracting.