Docs menu

Backend integrators: your validator, your party, your key

Treasury desks, bots, and services execute Synfin swaps from Node with no browser and no wallet vendor: your party lives on YOUR participant, your key signs every write, and Synfin never holds either. Quote and plan are the same keyed REST calls as everyone else; execution rides the same published adapter seam.

The model: self-hosted

You run your own Canton validator and your external party lives on your participant. This is the supported and documented path. Synfin does not offer hosted parties for now: hosting your party would put us in a custody-adjacent role we deliberately avoid, and self-hosting is better for you anyway; among other things you control the packages on your own participant (see atomic fees below). If a lighter tier ever exists it will be announced; nothing is promised today.

Participant requirements

  • A Canton validator on mainnet with the JSON Ledger API v2 reachable by your backend, and a ledger API user your backend authenticates as (the userId in every submission; splice validators commonly use administrator).
  • An external party allocated on your participant whose Ed25519 key YOUR backend holds. Allocation is a one-time operation: /v2/parties/external/generate-topology, sign the returned multi-hash with your key, then /v2/parties/external/allocate. Your validator tooling or the Canton wallet SDK can do the same; the raw endpoints are what our own mainnet canaries used.
  • Hard requirement: vet the registrar's packages for every asset you intend to receive, BEFORE any swap. Every token is implemented by its registrar's Daml packages (USDCx: Digital Asset's Utility packages, utility-registry-app-v0, utility-registry-holding-v0 and their dependencies, at the versions the registrar currently runs). Canton requires the RECEIVING party's participant to have vetted those packages, so a venue cannot even submit a payout to a party hosted on a participant that lacks them. That failure is INVISIBLE to the receiver: the submission is rejected on the venue's side, nothing is sequenced, nothing reaches your participant or any explorer, and your deposit stays accepted while the payout never comes. Measured on our own mainnet validator: three accepted deposits were never paid for exactly this reason (docs/canary/2026-09-17-tradecraft-payout-package-gap.md). Check before your first swap: POST /v2/interactive-submission/preferred-packages on your participant for each output package name must resolve, not return PACKAGE_NAMES_NOT_FOUND. Obtain the packages from the registrar's publisher, under their terms; they are node artifacts, not something to commit to your repository.
  • Receive preapprovals for payout assets. Hold one where the asset's registrar offers a creation path. How a venue delivers to a receiver WITHOUT one is not yet measured for a self-custodied party: the 2026-07-22 run that appeared to show "no delivery without a preapproval" is confounded by the missing packages above, and a receiver without a USDCx preapproval, hosted on a participant that HAS the packages, was paid as a two-step offer on 2026-07-16. Until re-measured on a vetted participant, treat pairs whose output you cannot preapprove as not-yet-receivable.
  • Optional, your choice: the CIP-0112 batching package (splice-util-token-standard-wallet 1.1.0 or later) uploaded to your participant. Without it everything works and fee collection degrades fee-less, exactly like the browser wallets today. With it, when fee collection turns on, your swaps can carry the deposit and the fee in ONE atomic transaction (the standing-utility pattern below). Because you control your own participant, you do not wait for any wallet vendor to install it: backend integrators can be the first segment where atomic fee collection works day one.

One rule the platform enforces: single root

Canton rejects multi-root transactions for external parties (proven on mainnet, twice). The backend signer therefore exposes no way to submit more than one command: every submission is exactly one exercise. Batching is done the standard way instead: create a standing BatchingUtility once (one transaction), then each batch is ONE exercise of it carrying multiple token-standard actions inside. Same seam, same single root.

The code: one signer, the same adapter

import { createClient, executePlan, track } from '@synfin/client';
import { createCip0103WalletAdapter, MAINNET_REGISTRY_BASE_URL } from '@synfin/wallet-partylayer/cip0103';
import { createBackendSigner } from '@synfin/wallet-partylayer/backend';
import { readFileSync } from 'node:fs';

const signer = createBackendSigner({
  participant: {
    baseUrl: process.env.PARTICIPANT_JSON_API,   // your validator
    token: process.env.PARTICIPANT_TOKEN,        // your ledger API auth
    userId: 'administrator',
  },
  party: process.env.BACKEND_PARTY,              // hint::fingerprint
  privateKey: readFileSync('/secure/party-key.pem', 'utf8'),
});

const wallet = createCip0103WalletAdapter({
  ledger: { baseUrl: process.env.PARTICIPANT_JSON_API, token: process.env.PARTICIPANT_TOKEN },
  registry: { baseUrl: MAINNET_REGISTRY_BASE_URL, token: '' },
  signer,
  instrumentAdmin,
});

// quote -> plan (keyed REST) -> execute -> track, unchanged from every
// other integrator:
const synfin = createClient({ apiKey: process.env.SYNFIN_API_KEY });
const plan = await synfin.createPlan({ /* ... */ });
const result = await executePlan({ plan, wallet });

Under the hood the signer does exactly what our mainnet canaries proved: prepare on your participant, a raw Ed25519 signature over the prepared-transaction hash with your key, execute, and the update id resolved honestly from your own update stream. Node 18 or later; no dependencies beyond the package.

Key custody, plainly

  • The key is a PKCS8 PEM your backend loads itself (file with tight permissions, or wrapped by your KMS and decrypted into memory). The signer accepts the PEM string or a Node KeyObject; it never logs it, never puts it in an error, never sends it anywhere.
  • Never pass keys or tokens as command-line arguments (they leak via the process list) and never commit them. Environment variables or files with restricted permissions.
  • The key signs only what your process asked Synfin to prepare: a deposit your plan specified, bounded by the plan's on-ledger minReceive and deadline. Synfin builds unsigned transactions and never sees the key.

Support status, honestly

Answer-as-tested, like every wallet class: the backend row ships with execute: false until a real funded mainnet swap through the PUBLISHED packages cites its update id. The signer's request wire shapes are pinned to our mainnet-proven canary flow and fully covered by automated tests up to the signing boundary; the funded proof run is a supervised, recorded release step. Want to be the first backend integration proven live? Write info@cayvox.com.