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'.
-
executePlanrefuses an expired plan, performs thevenue-depositstep (and thefee-escrow-lockstep when the plan collects fees), and emitsINITIATED. -
trackreads the deposit offer's state and the payout through your wallet adapter and derives the lifecycle state. OnCOMPLETEDit releases the fee escrow; onSLIPPAGE_FAILEDit 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.
| method | contract |
|---|---|
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.