Skip to main content
Platforms can call the SettlementCoordinator and KeystoneEscrow contracts directly instead of going through the KeyStone REST API. This gives maximum control and removes dependency on KeyStone’s service layer for critical operations.

Integration levels

LevelInstruction matchingDeposits / confirmationsReads / notifications
REST onlyKeyStone APIKeyStone API (signs via platform’s custody)KeyStone API + webhooks
HybridKeyStone APIPlatform calls contracts directly (via Fireblocks)KeyStone API + webhooks
Direct contract onlyNot used (terms agreed out-of-band)Platform calls contracts directlyPlatform runs own indexer
Most platforms start with REST only and move toward hybrid as they scale. Direct contract-only is for platforms with their own infrastructure that want to treat KeyStone as pure on-chain protocol.

Creating a settlement on-chain

If you are bypassing the REST API entirely, call createSettlement() on the SettlementCoordinator directly:
const { ethers } = require("ethers");

const coordinator = new ethers.Contract(
  COORDINATOR_ADDRESS,
  COORDINATOR_ABI,
  signer
);

// Convert UUID to bytes32 (16 bytes UUID + 16 bytes zero padding)
const settlementId = ethers.zeroPadValue(
  "0x" + uuid.replace(/-/g, ""),
  32
);

// Define state machine
const transitions = [
  [0, 1],  // INSTRUCTED -> COMPLIANCE_CHECKING
  [1, 2],  // COMPLIANCE_CHECKING -> COMPLIANCE_CLEARED
  [2, 3],  // COMPLIANCE_CLEARED -> AWAITING_DEPOSITS
  [3, 4],  // AWAITING_DEPOSITS -> EXECUTING_SWAP
  [4, 5],  // EXECUTING_SWAP -> SETTLED
  [5, 6],  // SETTLED -> FINALIZED
  [1, 7],  // COMPLIANCE_CHECKING -> ROLLED_BACK
  [3, 7],  // AWAITING_DEPOSITS -> ROLLED_BACK
];

// Define gates
const gates = [
  [2, 1],  // COMPLIANCE_CLEARED requires COMPLIANCE gate
  [3, 2],  // AWAITING_DEPOSITS requires ALL_DEPOSITS gate
];

// Party hashes
const partyHashes = [
  ethers.solidityPackedKeccak256(
    ["bytes32", "address"],
    [sellerIdBytes32, sellerWalletAddress]
  ),
  ethers.solidityPackedKeccak256(
    ["bytes32", "address"],
    [buyerIdBytes32, buyerWalletAddress]
  ),
];

const timeoutAt = Math.floor(Date.now() / 1000) + 86400; // 24 hours

const tx = await coordinator.createSettlement(
  settlementId,
  0,           // initial state index
  transitions,
  gates,
  partyHashes,
  timeoutAt
);
await tx.wait();
createSettlement() is permissionless. Gas cost is the only barrier. A settlement with no deposits is harmless since it simply times out.

Depositing to escrow

After the settlement is registered and compliance is cleared, parties deposit to the escrow contract:
const escrow = new ethers.Contract(
  ESCROW_ADDRESS,
  ESCROW_ABI,
  partySigner
);

// First, approve the escrow contract to spend tokens
const token = new ethers.Contract(tokenAddress, ERC20_ABI, partySigner);
await (await token.approve(ESCROW_ADDRESS, amount)).wait();

// Then deposit
const tx = await escrow.depositLeg(settlementId, legIndex);
await tx.wait();
The escrow contract validates that msg.sender matches the registered depositor for this leg. Only the correct party can deposit for each leg.

Monitoring events

Instead of using KeyStone webhooks, you can monitor contract events directly:
const provider = new ethers.JsonRpcProvider(RPC_URL);

// Listen for deposit events
escrow.on("LegDeposited", (settlementId, legIndex, depositor, amount) => {
  console.log(`Leg ${legIndex} deposited: ${amount} from ${depositor}`);
});

// Listen for state transitions
coordinator.on("StateTransition", (settlementId, fromState, toState, evidenceHash) => {
  console.log(`Settlement transitioned: ${fromState} -> ${toState}`);
});

// Listen for settlement completion
escrow.on("SettlementExecuted", (settlementId) => {
  console.log(`Settlement ${settlementId} executed`);
});

// Listen for timeouts
coordinator.on("SettlementTimedOut", (settlementId) => {
  console.log(`Settlement ${settlementId} timed out`);
});

Triggering timeout

If a settlement is past its deadline, anyone can trigger the timeout:
// Check if timeout is possible
const settlement = await coordinator.getSettlement(settlementId);
const now = Math.floor(Date.now() / 1000);

if (now >= settlement.timeoutAt) {
  const tx = await coordinator.timeout(settlementId);
  await tx.wait();
  // Deposits are returned to original depositors
}
This is permissionless. Even if KeyStone’s services are down, any party can recover their funds.

When to use direct integration

Use direct integration when:
  • You have your own custody infrastructure (Fireblocks, MPC wallets)
  • You want to minimize trust in KeyStone’s service layer
  • You need to verify settlement state independently
  • You want to trigger timeouts without waiting for KeyStone
Use the REST API when:
  • You want instruction matching (trade reference generation, bilateral matching)
  • You prefer REST over blockchain transactions
  • You want webhooks instead of event monitoring
  • You want KeyStone to handle transaction signing
Hybrid approach (most common):
  • Use the REST API for instruction matching and status queries
  • Call contracts directly for deposits (via your custody provider)
  • Use webhooks for notifications but verify against on-chain state

Contract ABIs

Contract ABIs are available in the keystone-smart-contracts repository. See Testnet Addresses for current deployment addresses.