Developers

Payment SDK

Deposit into Anyflo deposit accounts and verify server signatures from TypeScript — multichain, keyless by design.

@anyflo/payment-sdk is the multichain TypeScript client for depositing into Anyflo deposit accounts and verifying the server’s deposit signature. It works on EVM chains and Sui with a single SDK instance — see Supported chains.

Design principles

  • Chain-first — chains use CAIP-2 IDs (eip155:84532 = Base Sepolia, sui:testnet, …). The execution model (evm / sui-move) derives from the chain and is never stated separately.
  • One network per instance — the selected network (mainnet, testnet, devnet, or localnet) determines deployment defaults for every enabled chain. create rejects a chain/network mismatch.
  • Holds no keys, never signs — the SDK builds and simulates transactions; all signing is delegated to a caller-supplied Signer, so private keys never enter the SDK.
  • Dependency injection — clients are always injected (a viem PublicClient per EVM chain, a Sui ClientWithCoreApi); the SDK does not construct its own clients.

Installation

bun add @anyflo/payment-sdk

@mysten/sui and viem are peer dependencies — install them in your project as well.

Supported chains

The following chains are currently supported on testnet and localnet. Each has a CAIP-2 ID and is available through the exported EvmChain or SuiChain constants:

ConstantCAIP-2 IDNetwork
EvmChain.baseSepoliaeip155:84532Testnet
EvmChain.polygonAmoyeip155:80002Testnet
EvmChain.arbitrumSepoliaeip155:421614Testnet
SuiChain.suiTestnetsui:testnetTestnet
EvmChain.hardhateip155:31337Localnet
SuiChain.suiLocalnetsui:localnetLocalnet

Localnet requires your own deployment addresses as overrides.

More chains and networks become available as deployments go live.

Initializing the SDK

Each entry in the configuration object enables a chain. Its value contains the injected client and any deployment fields you want to override.

import {
  EvmChain,
  Network,
  PaymentSDK,
  SuiChain,
} from "@anyflo/payment-sdk";
import { createPublicClient, http } from "viem";
import { baseSepolia } from "viem/chains";

const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });

const sdk = PaymentSDK.create(Network.testnet, {
  [EvmChain.baseSepolia]: { publicClient },
  [SuiChain.suiTestnet]: { client }, // @mysten/sui ClientWithCoreApi
});

On testnet, per-network defaults fill depositAccountAddress (EVM) and packageId/registryId (Sui), so only the clients are required. On networks without defaults, every deployment field must be passed as an override. create checks that at least one chain is enabled, each enabled chain has a client, addresses and IDs are well formed, and each chain matches the selected network. It rejects invalid configuration immediately.

Payment instructions

Every deposit uses an OrderInstructions object — the server-signed payment instructions you receive from the Anyflo backend after confirming an order. The server constructs the order and signs it; that signature is what authorizes the deposit, and the contracts verify it on chain.

interface OrderInstructions {
  order: PaymentOrder;
  payments: Payment[];
  signature: string; // the server's authorization signature over the order
}

PaymentOrder — the order being paid

FieldMeaning
idOrder UUID (becomes a 16-byte ID on chain; a replayed ID aborts before any transfer)
senderAddrWho pays — must equal the Signer’s address during execution, enforced on chain
solverAddrSolver payout address
feeAddrFee payout address — required on Sui, ignored on EVM
directDepositOptional, defaults to false — EVM-only; part of the EVM signed data
expiresAtUnix timestamp in seconds; both chains reject the deposit after expiry

Payment — one token leg of the order

FieldMeaning
tokenERC-20 address (EVM) or fully qualified coin type, e.g. 0x2::sui::SUI (Sui)
solverAmountAmount forwarded to the solver, in base units (bigint)
feeAnyflo fee forwarded to feeAddr, in base units (bigint)

Amounts are always expressed in base units (bigint), never as floating-point values.

Per-VM differences

  • EVM — an order may carry several payments (token approvals are added as needed); directDeposit is part of the signed data; feeAddr is ignored.
  • Sui — an order carries exactly one payment and order.feeAddr is required; directDeposit is not used.

Verify a server signature

const ok: boolean = await sdk.verifyDepositSignature({
  chain: EvmChain.baseSepolia,
  instructions, // server-signed OrderInstructions
});

verifyDepositSignature returns false when the signature is not from an authorized signer. A malformed signature throws instead of returning false.

Deposit: prepare, then execute

Deposits are two-phase: prepareDeposit builds the transactions for the chain, then execute signs and sends them with a caller-supplied Signer.

EVM

// Zero or more token approvals + the deposit calldata.
const batch = await sdk.prepareDeposit({
  chain: EvmChain.baseSepolia,
  instructions,
});

const evmSigner: Signer<"evm"> = {
  vm: "evm",
  address: account.address,
  signAndSend: async ({ to, data, value }) =>
    walletClient.sendTransaction({ to, data, value }),
};

const hash = await sdk.execute(batch, evmSigner);

Sui

const batch = await sdk.prepareDeposit({
  chain: SuiChain.suiTestnet,
  instructions, // Sui orders: exactly one payment, order.feeAddr required
});

const suiSigner: Signer<"sui-move"> = {
  vm: "sui-move",
  address: keypair.toSuiAddress(),
  signAndSend: async ({ transaction }) => {
    const res = await keypair.signAndExecuteTransaction({
      transaction,
      client,
    });
    const tx =
      res.$kind === "Transaction" ? res.Transaction : res.FailedTransaction;
    if (!tx.status.success) {
      throw new Error(tx.status.error?.message ?? "transaction failed");
    }
    return tx.digest;
  },
};

const digest = await sdk.execute(batch, suiSigner);

The signer’s address must equal instructions.order.senderAddr — enforced by the SDK during execution and by the contracts on chain.

What each phase does

EVM prepareDeposit

  1. Verifies the injected publicClient actually serves the configured chain (getChainId()).
  2. For each payment, reads the token’s current allowance: tokens with sufficient allowance are skipped to save gas; otherwise an exact-amount approve(depositAccount, solverAmount + fee) step is emitted — never infinite approvals.
  3. Emits the deposit calldata step carrying the order ID, tokens, amounts, expiry, and the server signature.

EVM execute

  1. Re-verifies the chain ID and that signer.address equals order.senderAddr.
  2. Walks the steps in order, simulating the deposit via eth_call right before it is sent — a reverting deposit fails before gas is spent on it.
  3. Sends each step through the signer, waits for each receipt, and throws on revert. Returns the deposit transaction hash.

Sui prepareDeposit

  1. Verifies the client’s chain identifier against the known genesis digest of the configured chain.
  2. Builds a single PTB calling deposit_account::deposit<T> with coinWithBalance inputs (resolved by the wallet at sign time — which is why the prepared step carries the live Transaction instance), the order fields, and the parsed Ed25519 public key and signature.
  3. Pins the sender: transaction.setSender(order.senderAddr).

Sui execute

  1. Re-verifies the chain identifier and the pinned sender.
  2. Simulates the PTB — the on-chain signature check, expiry, and replay guard all run in simulation.
  3. Delegates to the signer and returns the digest. Finalization (waiting for the transaction and checking status) is the signer’s job.

Failure semantics at a glance

BehaviorEVMSui
Preflighteth_call of the deposit stepsimulateTransaction of the PTB
SubmissionOne transaction per step; waits for each receiptOne PTB submitted through the signer
Failure handlingThrows when a transaction receipt indicates failureSimulation throws on failure; the signer handles failures after submission
Return valueDeposit transaction hashTransaction digest from the signer

Configuration

Chain overrides

Deployment fields use network defaults where available; otherwise, supply them as overrides.

Chain kindRequired clientDeployment fields
EVM (eip155:*)publicClientdepositAccountAddress
Sui (sui:*)clientpackageId, registryId

Deployment defaults

Defaults are available on testnet:

Deployment fieldTestnet value
EVM depositAccountAddress0xfE1A0F2dAE0C173ec82327CC6f9dB0459ba0c691
Base Sepolia, Polygon Amoy, and Arbitrum Sepolia
Sui packageId0xa2d0a7855a8cd09893c7ae51c3f81a0188cfef2ccdda9592ddc75b7eca6475c0
Sui registryId0xb06fbcdb75917aed6a3ebe84ab28376900ec12ec77ece3a2b22b91fefd127c3c

There are no deployment defaults for mainnet, devnet, or localnet. Supply depositAccountAddress for EVM and both packageId and registryId for Sui as overrides.

Custom chain adapters

Every chain is implemented as an adapter behind one interface. sdk.register(...) replaces the built-in adapter for an enabled chain — e.g. an instrumented adapter in tests:

const custom: ChainAdapter<typeof SuiChain.suiTestnet> = {
  chain: SuiChain.suiTestnet,
  prepare: async (instructions) => buildMySteps(instructions),
  execute: async (steps, signer) => myExecute(steps, signer),
  verifyDepositSignature: async (instructions) => myVerify(instructions),
};

sdk.register(custom);

Registering a chain that already has an adapter throws; new chain IDs require extending the SDK itself.