Docs menu

Quickstart

Install one package, get a key, quote a swap in minutes, and execute one from your own Canton wallet within the hour. Every snippet here matches the runnable example at synfin-examples/quickstart-node, so you can clone it and run against the live API.

1. Install

@synfin/client (zero runtime dependencies, Node 18+ or any browser) covers quote and plan. To EXECUTE a swap you also add the reference wallet adapter and its PartyLayer peer (step 4):

npm install @synfin/client
npm install @synfin/wallet-partylayer @partylayer/sdk   # for execute (step 4)

Do not install @synfin/spec, @synfin/adapters, @synfin/router-ref, @synfin/conformance, or @synfin/cli, those are deprecated. Synfin is a hosted API; @synfin/client is how you call it (and @synfin/widget is the optional drop-in UI).

2. Get a key

Create a free key at portal.synfin.xyz. It carries your rate limit and your fee configuration, and is shown once at creation. Design partners can also reach a human at info@cayvox.com. Keep the key server-side; in a browser widget use a dedicated widget key.

3. First quote (minutes)

A keyed quote returns each venue's net receive, ranked best first, plus your disclosed clientFees. Tradecraft is the executable venue today; the other venues appear for comparison but are not yet plan-buildable.

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

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

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 would settle
});

const best = quote.venues.find((v) => v.available);
console.log(best.venueId, best.net, best.clientFees?.userReceives);

Read the response like this: gross is a venue's headline output; net is what arrives after the venue's network fee; best is the highest net. A venue that cannot quote stays in the list as available: false with a rejectionCode. Note CC resolves to its registry name Amulet in responses. The full response shape is in the API reference.

4. First swap (within the hour)

Three paths from here, one seam. A dApp connecting a USER'S wallet follows the PartyLayer path below. A HOSTED wallet (you are the wallet: the party is yours and you sign natively) takes the hosted-wallet path. A BACKEND with its own validator and self-custodied key (treasury desks, bots) takes the backend path. Both non-browser paths rejoin here for the plan-and-track calls, which are identical.

Ask for a one-call plan for the venue you chose. The server computes the memo floor (minReceive) and pins the quote, you never do. Then execute it from your own wallet. If you use a PartyLayer wallet (Loop and others), the reference adapter @synfin/wallet-partylayer gives you a ready WalletAdapter from just the connected client and the taker party, no network identifiers to paste. executePlan drives it through the plan; Synfin holds no keys. (Where partyLayerClient comes from: connect a wallet.)

import { executePlan, track, isPartnerTerminal } from '@synfin/client';
import { createPartyLayerWalletAdapter } from '@synfin/wallet-partylayer';

const plan = await synfin.createPlan({
  from: 'CC', to: 'USDCx', amount: '100',
  venueId: best.venueId,
  takerParty: 'yourparty::1220...',
  idempotencyKey: crypto.randomUUID(), // retries return the SAME plan
});

// minReceive rides the deposit memo and is enforced ON-LEDGER: a bad quote
// aborts and your funds never leave the wallet.
console.log(plan.quoteRef.minReceive, plan.collectsFees, plan.steps.map(s => s.kind));

// Wallet in, adapter out: no CIP-56 code, no ledger reads, and no network
// identifiers. The registry base defaults to mainnet and the deposit admin is
// read from the wallet's own holdings.
const wallet = createPartyLayerWalletAdapter(partyLayerClient, {
  party: 'yourparty::1220...', // the connected taker party
});

const handle = await executePlan(plan, {
  wallet,
  hooks: { onStatus: (s) => console.log(s.status, s.note) },
});

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);

Not on PartyLayer? A hosted or self-signing wallet gets the same adapter from one signer seam, no connect, no popup: Hosted wallets. Beyond that, you can implement the WalletAdapter interface yourself (five methods: sendDeposit, withdrawDeposit, depositActive, observePayout, and the retired lockFeeEscrow no-op) against any Canton wallet. The reference adapter is the fast path; the interface is the escape hatch.

A terminal status is one of COMPLETED, SLIPPAGE_FAILED, REFUNDED, or ABORTED. Which wallets can drive this, and which venues execute, is the support matrix.

The fee reality, up front

Do not ship a fee surprise. The current state, exactly:

  • Fees are DISCLOSED, not COLLECTED on-ledger today. The server flag FEE_COLLECTION_ENABLED is false, so every swap is fee-less right now. Your quote's clientFees and your plan's collectsFees: false say so directly, the plan carries no fee step while the flag is off.
  • When the flag flips, the partner fee is planned and disclosed but not executed by this SDK yet. The fee rides an atomic CIP-0112 batch with the deposit, a wallet-side capability. The WalletAdapter above does not perform that batch, so the fee legs are not SDK-executed even after the flip, until @synfin/client's adapter gains batch capability. That is the current limitation, stated plainly.
  • The atomic path depends on the wallet's Canton participant carrying splice-util-token-standard-wallet. Loop's participant currently does not, so a Loop-signed swap degrades to fee-less: the deposit stands, no fee is collected, and the outcome says so. Never a broken swap for a fee, never a silent charge.

When collection is on, your integrator fee is a separate leg and you keep 100 percent of it; Synfin's flat service fee is its own independent leg, both disclosed in clientFees. How the two legs are built is in Earn with your fee; the user-facing fee page is How fees work.

Run the whole thing

The example repo is the same journey as a real, cloneable project:

git clone https://github.com/cayvox/synfin-examples
cd synfin-examples/quickstart-node
npm install
cp .env.example .env                 # add your SYNFIN_API_KEY
node --env-file=.env src/quote.mjs   # a live quote, no funds move

src/swap.mjs in that repo walks quote → plan → execute → track, using the published @synfin/wallet-partylayer reference adapter. Execution moves real funds on mainnet, so it runs only once you connect a real wallet (see connect a wallet).

Try it before you sign up

You do not need a key to see a live quote: the quote endpoint answers keyless (venue nets, without your fee disclosure). One curl, no account:

curl "https://synfin.xyz/api/quote?from=CC&to=USDCx&amount=100"

The keyed path above adds your disclosed clientFees; a key also unlocks planning and higher rate limits.