Skip to main content

Fees and Payout Address

A merchant's withdraw fee and payout address live in the PaymentFactory contract. Changing them normally costs gas. With SodaPop the owner wallet signs EIP-712 typed data, and the API submits the transaction and pays the gas.

SettingDefaultTyped dataEndpoint
Withdraw fee, 0–500 bps10 bps (0.1%) until you set oneSetFeePOST /integration/fees/set-meta
Payout addressThe owner walletSetPayoutPOST /integration/fees/set-payout-meta

The withdraw fee applies only when someone other than the owner executes the withdrawal; see Fees.

These endpoints accept an API key or a request signature.

There are two signatures involved:

  1. the EIP-712 signature of the setting, from the owner wallet (in the body);
  2. the request auth (API key or request signature), as for any other call.

EIP-712 domain​

const domain = {
name: "PaymentFactory",
version: "2",
chainId: 8453, // the chain you are changing the setting on
verifyingContract: "0x8dAE900Fb6A1179A69b1E70A49b8399c6b9e4B19",
} as const;

Settings are per chain. To use the same payout address on Base and Polygon, sign and submit once per chain.

Typed data​

const types = {
SetFee: [
{ name: "user", type: "address" },
{ name: "feeBps", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
SetPayout: [
{ name: "user", type: "address" },
{ name: "payout", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
} as const;
  • user — the owner wallet.
  • nonce — the current value of nonces(user) on the factory, on that chain. SetFee and SetPayout share one nonce.
  • deadline — Unix seconds. The API also accepts milliseconds and normalizes them.

Full example​

settings.ts
import axios from "axios";
import { createPublicClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";

const FACTORY = "0x8dAE900Fb6A1179A69b1E70A49b8399c6b9e4B19" as const;
const CHAIN_ID = 8453;

const account = privateKeyToAccount(
process.env.OWNER_PRIVATE_KEY as `0x${string}`,
);
const publicClient = createPublicClient({ chain: base, transport: http() });

const domain = {
name: "PaymentFactory",
version: "2",
chainId: CHAIN_ID,
verifyingContract: FACTORY,
} as const;

const api = axios.create({
baseURL: process.env.SODAPOP_BASE_URL,
headers: { "x-depay-api-key": process.env.SODAPOP_API_KEY ?? "" },
});

async function readNonce(): Promise<bigint> {
return publicClient.readContract({
address: FACTORY,
abi: [
{
name: "nonces",
type: "function",
stateMutability: "view",
inputs: [{ name: "owner", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
] as const,
functionName: "nonces",
args: [account.address],
});
}

const oneHourFromNow = () => BigInt(Math.floor(Date.now() / 1000) + 3600);

export async function setFee(feeBps: number) {
const nonce = await readNonce();
const deadline = oneHourFromNow();

const signature = await account.signTypedData({
domain,
types: {
SetFee: [
{ name: "user", type: "address" },
{ name: "feeBps", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
},
primaryType: "SetFee",
message: { user: account.address, feeBps: BigInt(feeBps), nonce, deadline },
});

const { data } = await api.post("/integration/fees/set-meta", {
ownerAddress: account.address,
chainId: CHAIN_ID,
feeBps,
nonce: nonce.toString(),
deadline: deadline.toString(),
signature,
});
return data; // { txHash, ... }
}

export async function setPayout(payoutAddress: `0x${string}`) {
const nonce = await readNonce(); // re-read: shared with SetFee
const deadline = oneHourFromNow();

const signature = await account.signTypedData({
domain,
types: {
SetPayout: [
{ name: "user", type: "address" },
{ name: "payout", type: "address" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
},
primaryType: "SetPayout",
message: { user: account.address, payout: payoutAddress, nonce, deadline },
});

const { data } = await api.post("/integration/fees/set-payout-meta", {
ownerAddress: account.address,
chainId: CHAIN_ID,
payoutAddress,
nonce: nonce.toString(),
deadline: deadline.toString(),
signature,
});
return data;
}

Best practices​

  • Read the nonce right before signing, and re-read it between consecutive calls. After a successful call the nonce increments.
  • Use short deadlines (for example one hour).
  • On 400 Invalid nonce. Expected X, got Y, re-read the nonce, re-sign and submit again.
  • The API returns as soon as the transaction is sent (txHash). Check the transaction on a block explorer if you need confirmation.
  • To revoke a signature you have shared but not yet submitted, call invalidateNonce() on the factory from the owner wallet. It increments your nonce, so every outstanding SetFee / SetPayout signature becomes invalid. This is an on-chain call and costs gas. Changing the fee or payout address directly on the contract has the same effect.