Skip to main content
Instruction submission is one of two ways to create a settlement: each side submits its own instruction independently, and KeyStone confirms them against each other. The trade itself is agreed upstream, on an execution venue or directly between the counterparties. KeyStone accepts pre-agreed instructions - it never selects a counterparty, prices, nets, or takes custody. Use this route when each side wants to submit for itself, typically because they sit on different platforms and neither will submit on the other’s behalf. Use direct initiation when one platform submits for every party at once.
This route currently confirms exactly two instructions and creates exactly two parties. That is a property of instruction submission as it works today, not of settlements in general: a settlement carries up to 50 parties and 50 legs, and the party count for any given settlement is set by its template. A template naming three or more roles, such as the tri-party repo, is created through direct initiation with POST /v1/settlements.

How it works

  1. First party submits: Platform A sends an instruction without a trade_reference. KeyStone generates one (format: KS-{uuid}).
  2. Share the reference: Platform A shares the trade reference with Platform B through their own channels (email, chat, API, etc.).
  3. Second party submits: Platform B sends an instruction with the same trade_reference. KeyStone finds the pending counterpart instruction.
  4. Automatic confirmation: KeyStone validates that both instructions describe the same trade and creates a settlement with both parties confirmed. (On-chain registration follows once compliance clears.)

What “matched” means

The pending_match and matched statuses describe reference correlation, not trade matching. The trade itself was matched before KeyStone ever saw it - on an execution venue, through an OTC desk, or directly between the counterparties. What KeyStone does is bring the two halves of that already-matched trade together:
  • A pending instruction is not visible to any other participant. There is no order book to browse and no way for a third party to discover or take the other side of it.
  • An instruction can only ever pair with one that carries the same trade reference - a value the counterparties exchanged out-of-band when they agreed the trade. Two instructions that never shared a reference cannot cross, structurally.
  • A reference has a match scope: one you choose yourself pairs only within your own environment, while cross-platform pairing uses the reference KeyStone generates. See Match scope.
  • KeyStone contributes no economic terms. Price and quantity arrive on both instructions; KeyStone checks that they are equal and complementary, and rejects the pairing into pending_match if they are not. It never sets, improves, or discovers a price.
matched therefore records one fact: both halves of a pre-agreed trade have arrived and agree. There is no order matching, no counterparty discovery, and no price formation anywhere in the flow.

Match scope

The rule is one line: a reference you choose yourself matches only within your own environment; cross-platform matching uses the reference KeyStone generates, between environments of the same type. Caller-chosen references exist so you can carry your own trade ids - the id from your execution venue, OTC desk, or internal booking system - when both sides of the trade are submitted under your environment. They are ordinary strings a third party could know or guess, so they never reach across environments; for a trade settling between two platforms, always let KeyStone generate the reference and share that. Environment types (development, staging, production) are isolation worlds. An instruction submitted from a development environment is a test trade and can never become the counterparty of a production instruction, whatever reference it presents - so an integration test cannot consume, or settle against, a live trade. Both environments must also be active. Deactivating an environment takes its resting instructions out of match scope once the deactivation commits: no credential resolves into a deactivated environment, so its owner could not see the settlement a match would create, act on it, or cancel the instruction behind it. Instructions resting there are left as they are and expire on their own. A match already in flight when the deactivation commits can still land; the settlement it creates is in flight like any other and completes. A submission that names a reference resting in another environment - in an environment of another type, or in a deactivated one - is not rejected: it is stored as pending_match in your own environment, exactly as if you were the first party - and within everything your credential can see, you are. The API never reveals whether a reference is in use in an environment outside your match scope. When more than one counterparty instruction rests on a reference within your match scope, your submission pairs with the oldest one; if that instruction does not agree, your submission rests as pending_match rather than trying a younger one.

Step 1: Submit the first instruction

The first party submits their side of the trade. Leave trade_reference null to have KeyStone generate one.
Response:
You may submit your own trade_reference instead - for example the trade id from your execution venue - but it pairs only with instructions in your own environment. For a trade settling with another platform, leave the field null and use the generated reference (see Match scope).

Step 2: Share the trade reference

The trade reference (KS-abc123...) is shared between counterparties through their own communication channels. KeyStone does not handle this exchange - it is an out-of-band coordination step. Treat a generated reference like a secret: anyone you give it to can submit the other side of the trade. Common methods:
  • Platform-to-platform API integration
  • Pre-agreed trade reference via OTC desk
  • Shared trade confirmation system

Step 3: Submit the counterpart instruction

The second party submits their instruction with the trade reference from Step 1.
If the instructions agree, the response includes the settlement:

Refused: an instruction with this role already rests

A submission whose role already has a resting (pending_match, unexpired) instruction on the same trade reference within your match scope is refused with 409 DUPLICATE_RESOURCE, and nothing is written. This is what a retry with a fresh idempotency_key after a timed-out first submission looks like: the first submission landed. A retry that reuses the original idempotency_key returns that instruction instead. To replace a resting instruction, cancel it first (DELETE /v1/instructions/{id}; list your pending instructions with GET /v1/instructions?status=pending_match to find it) or wait for it to expire. The refusal reveals only that a same-role instruction rests within your match scope, nothing about where: for a Keystone-generated reference, that instruction can be in another environment. Two same-role submissions that race each other can both pass this check and both rest; each counterparty submission then pairs with one of them, and the second pairing creates a second settlement with its own deposit keys that reaches its deadline like any other settlement and that no endpoint cancels. It announces itself the way any settlement does: a settlement_id on the 201 that created it, then its own settlement.state.awaiting_deposits. Where a duplicate submission is possible, reconcile each settlement_id against the trade you intended before funding, rather than funding every deposit window you are told about.

What confirmation validates

Confirmation only considers a resting instruction that is within the submission’s match scope (see Match scope) and not past its expires_at. A lapsed instruction is skipped even before the expiry sweep marks it expired, so a counterpart can be out of reach while it still reads pending_match - see Expiry. Both instructions must then agree on: Both sides’ stored data must also be admissible: before the settlement is created, each instruction is re-checked against what submission itself admits, and the match is refused when, among other checks, a stored wallet_address is outside the required form, a party chain_id is outside the stored range, a party field exceeds its length bound, or a leg’s leg_type, token_standard, or chain_id is outside the type and length bounds submission enforces (see the field rules in parties and legs). An instruction that submission accepts as it now stands always passes this check, so only a resting instruction whose stored data submission would no longer admit can refuse the match; it stays unmatched until it expires or is cancelled. These are not the only conditions a match needs, but every check in this section refuses a pairing the same way: the second instruction is still saved as pending_match. It will not be automatically paired with the first instruction. A role collision never gets this far: a submission whose role already rests on the reference within your match scope is refused at intake with 409 DUPLICATE_RESOURCE and nothing is written (see Refused: an instruction with this role already rests). For a template_slug or legs refusal, the submitting platform can cancel it and resubmit with corrected details. When the refusal came from the resting counterpart instead - it has lapsed, or its stored data is what submission would no longer admit - that remedy reproduces the refusal: the instruction that must change is the resting one, and only the platform that submitted it can cancel it. While it still reads pending_match it can be cancelled and resubmitted with corrected details; a lapsed one can no longer match whether or not it is cancelled, so a fresh instruction for that side is what completes the trade. The second instruction itself needs no resubmission: it stays eligible until its own expires_at.

Optional fields

Cancellation

Cancel a pending instruction if it was submitted in error or the trade is no longer needed:
Only instructions with status: pending_match can be cancelled. Instructions already in matched or expired status cannot be cancelled.

Expiry

Instructions expire 24 hours after submission; the deadline is returned on the instruction as expires_at. Eligibility for confirmation ends at expires_at itself, not when the periodic sweep later stamps the row expired: until the sweep runs, a lapsed instruction still reads pending_match but can no longer match. This prevents stale instructions from accidentally pairing with new ones submitted days later.

Listing instructions

View your platform’s instructions:

Single-platform use

Paired instructions work for single-platform settlements too. When both instructions come from the same platform, the settlement is created as single_platform type. This is useful when your platform intermediates trades between its own users.

Escrow Deposits

How parties deposit into the settlement contract with commitment secrets.

Escrow Deposits

How parties deposit to escrow using commitment secrets.