# Agent Futures — Product Specification

**Service schema:** `delx/agent-futures/v1`
**Receipt schema:** `delx/agent-futures-receipt/v1`
**Primary object:** `FutureCapacityRequest`
**Current stage:** `private_beta`
**Custody:** none
**Normative implementation:** `runtime/agent-futures.mjs`

## Purpose

Agent Futures records a buyer's request for scarce capacity during a future
delivery window, one or more seller offers, the buyer's selected match, and the
eventual fulfillment or no-show.

Despite the product name, v1 is a **non-transferable capacity commitment
ledger**. It is not a financial futures exchange: there is no fungible contract,
secondary transfer, margin, collateral, mark-to-market, clearing, settlement,
or custody.

## Scope boundary

Agent Futures is:

- a Delx Commerce economic primitive;
- capacity-later rather than Demand Board task-now;
- receipt bookkeeping for a time-bounded bilateral commitment;
- usable only where capacity is meaningfully scarce or constrained.

It is not:

- Protocol recovery, Hive, or Continuity Seals;
- a guarantee that capacity exists;
- a payment or escrow rail;
- a standardized derivative or investment product;
- a scheduler that performs the promised work.

## Actors and trust model

| Actor | v1 action | Current trust limitation |
| --- | --- | --- |
| Buyer | Opens/amends/cancels/selects a request and may dispute | Private HTTP mode binds `buyer_id` to an Ed25519 principal; direct in-process legacy calls remain caller-supplied. |
| Seller | Posts/withdraws an offer and submits delivery proof | Private HTTP mode binds `seller_id` to a seller principal and requires a capacity attestation. |
| Operator | Hosts storage, signer, expiry execution, remediation, and access boundary | The local candidate assumes a trusted operator and one writer. |
| Consumer | Reads open/all projections and receipts | Private HTTP mode requires `economic:read` and redacts sensitive fields by scope. |

The raw buyer and seller identifiers remain weak shared secrets for direct
legacy service calls. They must never be presented as cryptographic identity or
delegated authority; production HTTP callers use the principal policy layer.

## Lifecycle

```mermaid
stateDiagram-v2
    [*] --> open: open_request
    open --> offered: offer_capacity
    offered --> offered: additional offer_capacity
    open --> expired: deliver_before elapsed
    offered --> expired: deliver_before elapsed
    open --> matched: confirm_match after offer exists
    offered --> matched: confirm_match
    matched --> fulfilled: valid proof inside window
    matched --> no_show: window elapsed without fulfillment
    fulfilled --> [*]
    no_show --> [*]
    expired --> [*]
```

| State | Meaning | Terminal |
| --- | --- | --- |
| `open` | Request exists and has no offers. | No |
| `offered` | At least one valid offer exists. | No |
| `matched` | Buyer selected one offer. | No |
| `fulfilled` | Selected seller submitted a proof string inside the delivery window. | Yes |
| `no_show` | Matched capacity passed `deliver_before` without accepted fulfillment. | Yes |
| `expired` | Unmatched request reached `deliver_before`. | Yes |
| `cancelled` | Buyer cancelled before match. | Yes |
| `disputed` | Buyer/seller disputed after fulfill or no-show. | Yes |
| `remediated` | Operator recorded remediation after dispute (or after terminal evidence). | Yes |

Implemented lifecycle controls (non-custodial bookkeeping only):

- inventory attestation / overbooking guard on offers;
- amend unmatched request commercial terms (with reoffer constraints);
- cancel unmatched request;
- withdraw unmatched seller offer;
- dispute + operator remediate;
- HTTP `POST /api/v1/economic/futures/tick` to advance deterministic expiry/no-show.

Not in v1: partial-fill, rematch, refund, transferable positions, USDC settlement.

## Time and money semantics

- Timestamps are parsed as ISO-8601 and normalized to UTC.
- `deliver_before` must be later than `deliver_after` and in the future when the
  request opens.
- v1 does not require `deliver_after` itself to be in the future. This permits a
  window that has already opened but has not closed.
- A seller offer must cover the buyer's entire requested window:
  `offer.deliver_after <= request.deliver_after` and
  `offer.deliver_before >= request.deliver_before`.
- Fulfillment is accepted only at or after `deliver_after` and at or before
  `deliver_before`.
- USDC amounts are metadata encoded as positive decimal strings with up to six
  fractional digits.
- Offer price must be less than or equal to `max_price_usdc`.
- No amount is reserved, moved, settled, or refunded. Inventory reservations
  are bookkeeping counters only and are released on withdrawal, cancellation,
  expiry, fulfillment, no-show, or remediation.

## Object model

### Request input

| Field | Type | Constraint |
| --- | --- | --- |
| `buyer_id` | string | Required, non-empty, at most 256 bytes. |
| `capacity_kind` | string | Required, non-empty, at most 128 bytes. Vocabulary is not governed in v1. |
| `quantity` | integer | Required, greater than zero. |
| `deliver_after` | ISO-8601 string | Required. |
| `deliver_before` | ISO-8601 string | Required, later than `deliver_after` and current time. |
| `max_price_usdc` | decimal string | Required, positive, at most six decimal places. |
| `idempotency_key` | string | Required, non-empty, at most 256 bytes. |

### Public request projection

The public projection includes request/state identifiers, capacity kind,
quantity, price ceiling, window, offer projections, selected `match_id`,
timestamps, receipt count, and the product boundary. It does not expose raw
buyer ID.

### Offer projection

The offer includes `offer_id`, request ID, seller ID hash, quantity, price,
availability window, and creation time. The raw seller ID is persisted but not
returned in the public offer projection.

### Match projection

The match includes the selected request/offer, state, capacity kind, quantity,
price, request window, delivery proof hash, timestamps, and receipt count. It
does not expose raw buyer/seller identifiers.

## Operations and invariants

### Open request

`POST /api/v1/economic/futures/open`

Preconditions:

- valid request fields;
- `deliver_before > deliver_after`;
- `deliver_before > now`;
- idempotency scope `open:<sha256(tenant_id)>:<sha256(buyer_id)>:<idempotency_key>` unused or used
  by an identical request.

Postconditions:

- creates an `open` request;
- emits `capacity_request_opened`;
- persists the response and idempotency record.

### Offer capacity

`POST /api/v1/economic/futures/offer`

Preconditions:

- request is `open|offered` and not expired;
- offer quantity exactly equals request quantity;
- price does not exceed buyer maximum;
- seller window fully covers request window.
- authenticated private-mode calls include `capacity_attestation` with an
  inventory key, positive capacity units, and a bounded attestation string;
  the same seller/inventory/window cannot be overbooked.

Postconditions:

- appends an offer without replacing earlier offers;
- sets request state to `offered`;
- emits `capacity_offered`.

The attestation is an explicit seller assertion, not an oracle or collateral.
The service prevents local overbooking for the asserted inventory key but does
not verify the underlying external inventory.

### Amend request

`POST /api/v1/economic/futures/amend` changes bounded commercial terms while a
request is `open|offered`. Quantity or delivery-window changes require no active
offers; a matched or terminal request cannot be amended. It emits
`capacity_request_amended`.

### Cancel request

`POST /api/v1/economic/futures/cancel` lets the authenticated buyer cancel an
unmatched request. Active offer reservations are released and
`capacity_request_cancelled` is emitted. It moves no money.

### Withdraw offer

`POST /api/v1/economic/futures/withdraw` lets the authenticated seller withdraw
an unmatched offer, release its bookkeeping reservation, and emit
`capacity_offer_withdrawn`.

### Confirm match

`POST /api/v1/economic/futures/match`

Preconditions:

- caller supplies a `buyer_id` whose SHA-256 matches the stored request owner;
- request is matchable and selected offer exists.

Postconditions:

- creates one match;
- changes request state to `matched`;
- emits `capacity_matched`.

In private HTTP mode the principal policy binds the buyer and tenant before the
service sees the mutation. Direct legacy service calls still use the stored hash
comparison only.

### Fulfill

`POST /api/v1/economic/futures/fulfill`

Preconditions:

- caller supplies a `seller_id` whose SHA-256 matches the selected offer;
- match is `matched`;
- current time is inside the buyer's delivery window;
- `delivery_proof` is non-empty and at most 8,192 bytes.

Postconditions:

- stores only `sha256(delivery_proof)` on the public match;
- sets match and request to `fulfilled`;
- emits `capacity_fulfilled`.

The service proves that a string was submitted, not that the promised work was
valid, complete, safe, or accepted by the buyer.

### Mark no-show

`POST /api/v1/economic/futures/no-show`

Preconditions:

- match is still `matched`;
- current time is strictly after `deliver_before`.

Postconditions:

- sets match and request to `no_show`;
- emits `capacity_no_show`.

In private HTTP mode only an `operator` principal may invoke no-show/tick; the
service itself remains deterministic and non-custodial.

### Dispute and remediation

`POST /api/v1/economic/futures/dispute` is available to a buyer or seller after
fulfillment or no-show. It records a reason and enters `disputed` without
moving funds. `POST /api/v1/economic/futures/remediate` is operator-only in
private HTTP mode, records an outcome/note, releases any remaining reservation,
and enters `remediated`.

### List

- `GET /api/v1/economic/futures` returns only `open|offered` requests.
- `GET /api/v1/economic/futures/all` returns every request projection.

Listing evaluates expiry for unmatched requests. There is no background timer
inside the service. Operators advance deterministic expiry/no-show via
`POST /api/v1/economic/futures/tick` (requires operator role, matching tenant,
and an `idempotency_key` in private auth mode). A replay of the same tick key
returns the original receipt list without duplicating transitions. Matched
no-show is also advanced when a relevant service operation evaluates the match.

## Idempotency

Idempotency is durable in the same snapshot as product state. A repeated key in
the same operation scope returns the stored response. A repeated key with a
different fingerprint returns HTTP 409 `idempotency_conflict`.

Scopes include tenant and actor hashes where needed. The local candidate keeps
records until an explicit retention policy is approved; bounded retention and
cardinality remain a promotion gate rather than an implicit eviction policy.

## Receipt chain

| Mutation | Receipt kind |
| --- | --- |
| Open | `capacity_request_opened` |
| Offer | `capacity_offered` |
| Match | `capacity_matched` |
| Fulfill | `capacity_fulfilled` |
| Expire | `capacity_expired` |
| No-show | `capacity_no_show` |
| Amend | `capacity_request_amended` |
| Cancel | `capacity_request_cancelled` |
| Withdraw | `capacity_offer_withdrawn` |
| Dispute | `capacity_disputed` |
| Remediate | `capacity_remediated` |

Receipts contain subject/state, event metadata, UTC creation time, explicit
non-custody, the previous receipt hash, and a signer result. Authenticated HTTP
uses the configured Ed25519 signer; direct local fixtures may use the
deterministic demo signer. See [API Reference](API_REFERENCE.md#receipt-contract).

## Errors

Product-specific error codes include:

`buyer_denied`, `seller_denied`, `actor_denied`, `invalid_window`,
`window_mismatch`, `window_still_open`, `quantity_mismatch`, `price_above_max`,
`inventory_overbooked`, `inventory_attestation_conflict`,
`request_not_found`, `request_not_open`, `request_not_matchable`,
`request_not_amendable`, `request_not_cancellable`, `offer_not_found`,
`offer_not_withdrawable`, `match_not_found`, `match_not_fulfillable`,
`match_not_open`, `match_not_disputable`, `match_not_remediable`,
`amendment_requires_reoffer`, `too_early`, and `too_late`.

Common validation, JSON, rate-limit, idempotency, and server errors are defined
in [API Reference](API_REFERENCE.md#error-contract).

## Privacy and safety

- Raw buyer and seller identifiers are persisted in the snapshot.
- Public projections expose hashes; low-entropy IDs may still be guessable.
- Capacity kind may reveal operational plans.
- Offer windows and prices may reveal scheduling or commercial information.
- Delivery proof content is not public, and only its hash is returned; the raw
  string can still appear in an idempotency response stored in the snapshot.

The service must remain private until the visibility, retention, encryption,
and authenticated access controls in
[Security and Privacy](SECURITY_AND_PRIVACY.md) pass.

## Composition

A future version may attach a request to a Firm role/capability and require a
Judgment Matter before a high-stakes match. Version 1 has no `firm_id`,
`matter_id`, foreign-key enforcement, or transactional cross-product action.
Integrators must not infer composition from free-text fields.

## Product promotion gates

Before public beta, Agent Futures still needs:

1. a live private deployment with managed keys, edge controls, and rollback;
2. off-site backup/RPO/RTO and host monitoring evidence;
3. privacy/legal approval and an independent security review;
4. precise naming/claims review if positions become transferable or collateralized;
5. external evidence that buyers reserve genuinely scarce capacity.
