# Capacity marketplace (/docs/strategies/capacity)



The capacity marketplace lets <Term name="Worker" />s (modelled today as
recipients) **buy and sell spare capacity units** among themselves. A worker with
idle headroom lists it; a worker that is saturated buys units to raise its own
cap. It is the **over-subscription economics** plane of Ductor — a market for
*capacity itself*, settled per pool under a configurable fee and price band. It is
a second clearing surface alongside the [work markets](/docs/strategies/markets):
the same settlement discipline, applied to the right to handle more work rather
than to a unit of work.

<Callout title="Not the same thing as Markets" type="warn">
  [Markets](/docs/strategies/markets) is **auction routing** — recipients compete
  for a unit of work being routed *right now* (claim races, ping/post, first- and
  second-price auctions). The unit traded there is *an opportunity*.

  The **capacity marketplace** trades *capacity* — the right to handle more work
  later. No routing decision is being made; recipients are rebalancing their caps
  by transacting units between each other. Different plane, different unit, don't
  conflate them.
</Callout>

```mermaid
flowchart LR
  S[Seller lists spare units] --> OB[Order book per pool]
  OB --> B[Buyer purchases units]
  B --> T[Trade: units + fee settled]
  T --> Cap[Buyer cap up, seller cap down]
```

<Callout title="Opt-in per pool" type="warn">
  The marketplace is **disabled by default** — `MarketConfig.Enabled` starts
  `false` for every pool. A pool does not participate until you enable it and set
  its price band via [market config](#market-config). The service ships as a
  contrib module (`modules/services/capacity`) wired to the marketplace store.
</Callout>

## Operations [#operations]

Ten endpoints under `/api/capacity`, in four groups.

| Group                                        | Operations                                                                   |
| -------------------------------------------- | ---------------------------------------------------------------------------- |
| [Listings](#listings)                        | `GET` · `POST /listings` · `GET` · `PATCH` · `DELETE /listings/{listing_id}` |
| [Order book & pricing](#order-book--pricing) | `GET /pools/{pool_id}/orderbook` · `GET /pools/{pool_id}/suggest-price`      |
| [Trades](#trades)                            | `GET /trades`                                                                |
| [Market config](#market-config)              | `GET` · `PUT /pools/{pool_id}/config`                                        |

## Listings [#listings]

A **listing** is spare capacity offered for sale: a seller posts `units` at a
`price_per_unit`, optionally bounded by `min_purchase`/`max_purchase`,
`routable_types`, and an expiry. Listings move through
`active → filled | expired | cancelled`.

| Operation            | Method & path                                |
| -------------------- | -------------------------------------------- |
| List active listings | `GET /api/capacity/listings`                 |
| Create listing       | `POST /api/capacity/listings`                |
| Get one listing      | `GET /api/capacity/listings/{listing_id}`    |
| Update listing       | `PATCH /api/capacity/listings/{listing_id}`  |
| Cancel listing       | `DELETE /api/capacity/listings/{listing_id}` |

`GET /api/capacity/listings` filters by `pool_id` and paginates
(`pagination.page_size` up to 1000, `pagination.page_token`), newest first.
`PATCH` is a partial update — units, price, purchase bounds, expiry, and
metadata — and `DELETE` cancels a listing without touching already-executed
trades.

```bash
curl -X POST -H "X-Tenant-ID: $DUCTOR_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{"pool_id":"'$POOL_ID'","seller_id":"'$SELLER_ID'","units":50,"price_per_unit":2.5,"currency":"USD","min_purchase":5}' \
  http://localhost:8080/api/capacity/listings
```

## Order book & pricing [#order-book--pricing]

`GET /api/capacity/pools/{pool_id}/orderbook` returns the pool's live market
state — every active listing, `total_units_available`, and the average, minimum,
and maximum price per unit, plus recent trades.

`GET /api/capacity/pools/{pool_id}/suggest-price` returns platform pricing
guidance for a pool: a suggested bid and ask, the observed market price, the
spread, available supply, and a demand signal — computed from current market
conditions.

## Trades [#trades]

A **trade** is a completed capacity transfer. It records the parties
(`seller_id`, `buyer_id`), `units`, `price_per_unit`, `total_price`, the
`platform_fee` and `seller_credit`, and the buyer's and seller's cap before and
after — so the economic effect on each side is fully auditable.

`GET /api/capacity/trades` lists completed trades for a `recipient_id` (with a
`limit`, default 50).

<Callout title="Trades are listed, not posted here" type="info">
  There is **no public endpoint that executes a trade**. `GET /api/capacity/trades`
  only *reads* completed trades. Trade execution runs inside the marketplace engine
  (`ExecuteTrade` in `modules/services/capacity/domain/engine.go`), which validates
  and deducts units atomically to avoid over-selling a listing. The REST surface
  exposes listings, the order book, pricing, config, and this read-only trade
  history.
</Callout>

## Market config [#market-config]

`GET`/`PUT /api/capacity/pools/{pool_id}/config` reads and writes the pool's
marketplace configuration — the fee structure, price band, and trading limits
that govern every listing and trade in the pool.

<TypeTable
  type="{
  enabled: { description: &#x22;Whether the marketplace is live for this pool.&#x22;, type: &#x22;boolean&#x22;, default: &#x22;false&#x22; },
  fee_percent: { description: &#x22;Platform fee as a percentage of trade value.&#x22;, type: &#x22;number&#x22;, default: &#x22;10.0&#x22; },
  min_fee: { description: &#x22;Minimum absolute fee per trade.&#x22;, type: &#x22;number&#x22;, default: &#x22;0&#x22; },
  max_fee: { description: &#x22;Maximum absolute fee per trade.&#x22;, type: &#x22;number&#x22;, default: &#x22;0&#x22; },
  min_price_per_unit: { description: &#x22;Floor price per unit (0 = unbounded).&#x22;, type: &#x22;number&#x22;, default: &#x22;0&#x22; },
  max_price_per_unit: { description: &#x22;Ceiling price per unit (0 = unbounded).&#x22;, type: &#x22;number&#x22;, default: &#x22;0&#x22; },
  max_listings_per_seller: { description: &#x22;Cap on concurrent listings per seller.&#x22;, type: &#x22;integer&#x22;, default: &#x22;5&#x22; },
  max_trades_per_day: { description: &#x22;Cap on trades per day.&#x22;, type: &#x22;integer&#x22;, default: &#x22;20&#x22; },
  cooldown_minutes: { description: &#x22;Minimum minutes between a seller's trades.&#x22;, type: &#x22;integer&#x22;, default: &#x22;5&#x22; },
  auto_list: { description: &#x22;Automatically list idle capacity.&#x22;, type: &#x22;boolean&#x22;, default: &#x22;false&#x22; },
  auto_list_discount: { description: &#x22;Discount percent applied to auto-listed units.&#x22;, type: &#x22;number&#x22;, default: &#x22;20.0&#x22; },
}"
/>

`PUT` rejects an inconsistent band (e.g. `min_fee > max_fee`) with `422`.

## Related [#related]

<Cards>
  <Card title="Markets" href="/docs/strategies/markets">
    Auction routing — competing for work, not trading capacity. The plane to not confuse this with.
  </Card>

  <Card title="Yield & Quality analytics" href="/docs/strategies/yield-and-quality">
    The pricing and performance signals behind capacity valuation.
  </Card>

  <Card title="Idempotency" href="/docs/concepts/idempotency">
    How trades commit exactly once under concurrency.
  </Card>
</Cards>
