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
userIdin every submission; splice validators commonly useadministrator). -
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-wallet1.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
minReceiveand 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.