@synfin/client
v0.1.7
Published
Synfin partner client: get a best-execution quote, plan a route, execute it from your own Canton wallet, and track it to a terminal state. Talks to the hosted Synfin API; holds no keys and drives no ledger.
Maintainers
Readme
@synfin/client
The Synfin partner client. Get a best-execution quote across Canton venues, plan a route, execute it from your own wallet, and track it to a terminal state. Synfin holds no keys and drives no ledger: the quote and the plan come from the hosted API, and your wallet signs and sends.
- Docs: https://synfin.xyz/docs
- SDK guide: https://synfin.xyz/docs/sdk
Status: v0. The API surface may evolve with design-partner feedback before a 1.0. Pin an exact version and watch the changelog.
What is live today (the honest state)
- Quote and plan are live against the hosted API. Quotes aggregate the Canton venues; Tradecraft is the executable venue today (others appear in the quote for comparison but are not yet plan-buildable).
- Execution is non-custodial from your own wallet. You bring a
WalletAdapteron the taker's party; Synfin holds no keys. - Fees are DISCLOSED, not yet COLLECTED on-chain. Your plan's
clientFeesshows the service fee and your integrator split so you can display them, but on-ledger fee collection is gated behind a flag that is off today (FEE_COLLECTION_ENABLED = false). When it turns on, the fee rides the swap as an atomic CIP-0112 batch leg — and it is wallet- dependent: a wallet whose Canton participant lacks the batching package degrades the swap to fee-less honestly (never a broken swap, never a silent charge). Loop's participant is pending that package upload; other wallets that have it collect atomically. Until the flag flips, every swap is fee-less regardless. - Guaranteed minimums are enforced on-ledger.
minReceiverides the deposit memo; a bad quote aborts and your funds never leave the wallet.
Install
npm install @synfin/clientRequires Node 18+ (built-in fetch) or any modern browser. Zero runtime
dependencies.
The five-step journey
1. Get a key
Create a free API key at portal.synfin.xyz. It carries your rate limit and your fee configuration. Then:
import { createClient } from '@synfin/client';
const synfin = createClient({ apiKey: process.env.SYNFIN_API_KEY! });2. Quote, with your fee
A keyed quote returns each venue's net, ranked best-first, plus your disclosed
clientFees — the 15 bps Synfin service fee and your integrator split (your
feeBps, capped, split 80/20 with Synfin). These are DISCLOSED for you to
show; on-ledger collection is flag-gated off today (see "What is live today").
const quote = await synfin.getQuote({
from: 'CC',
to: 'USDCx',
amount: '100',
feeBps: 30, // your integrator fee (0 to the cap)
feeRecipient: 'yourparty::1220...', // where your fee settles
});
const best = quote.venues.find((v) => v.available);
console.log(best?.venueId, best?.net, best?.clientFees?.userReceives);3. Plan
Ask the API for a one-call execution plan for the venue you chose. The server
computes the memo floor (minReceive) and the fee amounts; you never do. Pass a
stable idempotencyKey so retries return the same plan.
const plan = await synfin.createPlan({
from: 'CC',
to: 'USDCx',
amount: '100',
venueId: best!.venueId,
takerParty: 'yourparty::1220...',
idempotencyKey: crypto.randomUUID(),
feeBps: 30,
feeRecipient: 'yourparty::1220...',
});4. Execute, from your own wallet
You provide a WalletAdapter that acts on the taker's own party (a test harness
implements it for tests; a Loop-class wallet implements it in production).
executePlan drives it through the plan steps.
import { executePlan, type WalletAdapter } from '@synfin/client';
const wallet: WalletAdapter = {
sendDeposit: (step) => myWallet.createTransferOffer(step),
lockFeeEscrow: () => Promise.resolve(null),
withdrawDeposit: (id) => myWallet.withdraw(id),
depositActive: (id) => myWallet.offerActive(id),
observePayout: ({ instrument, sinceIso }) =>
myWallet.received(instrument, sinceIso),
};
const handle = await executePlan(plan, {
wallet,
hooks: { onStatus: (s) => console.log(s.status, s.note) },
});5. Track
Observe the taker's own ledger view and derive the partner status until it is
terminal (COMPLETED, SLIPPAGE_FAILED, REFUNDED, or ABORTED).
import { isPartnerTerminal, track } from '@synfin/client';
let state = await track(handle, { wallet });
while (!isPartnerTerminal(state.status)) {
await new Promise((r) => setTimeout(r, 3000));
state = await track(handle, { wallet });
}
console.log('final:', state.status, state.payoutAmount);Errors
Non-2xx API responses throw a SynfinApiError with the HTTP status and the
machine-readable code:
import { SynfinApiError } from '@synfin/client';
try {
await synfin.getQuote({ from: 'CC', to: 'USDCx', amount: '100' });
} catch (err) {
if (err instanceof SynfinApiError) console.error(err.status, err.code, err.message);
}What this package is not
It talks only to the hosted Synfin API and your wallet. It contains no venue adapter code, no fee-schema logic, and no server internals: pricing and the memo floor are computed server-side and disclosed to you, never recomputed here.
License
MIT. Copyright Cayvox Labs.
