Docs menu

SDK execution client

The SDK drives the full journey: quote, plan, execute from the user's own wallet, track to a terminal state. It is published on npm as @synfin/client (MIT). Status, honestly: v0, the API surface may evolve with design-partner feedback before a 1.0, so pin an exact version.

Install

npm install @synfin/client

Node 18+ or any modern browser. Zero runtime dependencies. Get a free API key at portal.synfin.xyz.

Minimal example

import { createClient } from '@synfin/client';

const synfin = createClient({ apiKey: process.env.SYNFIN_API_KEY });

// 1. quote (keyed, so it carries your clientFees), ranked best net first
const quote = await synfin.getQuote({
  from: 'CC', to: 'USDCx', amount: '100',
  feeBps: 30, feeRecipient: 'yourparty::1220...', // your integrator fee
});
const best = quote.venues.find((v) => v.available);

// 2. plan: the server pins the numbers and the memo floor
const plan = await synfin.createPlan({
  from: 'CC', to: 'USDCx', amount: '100',
  venueId: best.venueId,
  takerParty: 'yourparty::1220...',
  idempotencyKey: crypto.randomUUID(),
});

getQuote and createPlan call the hosted API with your key; executePlan and track below are wallet-driven and need no key.

The journey

// 1. quote (GET /api/quote with your fee) and let the user pick
// 2. plan (POST /api/execute/plan) pins the numbers server-side
// 3. execute: YOUR wallet performs the steps
const handle = await executePlan(plan, {
  wallet,                       // your WalletAdapter
  hooks: {
    onStatus: (s) => render(s), // INITIATED, COMPLETED, ...
    onStep: (step, result) => log(step.kind, result),
  },
});

// 4. track: observe the user's own ledger view until terminal
let state = await track(handle, { wallet, hooks });
while (!isPartnerTerminal(state.status)) {
  await sleep(15_000);
  state = await track(handle, { wallet, hooks });
}

From @synfin/client: import { executePlan, track, isPartnerTerminal } from '@synfin/client'.

  • executePlan refuses an expired plan, performs the venue-deposit step (and the fee-escrow-lock step when the plan collects fees), and emits INITIATED.
  • track reads the deposit offer's state and the payout through your wallet adapter and derives the lifecycle state. On COMPLETED it releases the fee escrow; on SLIPPAGE_FAILED it does not.
  • The hooks mirror the pattern wallet developers know from LI.FI's executeRoute: the SDK provides steps and status, your code signs and renders. The SDK never holds keys.

The wallet adapter

You implement one interface against your participant view; the SDK does the rest.

methodcontract
sendDeposit(step)Create the venue deposit (a CIP-56 transfer offer with the plan's memo). Returns the offer's contract id.
lockFeeEscrow(step)Lock the fee legs; return the escrow id, or null when not collecting.
withdrawDeposit(id)Withdraw a still-active offer (user abort).
depositActive(id)Is our offer still open (not accepted, not expired)?
observePayout({instrument, sinceIso})Total the taker RECEIVED in the instrument since the start, from the taker's own transaction history.
releaseFeeEscrow(id)Optional: release the escrow against the observed payout.

Why tracking is client-side

On Canton, contracts are private to their stakeholders. The party that can see the user's deposit offer and incoming payout IS the user's own participant view, which your wallet already has. So v1 derives the state exactly where the evidence lives, from the party's own holdings and transaction history, never from a third-party explorer snapshot. It also means Synfin holds no funds, no keys, and no visibility into your user's ledger. A server-side status endpoint is planned for integrations that want polling without a wallet adapter; it will report the same states.

Embeddable widget

Not writing code against the SDK? @synfin/widget (MIT) is a prebuilt <synfin-widget> custom element: a live best-execution quote across venues, non-custodial. It is shown to your END USER, so by default it shows the net they receive (fees included) and a single neutral fee line, not the fee attribution; add show-fee-breakdown to see the full partner breakdown while integrating. It is the third integration tier after curl and the SDK. Two install paths, both register the same element.

Script tag (no build step)

One self-contained bundle (@synfin/client is inlined, so there is no import map). Works in plain HTML, Vue, Svelte, Angular, Rails, WordPress. Pin a version in production.

<script src="https://unpkg.com/@synfin/widget@0.1.0"></script>
<synfin-widget
  api-key="sk_live_..." fee-bps="30" fee-recipient="you::1220..."
  from="CC" to="USDCx" amount="100"></synfin-widget>

Also on jsDelivr: https://cdn.jsdelivr.net/npm/@synfin/widget@0.1.0.

npm (with a bundler)

npm install @synfin/widget
import '@synfin/widget'; // registers <synfin-widget>
// React: import { SynfinWidget } from '@synfin/widget/react';

The API key rides in the browser by nature; Synfin keys are free, rate-limited, non-custodial, and revocable in one click, so use a dedicated widget key. A subtle "Powered by Synfin" footer link shows by default (set show-attribution="false" to white-label). Theming, events, and every state are documented in the package README.

Execution in v1: "Create a plan" (key-gated) emits a synfin:plan event for your app to execute through its own WalletAdapter (the same one the SDK uses above). In-widget wallet execution is coming once a standard Canton browser wallet adapter exists; the widget holds no keys.