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.
| Setting | Default | Typed data | Endpoint |
|---|---|---|---|
| Withdraw fee, 0–500 bps | 10 bps (0.1%) until you set one | SetFee | POST /integration/fees/set-meta |
| Payout address | The owner wallet | SetPayout | POST /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:
- the EIP-712 signature of the setting, from the owner wallet (in the body);
- 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 ofnonces(user)on the factory, on that chain.SetFeeandSetPayoutshare one nonce.deadline— Unix seconds. The API also accepts milliseconds and normalizes them.
Full example
- TypeScript
- Python
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;
}
settings.py
import os
import time
import requests
from eth_account import Account
from web3 import Web3
FACTORY = Web3.to_checksum_address("0x8dAE900Fb6A1179A69b1E70A49b8399c6b9e4B19")
CHAIN_ID = 8453
NONCES_ABI = [
{
"name": "nonces",
"type": "function",
"stateMutability": "view",
"inputs": [{"name": "owner", "type": "address"}],
"outputs": [{"name": "", "type": "uint256"}],
}
]
DOMAIN_TYPE = [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"},
]
account = Account.from_key(os.environ["OWNER_PRIVATE_KEY"])
w3 = Web3(Web3.HTTPProvider(os.environ["BASE_RPC_URL"]))
factory = w3.eth.contract(address=FACTORY, abi=NONCES_ABI)
def _sign(primary_type: str, fields: list, message: dict) -> str:
typed_data = {
"types": {"EIP712Domain": DOMAIN_TYPE, primary_type: fields},
"primaryType": primary_type,
"domain": {
"name": "PaymentFactory",
"version": "2",
"chainId": CHAIN_ID,
"verifyingContract": FACTORY,
},
"message": message,
}
signed = Account.sign_typed_data(account.key, full_message=typed_data)
return "0x" + signed.signature.hex().removeprefix("0x")
def _post(path: str, body: dict) -> dict:
response = requests.post(
f"{os.environ['SODAPOP_BASE_URL']}{path}",
json=body,
headers={"x-depay-api-key": os.environ["SODAPOP_API_KEY"]},
timeout=60,
)
response.raise_for_status()
return response.json()
def set_fee(fee_bps: int) -> dict:
nonce = factory.functions.nonces(account.address).call()
deadline = int(time.time()) + 3600
signature = _sign(
"SetFee",
[
{"name": "user", "type": "address"},
{"name": "feeBps", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "deadline", "type": "uint256"},
],
{"user": account.address, "feeBps": fee_bps, "nonce": nonce, "deadline": deadline},
)
return _post("/integration/fees/set-meta", {
"ownerAddress": account.address,
"chainId": CHAIN_ID,
"feeBps": fee_bps,
"nonce": str(nonce),
"deadline": str(deadline),
"signature": signature,
})
def set_payout(payout_address: str) -> dict:
payout = Web3.to_checksum_address(payout_address)
nonce = factory.functions.nonces(account.address).call() # shared with SetFee
deadline = int(time.time()) + 3600
signature = _sign(
"SetPayout",
[
{"name": "user", "type": "address"},
{"name": "payout", "type": "address"},
{"name": "nonce", "type": "uint256"},
{"name": "deadline", "type": "uint256"},
],
{"user": account.address, "payout": payout, "nonce": nonce, "deadline": deadline},
)
return _post("/integration/fees/set-payout-meta", {
"ownerAddress": account.address,
"chainId": CHAIN_ID,
"payoutAddress": payout,
"nonce": str(nonce),
"deadline": str(deadline),
"signature": signature,
})
Install with pip install eth-account web3 requests.
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 outstandingSetFee/SetPayoutsignature 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.