---
title: "Create (or preview) a self-serve cross-venue market link"
method: POST
path: "/orders/market-links"
tags: ["Routing"]
---

# Create (or preview) a self-serve cross-venue market link

`POST /orders/market-links`

## Request

- Base URL: `https://execution.kairos.trade`
- URL: `https://execution.kairos.trade/orders/market-links`
- Auth: API key in header `X-Client-Id` and API key in header `X-Api-Key` and API key in header `X-Api-Secret`

Declares that two markets on two different venues are the same real-world contract, making them routable together by `GET /orders/route-quote`, `POST /orders/route-buy` and `POST /orders/route-close`.
**Two-step by design.** `confirm: false` (the default) returns a verified preview and writes nothing. Show the user the resolved pairing, then call again with `confirm: true` AND the preview's `verification_fingerprint` as `fingerprint`. The confirm re-resolves against fresh metadata and requires the result to fingerprint-match what was reviewed, so index drift between the two calls becomes a `409` ("review the pairing again") rather than a silently different link.
**You attest; the server verifies.** Both legs are resolved against indexed market metadata and the link is REFUSED unless the venues' yes/no outcome labels pair exactly (case-insensitive). Outcome inversion is the one catastrophic failure mode here, so it is gated deterministically rather than by the user's click — a `Yes/No` vs `Trump/Harris` pair may well be the same market, but the mapping is not machine-provable and is refused rather than guessed.
**Scope of a created link.** Links are approved but USER-SCOPED: routable only by their creator, so a bad self-link's blast radius is the creator's own wallet. Capped at 25 links per user, enforced inside the insert transaction so concurrent confirms cannot overshoot it.
Expiry gaps and resolution-source divergence come back as `warnings`, not errors — for a self-scoped link they are the creator's risk to accept.
**Rate limited** at 15 requests/minute per user (`MARKET_LINK_RATE_LIMIT_PER_MIN`), fail-closed. Previews count: every call costs two indexed-metadata lookups plus duplicate checks, and the 25-link cap only bounds completed inserts.
> **Error shape.** This endpoint returns `{"message": "..."}` — NOT the `{"error": ...}` used everywhere else on this service, and with no `code` field. See `OrderMarketLinkErrorResponse`.
**Auth.** Requires authentication and the mutation gate (API-key triple, `X-Service-Token`, or CSRF token). API keys and delegations need `trade:execute`, previews included; a created link steers the creator's later routed orders.

## Request body

- OrderCreateMarketLinkRequest: Body for `POST /orders/market-links`.
  - `legs` OrderMarketLinkLegRef[], required: Exactly two legs, on two DIFFERENT venues.
    - `provider` 'polymarket' | 'predictfun', required: Routable venue. Only `polymarket` and `predictfun` are accepted.
    - `market_id` string, required: Either the venue-native executor id or the stream-side alias (the web terminal's Polymarket ids are the numeric Gamma form) — both are matched against the index.
  - `title` string, nullable: Optional display title. Trimmed, truncated to 512 characters; a blank value falls back to the first leg's market name.
  - `similarity` number, double, nullable: Matcher confidence, carried through for provenance only.
  - `confirm` boolean: `false` (the default) returns a verified preview and writes nothing. `true` inserts the link, and additionally requires `fingerprint`.
  - `fingerprint` string, nullable: REQUIRED when `confirm` is `true` — the `verification_fingerprint` from the preview. The confirm re-resolves against fresh metadata and requires the result to match what you reviewed, so index drift becomes a `409` rather than a silently different link.

## Response `200`

A verified preview, a newly created link, or an existing routable link — read `status` to tell which.

- OrderMarketLinkResponse: Response for `POST /orders/market-links`.
  - `status` 'preview' | 'created' | 'exists', required: `preview` (nothing written), `created` (inserted), or `exists` (a link the caller can already route).
  - `link_id` string, uuid, nullable: Null on a `preview`.
  - `scope` 'global' | 'user', nullable: On `exists`, whether the existing link is `global` or the caller's own `user` link. Always `user` on `created`; null on `preview`.
  - `title` string, nullable: Null on an `exists` response.
  - `legs` OrderMarketLinkLegPreview[], required
    - `provider` string, required
    - `market_id` string, required: Executor-convention market id (the condition id on Polymarket).
    - `stream_key` string, required: The stream-side id clients hold (the numeric Gamma id on Polymarket).
    - `outcome_yes_label` string, required
    - `outcome_no_label` string, required
    - `market_name` string, required
    - `expires_at` string, required: `YYYY-MM-DD HH:MM:SS`, as indexed — not RFC 3339.
  - `warnings` string[], required: Non-blocking advisories (venue expiry gaps, resolution-source divergence). Always includes the standing "venues may resolve on different data sources" notice.
  - `verification_fingerprint` string, required: 16-hex-character hash of the verified identity material (ids, tokens, case-folded outcome labels). Echo it back as `fingerprint` on confirm. Change detection, not security.

## Other responses

- `400`: `invalid user id`; `exactly two legs required`; or `confirm requires the preview's verification fingerprint`.
- `401`: No valid credential presented — missing/invalid API-key headers or an invalid/expired session token.
- `409`: `one of these markets is already claimed by another link`; `one of these markets was just claimed by another link` (lost insert race); or `market metadata changed since the preview — review the pairing again` (fingerprint mismatch).
- `422`: The pairing is not verifiable. Messages: `<provider> is not a routable venue`; `legs must be on two different venues`; `<provider>: market <id> is not indexed`; `<provider>: market is settled|closed|resolved`; `<provider>: self-serve linking not supported`; `polymarket: not a binary market (N outcomes)`; `polymarket: outcome labels unavailable` / `token ids unavailable` / `condition id unavailable`; `predictfun: yes token unavailable` / `no token unavailable` / `outcome labels unavailable`; `outcome labels differ (A/B vs C/D) — the side mapping can't be verified`; `link limit reached (25 per user)`.
- `429`: `too many link requests — wait a moment and try again`. No `Retry-After` header.
- `500`: `link lookup failed` or `link insert failed`.
- `502`: `market metadata lookup failed` — the indexed-metadata query failed.

---

[API](https://skmtc.dev/kairos/apis/kairos-order-execution-api.md) - [All operations](https://skmtc.dev/kairos/apis/kairos-order-execution-api/llms.txt) - [OpenAPI document](https://skmtc.dev/kairos/apis/kairos-order-execution-api/revisions/9ba7d463d505?raw)
