Multi-Settlement (Settlement Menus)

Introduction

Before multi-settlement, an intent named exactly one way to be paid: one
settlement asset, one amount, one destination. Multi-settlement lets a single
intent offer several settlement alternatives, and lets each alternative be a
bundle of legs that must all settle together.

The model is a menu: pick exactly one option, take all of it.

settlements (ONE-OF — the executor picks exactly one)
├── group 0 (ALL-OF — every leg settles, or none do)
│   ├── leg: 50 USD  → seller's USD account
│   └── leg:  3 GBP  → seller's GBP account
└── group 1 (ALL-OF)
    └── leg: 45 EUR  → seller's EUR account

Two terms are used throughout:

  • settlement group — one alternative on the menu. All of its legs settle
    atomically; there is no partial settlement of a group.
  • leg — one asset movement within a group: an asset, an amount, and the
    accounts it moves between.

Supported on buying, selling and private offer intents. Issuance,
loan, transfer, redemption, request-for-transfer and move are unchanged.

The flow

  1. Declare — the intent owner creates an intent carrying a list of settlement
    groups.
  2. Select — the executor submits a flat list of the legs it will settle.
  3. Match — the router matches that leg-set against the declared groups. It
    must match exactly one group, as a set, on both leg identity and amount.
  4. Execute — the matched group is recorded on the execution plan as
    selectedSettlement, and every leg gets its own settlement instructions.

Matching is set-based: leg order does not matter.

Where the fields live

The same concept appears under three different names, one per stage. This is the
most common source of confusion, so it is worth memorising:

StageEndpoint / objectFieldShape
Declarecreate intent (buyingIntent, sellingIntent, privateOfferIntent)settlementsarray of groups, each { legs: [...] }nested
Selectexecute intent (buyingIntentExecution, sellingIntentExecution, privateOfferIntentExecution)settlementsflat array of the legs being settled — not grouped
Resultexecution plan → contract.contractDetailsselectedSettlement.legsthe one matched group, flattened

The executor does not send a group index. It sends the legs, and the router
infers which group was chosen. That keeps the selection self-describing and
means a client cannot pick a group while paying something else.

Declaring a menu

A selling intent offering two alternatives — be paid in one bundle of USD +
GBP, or in EUR alone. The settlement asset is identified by the account the leg
pays into, not by the term:

{
  "settlements": [
    { "legs": [
        { "settlementTerm": { "type": "fullSettlement", "amount": "50" },
          "settlementInstruction": {
            "destinationAccount": {
              "account": { "type": "finId", "finId": "02e1...", "orgId": "bank-us" },
              "asset":   { "assetId": "bank-us:102:usd-asset" }
            } } },
        { "settlementTerm": { "type": "fullSettlement", "amount": "3" },
          "settlementInstruction": {
            "destinationAccount": {
              "account": { "type": "finId", "finId": "02e1...", "orgId": "bank-uk" },
              "asset":   { "assetId": "bank-uk:102:gbp-asset" }
            } } }
      ] },
    { "legs": [
        { "settlementTerm": { "type": "fullSettlement", "amount": "45" },
          "settlementInstruction": {
            "destinationAccount": {
              "account": { "type": "finId", "finId": "02e1...", "orgId": "bank-eu" },
              "asset":   { "assetId": "bank-eu:102:eur-asset" }
            } } }
      ] }
  ]
}

Selling and private-offer legs name a destination account — where the seller
gets paid. Buying legs are the mirror image: a buying leg names the buyer's
paying source account (settlementInstruction.sourceAccount), and leg
identity is keyed on it accordingly.

An empty settlements list (or a missing one, with no singular settlement
either) is valid and means free-of-payment: the asset moves and nothing is
paid.

Selecting at execution

To take group 0, the executor submits both of its legs, flat. Execution-side legs
use the shorter term / instruction names:

{
  "settlements": [
    { "term": { "amount": "50" }, "instruction": { "...": "USD leg accounts" } },
    { "term": { "amount": "3"  }, "instruction": { "...": "GBP leg accounts" } }
  ]
}

Submitting only the USD leg is rejected — a group is all-or-nothing.

Rules and errors

Checked when the intent is created:

CodeRule
7327A present group must declare at least one leg.
7325No duplicate leg (same asset + destination) within one group.
7326No two groups with an identical leg set. Groups are identified by which assets move where, never by amount — so two groups differing only in amount are the same option, and the second is rejected.
4003All legs within one group must share a settlement strategy — all fullSettlement, or all partialSettlement, never mixed.

Overlapping or subset groups across different groups are allowed. "USD+GBP" and
"USD alone" can both be on the menu.

Checked when the intent is executed:

CodeRule
7324Execution must select exactly one group. Zero legs against a non-empty menu, or legs against an empty menu, both fail here.
7321A declared leg of the selected group was not submitted.
7322A submitted leg belongs to no declared group.
7323A submitted leg's amount differs from the declared amount.
7328The submitted leg-set matches no declared group, and no single group was close enough to name a more specific reason.

For a partial (unit-priced) leg, the expected amount is the leg's declared
unitValue multiplied by the executed asset quantity, not a fixed figure.
Amounts are compared after exponent normalisation, so 50 and 50.00 are equal.

Backward compatibility

Nothing about single-settlement trades changes. A client that never sends
settlements keeps working exactly as before.

The behaviour worth knowing about is on the contract, because it is not
symmetrical:

TradecontractDetails.settlementcontractDetails.selectedSettlement
Single settlement (legacy shape)presentpresent, one leg
Menu, any group selectedabsentpresent, one leg per leg of the group

selectedSettlement is therefore the field to read in new integrations — it is
populated in both cases. Code that reads only the singular settlement will find
it missing on any menu-based trade.

Protocol version and signatures

Two gates apply, and both fail loudly rather than silently degrading:

  • Protocol version 2, gated on the selected group. Multi-settlement is
    advertised as the INTENT_FEATURE_MULTI_SETTLEMENT capability, which requires
    protocol version 2. The feature is inferred from the plan only when the
    selected group has more than one leg, so a menu whose groups are all
    single-leg still runs against a v1 counterparty.
  • Investor signature template v3, gated on the declared menu. Any intent
    that declares one or more settlement groups must sign with template version 3
    or above (hashlist or eip712) — even a single group with a single leg.
    v3 is additive on top of v2: for every intent type other than buying /
    selling / private offer it is byte-identical, and for those three it appends
    trailing hash groups encoding the menu, so the whole menu is inside what the
    investor signed. See Transaction Signature Schemes for the field-level
    layout.

The two gates deliberately trigger on different things: the signature must cover
whatever was offered, while the protocol version only has to cover what is
actually executed.

Policies

Each leg is policy-matched on its own settlement asset, so a two-leg group can
settle under two different policies — a USD leg under a HashList policy and a
GBP leg under an EIP-712 one, each holding under its own terms.

For policies that must govern every leg without knowing the leg count up
front, the operational API adds two selector types, batch_account and
batch_asset. Their path traverses the repeated legs field, and that
traversal is the fan-out:

contract_details.private_offer.selected_settlement.legs.asset_instruction.source_account

One batch selector resolves to one account per leg. The execution-plan
translator replicates the instruction per leg, and each replica is matched
against its own leg, which makes a policy written this way independent of how
many legs a group happens to have. A path that names a singular field degrades
to a single result, so the same selector works for a one-leg group.

Appendix — contract details JSON

Below is a real contract.contractDetails from an executed buying trade with a
two-leg group (50 of a bank-us asset plus 3 of a bank-uk asset), retrieved
with GET /finapi/execution/{planId}:

{
  "type": "buying",
  "asset": {
    "instruction": {
      "destinationAccount": {
        "account": {
          "custodian": { "orgId": "ownera-custody" },
          "finId": "02e154210ca0746c7739de6319ce1cdeab26ffbcfc58fe8cbe07f905fec6c2709c",
          "orgId": "bank-us",
          "type": "finId"
        },
        "asset": {
          "id": "bank-us:102:9afb3e85-16f5-4a19-a61e-16c6ee570caf",
          "ledgerIdentifier": {
            "assetIdentifierType": "CAIP-19",
            "network": "bank-us-vanilla",
            "standard": "GBA",
            "tokenId": "ebda20a6-4282-43b3-9fc0-09f360154829"
          }
        }
      },
      "sourceAccount": {
        "account": {
          "custodian": { "orgId": "ownera-custody" },
          "finId": "03abdf71432b51a664f2f82b98f6450c0fb87489496afdfb798735d86e41cf3afc",
          "orgId": "bank-us",
          "type": "finId"
        },
        "asset": {
          "id": "bank-us:102:9afb3e85-16f5-4a19-a61e-16c6ee570caf",
          "ledgerIdentifier": {
            "assetIdentifierType": "CAIP-19",
            "network": "bank-us-vanilla",
            "standard": "GBA",
            "tokenId": "ebda20a6-4282-43b3-9fc0-09f360154829"
          }
        }
      }
    },
    "term": { "amount": "10" }
  },
  "selectedSettlement": {
    "legs": [
      {
        "instruction": {
          "destinationAccount": {
            "account": {
              "custodian": { "orgId": "ownera-custody" },
              "finId": "03abdf71432b51a664f2f82b98f6450c0fb87489496afdfb798735d86e41cf3afc",
              "orgId": "bank-us",
              "type": "finId"
            },
            "asset": {
              "id": "bank-us:102:ac51ea85-705a-4aec-9ad0-c181520c1c75",
              "ledgerIdentifier": {
                "assetIdentifierType": "CAIP-19",
                "network": "bank-us-vanilla",
                "standard": "GBA",
                "tokenId": "5073d2b3-72b7-4db3-8a6a-140e8c091f26"
              }
            }
          },
          "sourceAccount": {
            "account": {
              "custodian": { "orgId": "ownera-custody" },
              "finId": "02e154210ca0746c7739de6319ce1cdeab26ffbcfc58fe8cbe07f905fec6c2709c",
              "orgId": "bank-us",
              "type": "finId"
            },
            "asset": {
              "id": "bank-us:102:ac51ea85-705a-4aec-9ad0-c181520c1c75",
              "ledgerIdentifier": {
                "assetIdentifierType": "CAIP-19",
                "network": "bank-us-vanilla",
                "standard": "GBA",
                "tokenId": "5073d2b3-72b7-4db3-8a6a-140e8c091f26"
              }
            }
          }
        },
        "term": { "amount": "50" }
      },
      {
        "instruction": {
          "destinationAccount": {
            "account": {
              "custodian": { "orgId": "ownera-custody" },
              "finId": "03abdf71432b51a664f2f82b98f6450c0fb87489496afdfb798735d86e41cf3afc",
              "orgId": "bank-uk",
              "type": "finId"
            },
            "asset": {
              "id": "bank-uk:102:e48bccc7-276a-4264-8b5a-611837f7bdf3",
              "ledgerIdentifier": {
                "assetIdentifierType": "CAIP-19",
                "network": "bank-us-vanilla",
                "standard": "GBA",
                "tokenId": "5073d2b3-72b7-4db3-8a6a-140e8c091f26"
              }
            }
          },
          "sourceAccount": {
            "account": {
              "custodian": { "orgId": "ownera-custody" },
              "finId": "02e154210ca0746c7739de6319ce1cdeab26ffbcfc58fe8cbe07f905fec6c2709c",
              "orgId": "bank-uk",
              "type": "finId"
            },
            "asset": {
              "id": "bank-uk:102:e48bccc7-276a-4264-8b5a-611837f7bdf3",
              "ledgerIdentifier": {
                "assetIdentifierType": "CAIP-19",
                "network": "bank-us-vanilla",
                "standard": "GBA",
                "tokenId": "5073d2b3-72b7-4db3-8a6a-140e8c091f26"
              }
            }
          }
        },
        "term": { "amount": "3" }
      }
    ]
  }
}

investors sits alongside contractDetails on the contract and is unchanged by
multi-settlement — the same buyer/seller entries, each with its signature. Note
that a selling or private offer contract has the identical
selectedSettlement.legs shape; only the type discriminator and which side of
each leg carries the counterparty account differ.