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, orlocalnet) determines deployment defaults for every enabled chain.createrejects 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
PublicClientper EVM chain, a SuiClientWithCoreApi); 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:
| Constant | CAIP-2 ID | Network |
|---|---|---|
EvmChain.baseSepolia | eip155:84532 | Testnet |
EvmChain.polygonAmoy | eip155:80002 | Testnet |
EvmChain.arbitrumSepolia | eip155:421614 | Testnet |
SuiChain.suiTestnet | sui:testnet | Testnet |
EvmChain.hardhat | eip155:31337 | Localnet |
SuiChain.suiLocalnet | sui:localnet | Localnet |
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
| Field | Meaning |
|---|---|
id | Order UUID (becomes a 16-byte ID on chain; a replayed ID aborts before any transfer) |
senderAddr | Who pays — must equal the Signer’s address during execution, enforced on chain |
solverAddr | Solver payout address |
feeAddr | Fee payout address — required on Sui, ignored on EVM |
directDeposit | Optional, defaults to false — EVM-only; part of the EVM signed data |
expiresAt | Unix timestamp in seconds; both chains reject the deposit after expiry |
Payment — one token leg of the order
| Field | Meaning |
|---|---|
token | ERC-20 address (EVM) or fully qualified coin type, e.g. 0x2::sui::SUI (Sui) |
solverAmount | Amount forwarded to the solver, in base units (bigint) |
fee | Anyflo 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);
directDepositis part of the signed data;feeAddris ignored. - Sui — an order carries exactly one payment and
order.feeAddris required;directDepositis 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
- Verifies the injected
publicClientactually serves the configured chain (getChainId()). - 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. - Emits the
depositcalldata step carrying the order ID, tokens, amounts, expiry, and the server signature.
EVM execute
- Re-verifies the chain ID and that
signer.addressequalsorder.senderAddr. - Walks the steps in order, simulating the deposit via
eth_callright before it is sent — a reverting deposit fails before gas is spent on it. - Sends each step through the signer, waits for each receipt, and throws on revert. Returns the deposit transaction hash.
Sui prepareDeposit
- Verifies the client’s chain identifier against the known genesis digest of the configured chain.
- Builds a single PTB calling
deposit_account::deposit<T>withcoinWithBalanceinputs (resolved by the wallet at sign time — which is why the prepared step carries the liveTransactioninstance), the order fields, and the parsed Ed25519 public key and signature. - Pins the sender:
transaction.setSender(order.senderAddr).
Sui execute
- Re-verifies the chain identifier and the pinned sender.
- Simulates the PTB — the on-chain signature check, expiry, and replay guard all run in simulation.
- 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
| Behavior | EVM | Sui |
|---|---|---|
| Preflight | eth_call of the deposit step | simulateTransaction of the PTB |
| Submission | One transaction per step; waits for each receipt | One PTB submitted through the signer |
| Failure handling | Throws when a transaction receipt indicates failure | Simulation throws on failure; the signer handles failures after submission |
| Return value | Deposit transaction hash | Transaction digest from the signer |
Configuration
Chain overrides
Deployment fields use network defaults where available; otherwise, supply them as overrides.
| Chain kind | Required client | Deployment fields |
|---|---|---|
EVM (eip155:*) | publicClient | depositAccountAddress |
Sui (sui:*) | client | packageId, registryId |
Deployment defaults
Defaults are available on testnet:
| Deployment field | Testnet value |
|---|---|
EVM depositAccountAddress | 0xfE1A0F2dAE0C173ec82327CC6f9dB0459ba0c691Base Sepolia, Polygon Amoy, and Arbitrum Sepolia |
Sui packageId | 0xa2d0a7855a8cd09893c7ae51c3f81a0188cfef2ccdda9592ddc75b7eca6475c0 |
Sui registryId | 0xb06fbcdb75917aed6a3ebe84ab28376900ec12ec77ece3a2b22b91fefd127c3c |
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.
