> ## Documentation Index
> Fetch the complete documentation index at: https://docs.keystoneos.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Bilateral Instruction Flow

> Submit instructions, share trade references, and track matched settlements.

This guide walks through building the bilateral instruction flow - where a seller submits their side of a trade, shares a trade reference with the buyer, and the buyer submits a matching instruction to create a settlement.

## Prerequisites

* `@keystoneos/react` installed
* `KeystoneProvider` configured with a session token that has `instructions:write` and `templates:read` scopes
* A React project with TypeScript

## How bilateral settlement works

In a bilateral flow, two independent parties agree on a trade off-platform (via phone, email, or OMS). Each party submits their side of the trade to KeyStone. When both sides match, a settlement is created automatically.

```mermaid theme={null}
sequenceDiagram
    participant S as Seller (Securitize Fund I)
    participant SP as Seller's Platform
    participant KS as KeyStone API
    participant BP as Buyer's Platform
    participant B as Buyer (Ondo Capital)

    S->>SP: Enters trade details
    SP->>KS: POST /v1/instructions (seller side)
    KS-->>SP: status: pending_match, tradeReference: KS-abc12345

    S-->>B: Shares trade reference (email, chat, OMS)

    B->>BP: Enters trade details + trade reference
    BP->>KS: POST /v1/instructions (buyer side, tradeReference: KS-abc12345)
    KS->>KS: Match found - create settlement
    KS-->>BP: status: matched, settlementId: 550e8400-...

    KS-->>SP: Webhook: settlement.state.awaiting_deposits<br/>(once compliance clears)
```

The key points:

* The **first** party to submit gets `status: "pending_match"` and a trade reference
* The **second** party submits with that trade reference and gets `status: "matched"` plus a settlement ID
* Either party can be the first to submit - there is no required order

## Understanding legs

A DvP (Delivery vs Payment) settlement involves two legs:

| Leg Type         | Description                      | Seller   | Buyer    |
| ---------------- | -------------------------------- | -------- | -------- |
| `asset_delivery` | The tokenized asset being traded | Delivers | Receives |
| `payment`        | The payment (e.g., USDC)         | Receives | Delivers |

Each party submits **both legs** in their instruction. The direction is derived automatically from the role:

```
seller + asset_delivery = deliver
seller + payment        = receive
buyer  + asset_delivery = receive
buyer  + payment        = deliver
```

Each leg specifies its own `chain_id` and `instrument_id` (the token contract address).

## Building the instruction form

### Template selector

Start by letting the user pick a settlement template. Templates define the structure of the settlement (roles, leg types, state machine).

```tsx theme={null}
// template-selector.tsx
import { useTemplates } from '@keystoneos/react';

interface TemplateSelectorProps {
  value: string | undefined;
  onChange: (slug: string) => void;
}

export function TemplateSelector({ value, onChange }: TemplateSelectorProps) {
  const { templates, isLoading } = useTemplates();

  if (isLoading) return <div>Loading templates...</div>;

  return (
    <div>
      <label style={{ display: 'block', marginBottom: '4px', fontWeight: 600, fontSize: '14px' }}>
        Settlement Template
      </label>
      <select
        value={value ?? ''}
        onChange={e => onChange(e.target.value)}
        style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db' }}
      >
        <option value="" disabled>Select a template</option>
        {templates.map(t => (
          <option key={t.id} value={t.slug}>
            {t.name} - Roles: {t.config.required_roles.join(', ')}
          </option>
        ))}
      </select>
    </div>
  );
}
```

### Trade reference input

When matching an existing instruction, the user provides the trade reference they received from the counterparty.

```tsx theme={null}
// trade-reference-input.tsx
interface TradeReferenceInputProps {
  value: string;
  onChange: (value: string) => void;
}

export function TradeReferenceInput({ value, onChange }: TradeReferenceInputProps) {
  return (
    <div>
      <label style={{ display: 'block', marginBottom: '4px', fontWeight: 600, fontSize: '14px' }}>
        Trade Reference (optional)
      </label>
      <input
        type="text"
        value={value}
        onChange={e => onChange(e.target.value)}
        placeholder="e.g. KS-abc12345 - leave empty to create new"
        style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db' }}
      />
      <p style={{ fontSize: '12px', color: '#6b7280', marginTop: '4px' }}>
        If matching an existing instruction, enter the trade reference shared by your counterparty.
      </p>
    </div>
  );
}
```

### The complete instruction form

This form collects role, party details, template-driven legs, and optional trade reference. Each template defines which leg types are required - for DvP, that is `asset_delivery` and `payment`.

```tsx theme={null}
// instruction-form.tsx
import { useState } from 'react';
import { useSubmitInstruction, useTemplates } from '@keystoneos/react';
import { TemplateSelector } from './template-selector';
import { TradeReferenceInput } from './trade-reference-input';
import { PendingMatchResult } from './pending-match-result';
import { MatchedResult } from './matched-result';

// Template slug to required leg types. Phase 1: all DvP templates.
// Future: read from TemplateConfig.required_leg_types.
const LEG_TYPES_BY_TEMPLATE: Record<string, string[]> = {
  'cross_platform_dvp': ['asset_delivery', 'payment'],
  'single_platform_dvp': ['asset_delivery', 'payment'],
};

const LEG_TYPE_LABELS: Record<string, string> = {
  asset_delivery: 'Asset Delivery',
  payment: 'Payment',
};

function deriveDirection(role: string, legType: string): 'deliver' | 'receive' {
  if ((role === 'seller' && legType === 'asset_delivery') ||
      (role === 'buyer' && legType === 'payment')) {
    return 'deliver';
  }
  return 'receive';
}

interface LegState {
  instrumentId: string;
  quantity: string;
  chainId: number;
}

interface FormData {
  role: 'seller' | 'buyer';
  partyReference: string;
  partyName: string;
  walletAddress: string;
  templateSlug: string;
  tradeReference: string;
  legs: Record<string, LegState>;
}

function buildInitialLegs(legTypes: string[]): Record<string, LegState> {
  return Object.fromEntries(
    legTypes.map(lt => [lt, { instrumentId: '', quantity: '', chainId: 11155111 }])
  );
}

export function InstructionForm() {
  const defaultLegTypes = ['asset_delivery', 'payment'];
  const [form, setForm] = useState<FormData>({
    role: 'seller',
    partyReference: '',
    partyName: '',
    walletAddress: '',
    templateSlug: '',
    tradeReference: '',
    legs: buildInitialLegs(defaultLegTypes),
  });
  const { submit, result, isSubmitting, error, reset } = useSubmitInstruction();

  const legTypes = LEG_TYPES_BY_TEMPLATE[form.templateSlug] ?? defaultLegTypes;

  const handleTemplateChange = (slug: string) => {
    const newLegTypes = LEG_TYPES_BY_TEMPLATE[slug] ?? defaultLegTypes;
    setForm(prev => ({ ...prev, templateSlug: slug, legs: buildInitialLegs(newLegTypes) }));
  };

  const updateLeg = (legType: string, field: keyof LegState, value: string | number) => {
    setForm(prev => ({
      ...prev,
      legs: { ...prev.legs, [legType]: { ...prev.legs[legType], [field]: value } },
    }));
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    await submit({
      role: form.role,
      party: {
        external_reference: form.partyReference,
        name: form.partyName,
        wallet_address: form.walletAddress,
      },
      legs: legTypes.map(lt => ({
        leg_type: lt,
        instrument_id: form.legs[lt].instrumentId,
        quantity: form.legs[lt].quantity,
        direction: deriveDirection(form.role, lt),
        chain_id: form.legs[lt].chainId,
      })),
      templateSlug: form.templateSlug,
      timeoutAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
      tradeReference: form.tradeReference || undefined,
    });
  };

  const handleReset = () => {
    reset();
    setForm({
      role: 'seller',
      partyReference: '',
      partyName: '',
      walletAddress: '',
      templateSlug: '',
      tradeReference: '',
      legs: buildInitialLegs(defaultLegTypes),
    });
  };

  // Show result screens after submission
  if (result?.status === 'pending_match') {
    return <PendingMatchResult result={result} onReset={handleReset} />;
  }

  if (result?.status === 'matched') {
    return <MatchedResult result={result} />;
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: '600px', display: 'flex', flexDirection: 'column', gap: '20px' }}>
      <h2 style={{ fontSize: '20px', fontWeight: 700 }}>Submit Settlement Instruction</h2>

      {/* Role selection */}
      <div>
        <label style={{ display: 'block', marginBottom: '4px', fontWeight: 600, fontSize: '14px' }}>Role</label>
        <div style={{ display: 'flex', gap: '12px' }}>
          {(['seller', 'buyer'] as const).map(role => (
            <label key={role} style={{ display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer' }}>
              <input
                type="radio"
                name="role"
                value={role}
                checked={form.role === role}
                onChange={() => setForm(prev => ({ ...prev, role }))}
              />
              <span style={{ textTransform: 'capitalize' }}>{role}</span>
            </label>
          ))}
        </div>
      </div>

      {/* Party details */}
      <fieldset style={{ border: '1px solid #e5e7eb', borderRadius: '8px', padding: '16px' }}>
        <legend style={{ fontWeight: 600, fontSize: '14px', padding: '0 8px' }}>Party Details</legend>
        <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
          <div>
            <label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>Account Reference</label>
            <input
              type="text"
              value={form.partyReference}
              onChange={e => setForm(prev => ({ ...prev, partyReference: e.target.value }))}
              placeholder="e.g. KSFI-II-4401"
              required
              style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db' }}
            />
          </div>
          <div>
            <label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>Display Name</label>
            <input
              type="text"
              value={form.partyName}
              onChange={e => setForm(prev => ({ ...prev, partyName: e.target.value }))}
              placeholder="e.g. Securitize Fund I"
              style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db' }}
            />
          </div>
          <div>
            <label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>Wallet Address</label>
            <input
              type="text"
              value={form.walletAddress}
              onChange={e => setForm(prev => ({ ...prev, walletAddress: e.target.value }))}
              placeholder="e.g. 0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18"
              required
              style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db', fontFamily: 'monospace', fontSize: '13px' }}
            />
          </div>
        </div>
      </fieldset>

      {/* Legs - template driven */}
      <fieldset style={{ border: '1px solid #e5e7eb', borderRadius: '8px', padding: '16px' }}>
        <legend style={{ fontWeight: 600, fontSize: '14px', padding: '0 8px' }}>
          Settlement Legs
        </legend>
        <p style={{ fontSize: '12px', color: '#6b7280', marginBottom: '16px' }}>
          Each leg has its own token address and chain. Direction is auto-derived from your role.
        </p>
        {legTypes.map((lt, index) => (
          <div key={lt}>
            {index > 0 && <hr style={{ border: 'none', borderTop: '1px dashed #e5e7eb', margin: '16px 0' }} />}
            <div style={{ marginBottom: '12px' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
                <span style={{
                  padding: '2px 10px',
                  borderRadius: '9999px',
                  fontSize: '11px',
                  fontWeight: 600,
                  textTransform: 'uppercase',
                  background: lt === 'asset_delivery' ? 'rgba(22,163,74,0.1)' : 'rgba(37,99,235,0.1)',
                  color: lt === 'asset_delivery' ? '#16a34a' : '#2563eb',
                  border: lt === 'asset_delivery' ? '1px solid rgba(22,163,74,0.3)' : '1px solid rgba(37,99,235,0.3)',
                }}>
                  {LEG_TYPE_LABELS[lt] ?? lt}
                </span>
                <span style={{ fontSize: '12px', color: '#6b7280', textTransform: 'capitalize' }}>
                  {deriveDirection(form.role, lt) === 'deliver'
                    ? `${form.role} delivers to ${form.role === 'seller' ? 'buyer' : 'seller'}`
                    : `${form.role === 'seller' ? 'buyer' : 'seller'} delivers to ${form.role}`}
                </span>
              </div>
              <div style={{ display: 'flex', gap: '12px' }}>
                <div style={{ flex: 1 }}>
                  <label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>Token Address</label>
                  <input
                    type="text"
                    value={form.legs[lt]?.instrumentId ?? ''}
                    onChange={e => updateLeg(lt, 'instrumentId', e.target.value)}
                    placeholder="0x..."
                    required
                    style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db', fontFamily: 'monospace', fontSize: '13px' }}
                  />
                </div>
                <div style={{ flex: 1 }}>
                  <label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>Quantity</label>
                  <input
                    type="text"
                    value={form.legs[lt]?.quantity ?? ''}
                    onChange={e => updateLeg(lt, 'quantity', e.target.value)}
                    placeholder="e.g. 12000000"
                    required
                    style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db', fontFamily: 'monospace' }}
                  />
                </div>
                <div style={{ width: '160px' }}>
                  <label style={{ display: 'block', marginBottom: '4px', fontSize: '13px' }}>Chain ID</label>
                  <select
                    value={form.legs[lt]?.chainId ?? 11155111}
                    onChange={e => updateLeg(lt, 'chainId', Number(e.target.value))}
                    style={{ width: '100%', padding: '8px 12px', borderRadius: '6px', border: '1px solid #d1d5db' }}
                  >
                    <option value={1}>Ethereum (1)</option>
                    <option value={137}>Polygon (137)</option>
                    <option value={43114}>Avalanche (43114)</option>
                    <option value={11155111}>Sepolia (11155111)</option>
                  </select>
                </div>
              </div>
            </div>
          </div>
        ))}
      </fieldset>

      {/* Template and trade reference */}
      <TemplateSelector value={form.templateSlug || undefined} onChange={handleTemplateChange} />
      <TradeReferenceInput value={form.tradeReference} onChange={val => setForm(prev => ({ ...prev, tradeReference: val }))} />

      {/* Submit */}
      {error && (
        <div style={{ padding: '12px', backgroundColor: '#fee2e2', color: '#991b1b', borderRadius: '6px', fontSize: '14px' }}>
          {error}
        </div>
      )}

      <button
        type="submit"
        disabled={isSubmitting || !form.templateSlug}
        style={{
          padding: '12px 24px',
          backgroundColor: isSubmitting ? '#9ca3af' : '#2DD4A8',
          color: '#fff',
          border: 'none',
          borderRadius: '8px',
          fontWeight: 600,
          cursor: isSubmitting ? 'not-allowed' : 'pointer',
        }}
      >
        {isSubmitting ? 'Submitting...' : 'Submit Instruction'}
      </button>
    </form>
  );
}
```

## Handling results

### Pending match - share the trade reference

When the submission returns `pending_match`, the counterparty has not submitted yet. Show the trade reference prominently so the user can share it.

```tsx theme={null}
// pending-match-result.tsx
import { useState } from 'react';

interface PendingMatchResultProps {
  result: {
    id: string;
    tradeReference: string;
    status: string;
    role: string;
    createdAt: string;
  };
  onReset: () => void;
}

export function PendingMatchResult({ result, onReset }: PendingMatchResultProps) {
  const [copied, setCopied] = useState(false);

  const copyReference = async () => {
    await navigator.clipboard.writeText(result.tradeReference);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };

  return (
    <div style={{ maxWidth: '500px', textAlign: 'center', padding: '40px 24px' }}>
      <div style={{ fontSize: '48px', marginBottom: '16px' }}>
        <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" strokeWidth="2">
          <circle cx="12" cy="12" r="10" />
          <line x1="12" y1="8" x2="12" y2="12" />
          <line x1="12" y1="16" x2="12.01" y2="16" />
        </svg>
      </div>
      <h2 style={{ fontSize: '20px', fontWeight: 700, marginBottom: '8px' }}>Waiting for Counterparty</h2>
      <p style={{ color: '#6b7280', marginBottom: '24px' }}>
        Your {result.role} instruction has been submitted. Share the trade reference below with
        your counterparty so they can submit their matching instruction.
      </p>

      <div style={{
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        gap: '8px',
        padding: '16px',
        backgroundColor: '#f9fafb',
        borderRadius: '8px',
        border: '1px solid #e5e7eb',
        marginBottom: '24px',
      }}>
        <code style={{ fontSize: '20px', fontWeight: 700, letterSpacing: '0.05em' }}>
          {result.tradeReference}
        </code>
        <button
          onClick={copyReference}
          style={{
            padding: '6px 12px',
            backgroundColor: copied ? '#dcfce7' : '#e5e7eb',
            color: copied ? '#166534' : '#374151',
            border: 'none',
            borderRadius: '6px',
            cursor: 'pointer',
            fontSize: '13px',
          }}
        >
          {copied ? 'Copied' : 'Copy'}
        </button>
      </div>

      <p style={{ fontSize: '13px', color: '#9ca3af' }}>
        Instruction ID: {result.id}
        <br />
        Submitted: {new Date(result.createdAt).toLocaleString()}
      </p>

      <button
        onClick={onReset}
        style={{
          marginTop: '24px',
          padding: '10px 20px',
          backgroundColor: 'transparent',
          border: '1px solid #d1d5db',
          borderRadius: '6px',
          cursor: 'pointer',
        }}
      >
        Submit Another Instruction
      </button>
    </div>
  );
}
```

### Matched - navigate to settlement

When the submission returns `matched`, both sides are in. Navigate directly to the settlement detail page.

```tsx theme={null}
// matched-result.tsx
interface MatchedResultProps {
  result: {
    id: string;
    tradeReference: string;
    status: string;
    settlementId: string | null;
  };
}

export function MatchedResult({ result }: MatchedResultProps) {
  return (
    <div style={{ maxWidth: '500px', textAlign: 'center', padding: '40px 24px' }}>
      <div style={{ fontSize: '48px', marginBottom: '16px' }}>
        <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#16a34a" strokeWidth="2">
          <circle cx="12" cy="12" r="10" />
          <polyline points="16 10 11 15 8 12" />
        </svg>
      </div>
      <h2 style={{ fontSize: '20px', fontWeight: 700, marginBottom: '8px' }}>Settlement Created</h2>
      <p style={{ color: '#6b7280', marginBottom: '24px' }}>
        Both parties have submitted. The settlement is now being processed.
      </p>

      <div style={{ padding: '16px', backgroundColor: '#f0fdf4', borderRadius: '8px', border: '1px solid #bbf7d0', marginBottom: '24px' }}>
        <p style={{ fontSize: '13px', color: '#166534' }}>
          Trade Reference: <strong>{result.tradeReference}</strong>
        </p>
        <p style={{ fontSize: '13px', color: '#166534', marginTop: '4px' }}>
          Settlement ID: <strong>{result.settlementId}</strong>
        </p>
      </div>

      <a
        href={`/settlements/${result.settlementId}`}
        style={{
          display: 'inline-block',
          padding: '12px 24px',
          backgroundColor: '#2DD4A8',
          color: '#fff',
          borderRadius: '8px',
          textDecoration: 'none',
          fontWeight: 600,
        }}
      >
        View Settlement
      </a>
    </div>
  );
}
```

## Realistic example flow

Here is how a real trade looks end-to-end:

**1. Seller at Securitize submits both legs:**

```tsx theme={null}
await submit({
  role: 'seller',
  party: {
    external_reference: 'KSFI-II-4401',
    name: 'Securitize Fund I',
    wallet_address: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18',
  },
  legs: [
    {
      leg_type: 'asset_delivery',
      instrument_id: '0x1234...abcd',   // OUSG token contract
      quantity: '12000000',
      direction: 'deliver',              // seller delivers the asset
      chain_id: 1,
    },
    {
      leg_type: 'payment',
      instrument_id: '0xA0b86...6EB48',  // USDC contract
      quantity: '12120000000000',
      direction: 'receive',              // seller receives payment
      chain_id: 1,
    },
  ],
  templateSlug: 'cross_platform_dvp',
  timeoutAt: '2026-04-02T15:00:00Z',
});
// Result: { status: 'pending_match', tradeReference: 'KS-abc12345', ... }
```

**2. Seller shares `KS-abc12345` with buyer via email or OMS.**

**3. Buyer at Ondo Capital submits with the trade reference:**

```tsx theme={null}
await submit({
  role: 'buyer',
  party: {
    external_reference: 'ONDO-CAP-8812',
    name: 'Ondo Capital',
    wallet_address: '0x8ba1f109551bD432803012645Hac136c9E2aB7f1',
  },
  legs: [
    {
      leg_type: 'asset_delivery',
      instrument_id: '0x1234...abcd',   // same OUSG token contract
      quantity: '12000000',
      direction: 'receive',              // buyer receives the asset
      chain_id: 1,
    },
    {
      leg_type: 'payment',
      instrument_id: '0xA0b86...6EB48',  // same USDC contract
      quantity: '12120000000000',
      direction: 'deliver',              // buyer delivers payment
      chain_id: 1,
    },
  ],
  templateSlug: 'cross_platform_dvp',
  timeoutAt: '2026-04-02T15:00:00Z',
  tradeReference: 'KS-abc12345',
});
// Result: { status: 'matched', settlementId: '550e8400-e29b-41d4-a716-446655440000', ... }
```

**4. Settlement is created automatically.** The match response carries the new `settlement_id`; the settlement starts at `INSTRUCTED` and the engine begins compliance checks. The first webhook both platforms receive is `settlement.state.awaiting_deposits` once compliance clears (or `settlement.state.compliance_checking` if a party is flagged).

## Error handling

| Error                             | What happened                        | How to handle                                        |
| --------------------------------- | ------------------------------------ | ---------------------------------------------------- |
| `TEMPLATE_NOT_FOUND`              | Invalid template slug                | Check available templates with `useTemplates()`      |
| `TRADE_REFERENCE_NOT_FOUND`       | The trade reference does not exist   | Verify the reference with the counterparty           |
| `TRADE_REFERENCE_ALREADY_MATCHED` | Both sides already submitted         | Check if settlement already exists                   |
| `ROLE_ALREADY_TAKEN`              | This role already has an instruction | The counterparty may have submitted as the same role |
| `VALIDATION_ERROR`                | Missing or invalid fields            | Check form validation before submission              |

## Next steps

<CardGroup cols={2}>
  <Card title="Settlement Dashboard" icon="table" href="/elements/guides/settlement-dashboard">
    Build a dashboard to monitor all your settlements.
  </Card>

  <Card title="Real-time Tracking" icon="signal" href="/elements/guides/realtime-tracking">
    Track settlement progress with live updates after matching.
  </Card>

  <Card title="useSubmitInstruction Reference" icon="code" href="/hooks/use-submit-instruction">
    Full API reference for the useSubmitInstruction hook.
  </Card>

  <Card title="Bilateral Instructions Guide" icon="book" href="/guides/bilateral-instructions">
    Server-side guide for bilateral instruction flow using the SDK.
  </Card>
</CardGroup>
