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: receive preapprovals for every payout asset, BEFORE any swap. Measured on mainnet 2026-07-22: a venue payout to a party WITHOUT a receive preapproval was never delivered at all (no on-ledger attempt of any kind; the deposit was accepted and the payout never came). Do not assume a two-step offer arrives. For assets whose registrar exposes no public preapproval path (USDCx today: the DA registrar's supported APIs carry none), venue payout to a self-custodied party is UNPROVEN; treat such pairs as not-yet-receivable until proven. The run record: docs/canary/2026-07-22-backend-proof-run.md.
  • 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.