diff --git a/AGENTS.md b/AGENTS.md
index 4963f39..a5d597b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -11,7 +11,7 @@ mvn -pl library package # build only the library module
mvn -pl examples package # build only the examples module
```
-There are no tests in this project. CI runs `mvn clean package` on PRs targeting `master`.
+The library's JUnit 5 tests use JSON fixtures and Mockito mocks of `KrakenRestRequester`, without network access or API keys. Run them with `mvn -pl library test`. CI runs `mvn clean package` on PRs targeting `master`.
Java 25 with Temurin is required (configured via `maven-compiler-plugin` with `25`).
@@ -19,7 +19,7 @@ Java 25 with Temurin is required (configured via `maven-compiler-plugin` with `<
`KrakenAPI` is the entry point: typed methods for implemented endpoints, generic `query()` methods taking a `Public`/`Private` enum value and returning a `JsonNode`, and raw `queryPublic()`/`queryPrivate()` taking a path string.
-Every endpoint extends `Endpoint`, either `PublicEndpoint` (GET on `/0/public/{path}`, parameters from `QueryParams`) or `PrivateEndpoint` (POST on `/0/private/{path}`, parameters from `PostParams`, signed with a nonce-based HMAC). Concrete endpoints live in a domain package under `endpoint/` — `market/` for public market data, `account/` for private account data, `subaccount/` for subaccount management, `transparency/` for public pre- and post-trade data, `earn/` for earn strategies and allocations — and follow `{Name}Endpoint`, `params/{Name}Params`, `response/{ResponseType}`.
+Every endpoint extends `Endpoint`, either `PublicEndpoint` (GET on `/0/public/{path}`, parameters from `QueryParams`) or `PrivateEndpoint` (POST on `/0/private/{path}`, parameters from `PostParams`, signed with a nonce-based HMAC). Concrete endpoints live in a domain package under `endpoint/` — `market/` for market data, including authenticated Level3, `account/` for private account data, `subaccount/` for subaccount management, `transparency/` for public pre- and post-trade data, `earn/` for earn strategies and allocations — and follow `{Name}Endpoint`, `params/{Name}Params`, `response/{ResponseType}`.
`KrakenRestRequester` performs the HTTP calls and can be swapped for another HTTP client. Responses are unwrapped from the Kraken `{error, result}` envelope by `KrakenResponse`; ZIP responses (report exports) go through `Endpoint.processZipResponse()`.
diff --git a/README.md b/README.md
index 1127e88..65e4dfd 100644
--- a/README.md
+++ b/README.md
@@ -80,6 +80,64 @@ JsonNode trades = api.queryPublic("Trades", Map.of("pair", "XBTUSD", "count", "1
// {"XXBTZUSD":[["68515.60000","0.00029628",1.7100231295628998E9,"s","m","",68007835]], …
```
+### Market Data
+
+All 12 Market Data endpoints in Kraken's current Spot REST specification have typed methods. In addition to `serverTime`, `systemStatus`, `assetInfo`, `assetPairs`, and `ticker`:
+
+| Endpoint | Typed method | Response |
+|---|---|---|
+| `OHLC` | `ohlc(pair)` / `ohlc(params)` | `OhlcData`: candles by pair and a `last` cursor |
+| `Depth` | `orderBook(pair)` / `orderBook(params)` | `Map` |
+| `GroupedBook` | `groupedOrderBook(pair)` / `groupedOrderBook(params)` | `GroupedOrderBook` |
+| `Trades` | `recentTrades(pair)` / `recentTrades(params)` | `RecentTrades`: trades by pair and a `last` cursor |
+| `Spread` | `recentSpreads(pair)` / `recentSpreads(params)` | `RecentSpreads`: spreads by pair and a `last` cursor |
+| `Level3` (private) | `level3OrderBook(pair)` / `level3OrderBook(params)` | `Level3OrderBook` |
+| `MaintenanceSchedule` | `maintenanceSchedule()` | `MaintenanceSchedule` |
+
+Use parameter builders to set optional fields; omitted fields retain Kraken's defaults:
+
+```java
+OhlcData candles = api.ohlc(OhlcParams.builder()
+ .pair("BTC/USD").interval(60).assetVersion(1).build());
+List hourly = candles.candles().get("BTC/USD");
+
+RecentTrades trades = api.recentTrades(RecentTradesParams.builder()
+ .pair("BTC/USD").count(10).build());
+RecentTrades nextBatch = api.recentTrades(RecentTradesParams.builder()
+ .pair("BTC/USD").since(trades.last()).count(10).build());
+```
+
+`OHLC`, `Depth`, `Trades`, and `Spread` accept `assetVersion(1)` for display pair keys such as `BTC/USD`; without it, Kraken returns internal keys such as `XXBTZUSD`. Their `assetClass("tokenized_asset")` option supports xStocks. Response maps preserve the keys Kraken returns.
+
+The required `pair` field selects one asset pair. Optional numeric fields accept the following values; omit them to use Kraken's defaults:
+
+| Endpoint | Option | Values | Default |
+|---|---|---|---|
+| `OHLC` | `interval` | 1, 5, 15, 30, 60, 240, 1440, 10080, 21600 minutes | 1 |
+| `Depth` | `count` | 1–500 entries per side | 100 |
+| `Trades` | `count` | 1–1000 trades | 1000 |
+| `GroupedBook` | `depth` | 10, 25, 100, 250, 1000 levels per side | 10 |
+| `GroupedBook` | `grouping` | 1, 5, 10, 25, 50, 100, 250, 500, 1000 ticks per level | 1 |
+| `Level3` | `depth` | 0 (full book), 10, 25, 100, 250, 1000 levels per side | 100 |
+
+OHLC candle times, L2 level times and spread times use `Instant`; the `since` fields for OHLC and spreads remain Unix seconds. Grouped books round asks up and bids down to the nearest grouped price level. `MaintenanceSchedule` returns scheduled events for the next seven days, ordered by expected start time; its times use `Instant`, and `cancelBefore` can be absent.
+
+OHLC includes a final candle that is still forming and retains at most 720 entries. Reuse its `last()` cursor as `since` to poll for committed updates. Trade cursors are opaque strings: pass `last()` unchanged. Prices and quantities use `BigDecimal`; trade and Level3 times use `Instant`, retaining nanosecond precision. Level3 decodes Kraken's integer epoch nanoseconds explicitly.
+
+Level3 requires credentials with **Orders and trades – Query open orders & trades** permission:
+
+```java
+KrakenAPI authenticated = new KrakenAPI("my key", "my secret");
+Level3OrderBook book = authenticated.level3OrderBook(Level3OrderBookParams.builder()
+ .pair("YFI/EUR").depth(10).build());
+```
+
+Run the public examples without credentials (after `mvn clean install`):
+
+```sh
+mvn -pl examples exec:java -Dexec.mainClass=dev.andstuff.kraken.example.MarketDataExample
+```
+
### Private endpoints
Private endpoints can be queried in the same way as the public ones, but an API key and secret must be provided to the `KrakenAPI` instance:
@@ -105,22 +163,19 @@ JsonNode order = api.query(KrakenAPI.Private.ADD_ORDER, Map.of(
### Custom endpoints
-An endpoint the library doesn't implement can also be given a proper type, instead of falling back to `JsonNode`. Extend `PublicEndpoint`, or `PrivateEndpoint` for a private one, and pass your endpoint to `query`:
+You can also define typed endpoints outside the library. The following example demonstrates the same mechanism used by the built-in order book endpoint. Extend `PublicEndpoint`, or `PrivateEndpoint` for a private one, and pass your endpoint to `query`:
```java
-public class TradesEndpoint extends PublicEndpoint