The Swap Flow
A programmatic swap is four steps: discover → quote → pay → track. This page covers each in depth, including every EVM payment variant (native, ERC-20 approve, ERC-20 permit), multisend, and where the Solana / TON payment differs.
All HTTP calls hit the base URL https://drip.tools/api. Full endpoint references live under API reference; the EVM contract is documented in Depository contract and the Solana / TON transfer in Paying from Solana & TON.
Step 1: Discover chains & tokens
Call GET /api/chains to learn what you can swap. Pass ?unwrap=true to get a single flat array instead of { testnet, mainnet }.
curl 'https://drip.tools/api/chains?unwrap=true'Each chain object tells you everything you need:
{
"chainId": "42161",
"name": "arbitrum",
"displayName": "Arbitrum",
"explorer": "https://arbiscan.io",
"isTestnet": false, // false = FROM (mainnet) side
"isEnabled": true,
"rpc": ["https://arbitrum.drpc.org", "https://arbitrum-one-rpc.publicnode.com", "…"],
"tokens": [
{ "symbol": "ETH", "address": "0x0000000000000000000000000000000000000000", "decimals": 18 },
{ "symbol": "USDC", "address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", "decimals": 6 }
],
"contracts": {
"depository": "0x52361c94…" // ← the pay target (FROM chains only)
}
}- FROM chains have
isTestnet: falseand exposecontracts.depository— where you pay. On an EVM chain it is the depository contract; on Solana and TON (family: "solana"/"ton") it is the treasury account you transfer to. Ifdepositoryis missing, that chain isn't enabled for programmatic swaps. - TO chains have
isTestnet: true. Skip the ones withisEnabled: false— they are listed, butPOST /api/quoteanswersBAD_TO_CHAIN. There is nothing to call on a TO chain — our relayers send your recipients their testnet tokens; the payout tx hash comes back on the order. rpcis an ordered list:rpc[0]is the primary endpoint, the rest are fallbacks.- Token
address0x0000…0000is the chain's native coin (also on Solana and TON). An SPL token on Solana is identified by its base58 mint address.decimalsis the base-unit exponent — you'll need it for every amount.
To see the testnet token's USD rate and whether the relayer currently has liquidity, call GET /api/quotes:
curl 'https://drip.tools/api/quotes?chainId=421614&symbol=ETH'
# { "symbol":"ETH", "price":0.25, "balance":"12513664307…", "available":true, … }available: false (or a balance smaller than your order) means that TO token can't be dispensed right now — the quote step will reject it with NO_TO_PRICE / NO_LIQUIDITY.
Step 2: Request a signed quote
Post the recipients and the exact TO amounts you want each to receive to POST /api/quote. Amounts are base-unit integer strings of the TO token.
curl -X POST 'https://drip.tools/api/quote' \
-H 'Content-Type: application/json' \
-d '{
"chainId": 8453,
"fromToken": "0x0000000000000000000000000000000000000000",
"sender": "0x9246ef7F2b7fEcE8AfD8a72271A76Cc2A8d8A3E5",
"toChainId": 421614,
"toToken": "0x0000000000000000000000000000000000000000",
"recipients": ["0x2F4edA5e7EEa6eb634C7FBb0acAFA04b8938F63A"],
"toAmounts": ["5000000000000000000"]
}'Response:
{
"quoteId": "0x1f5215a4577ef4548b550313d772594473e237c67514943751634818fdff9615",
"signer": "0x5C0917e114e49F3Cf266D6467ac129FEe42d77c5",
"vault": "0x52361c9412dcec6a83980fb065d0276ec001face",
"issuedAt": 1784120925,
"expiry": 1784121105,
"deadline": 1784121105,
"fromToken": "0x0000000000000000000000000000000000000000",
"fromTotal": "517554316856097",
"fromAmounts": ["517554316856097"],
"toAmounts": ["5000000000000000000"],
"usdTotal": "1.25",
"signature": "0x23fbc0…1b"
}What matters:
| Field | Use |
|---|---|
quoteId | The only reference carried on-chain. Pass it to the pay call, or put it in the Solana memo / TON comment. |
vault | The pay target — equal to the FROM chain's contracts.depository (contract on EVM, treasury on Solana / TON); lowercased on EVM, so compare case-insensitively. |
fromTotal | The exact FROM amount to pay (base units). msg.value for native, amount for ERC-20, lamports / nanotons on Solana / TON. |
deadline | Pass to the pay call. The tx reverts (Expired) after this. Equal to expiry. |
fromAmounts | Per-recipient FROM breakdown (bookkeeping; the backend expands the batch by these). |
signature | EIP-712 signature over the quote. Optional to verify client-side — see below. |
You pick TO, the API prices FROM
You never specify how much mainnet token to spend. You say "recipient gets 5 testnet ETH", and the API multiplies by the live FROM price (Chainlink) and the TO rate to return fromTotal. The quote locks those TO amounts — the backend delivers exactly toAmounts, regardless of price movement between quoting and payment.
Minimums & expiry
- $1 minimum for a single recipient, $5 for a multisend (>1 recipient). Below that:
BELOW_MIN_TOTAL. - TO amounts sit on a 4-decimal grid (whole sats on Bitcoin): more precision →
TO_TOO_PRECISE, less than one grid step →TO_TOO_SMALL. On a Solana TO chain each recipient needs at least 0.001 SOL (BELOW_SOL_RENT_MIN). - A quote is valid for 180 seconds. Submit your on-chain tx before
deadline; otherwise request a fresh quote. - Requesting a quote is free and non-binding — nothing happens until you pay. Quote right before you are ready to send.
Pay exactly what was quoted
The payment is matched against the quote off-chain: same payer as sender, same token as fromToken, an amount equal to fromTotal, before deadline. A payment that differs does not become an order — see Payment rules. Never round fromTotal, and never pay from a different wallet than the one you quoted for.
Verify the signature (optional)
The quote carries an EIP-712 signature from signer. Checking it before you pay is optional; a worked example is in Verifying the quote signature.
Step 3: Pay on-chain
Send one transaction on the FROM chain. The recipient list and amounts are not in it — only the quoteId (and, on EVM, the deadline). Pick the variant that matches your FROM chain and token.
Solana and TON
There is no contract call: transfer exactly fromTotal to the treasury (quote.vault) with the quoteId as the memo (Solana) or the text comment (TON). The transaction shape is a protocol — read Paying from Solana & TON before sending anything. The rest of this step is EVM only.
Native token (ETH, BNB, POL, AVAX, …)
msg.value must equal fromTotal.
import { parseAbi } from 'viem'
const abi = parseAbi(['function pay(bytes32 quoteId, uint256 deadline) payable'])
const hash = await wallet.writeContract({
address: quote.vault,
abi,
functionName: 'pay',
args: [quote.quoteId, BigInt(quote.deadline)],
value: BigInt(quote.fromTotal),
})ERC-20 token — approve + payWithToken
First approve the pay target (quote.vault) for at least fromTotal, then call payWithToken. amount must equal fromTotal.
const erc20 = parseAbi(['function approve(address spender, uint256 amount) returns (bool)'])
const abi = parseAbi([
'function payWithToken(address token, uint256 amount, bytes32 quoteId, uint256 deadline)',
])
// 1. approve (skip if the current allowance already covers fromTotal)
const approveHash = await wallet.writeContract({
address: quote.fromToken,
abi: erc20,
functionName: 'approve',
args: [quote.vault, BigInt(quote.fromTotal)],
})
await publicClient.waitForTransactionReceipt({ hash: approveHash })
// 2. pay
const hash = await wallet.writeContract({
address: quote.vault,
abi,
functionName: 'payWithToken',
args: [quote.fromToken, BigInt(quote.fromTotal), quote.quoteId, BigInt(quote.deadline)],
})Ethereum-mainnet USDT
USDT on Ethereum requires resetting the allowance to 0 before setting a new non-zero value. If a prior non-zero allowance exists, send approve(spender, 0) first, then approve(spender, fromTotal).
ERC-20 token — gasless approve with EIP-2612 permit
If the FROM token supports EIP-2612 (permit), you can skip the approve transaction entirely: sign a permit off-chain and pass it to payWithTokenPermit in a single transaction.
const abi = parseAbi([
'function payWithTokenPermit(address token, uint256 amount, bytes32 quoteId, uint256 deadline, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)',
])
// Sign an EIP-2612 permit for spender = depository, value = fromTotal.
// (Build domain/types from the token's name/version/nonces — see Examples.)
const { v, r, s, permitDeadline } = await signErc2612Permit(/* … */)
const hash = await wallet.writeContract({
address: quote.vault,
abi,
functionName: 'payWithTokenPermit',
args: [
quote.fromToken, BigInt(quote.fromTotal), quote.quoteId, BigInt(quote.deadline),
BigInt(permitDeadline), v, r, s,
],
})A full permit example (building the EIP-2612 signature) is in Examples → ERC-20 with permit. Tokens with a Uniswap Permit2 allowance can use payWithPermit2(...) instead — see the contract reference.
Multisend is the same call
A batch is not a different function. You requested many recipients in Step 2, so the quote's fromTotal already covers all of them. You still make one pay / payWithToken call (or one Solana / TON transfer) with msg.value / amount = fromTotal. The backend reads the recipients from the signed quote and delivers to each. This is the whole point of the off-chain model.
Step 4: Track your order
After the payment confirms, poll by your FROM tx hash. One payment expands into one order per recipient.
curl 'https://drip.tools/api/order?from_tx_hash=0x<yourFromTx>'{
"data": [
{
"status": "completed",
"id": "abc123…",
"from": { "chainId": "8453", "symbol": "ETH", "amount": "0.00051755", "txHash": "0x…" },
"to": { "chainId": "421614", "symbol": "ETH", "amount": "5.00000000",
"address": "0x2f4e…f63a", "txHash": "0x<payoutTx>", "ts": 1784120990 }
}
],
"refund": null
}- Each order's
statusstarts asprocessingand flips tocompleted(orfailed). to.txHashis the payout transaction on the testnet — proof the recipient was funded.- If some recipients failed, their FROM slice is refunded and surfaces in
refund— see Order lifecycle & refunds. - What to pass as
from_tx_hash: the EVM transaction hash, the Solana transaction signature, or — on TON, where your wallet never learns the treasury-side hash — thequoteIditself. See Orders.
Other tracking options:
- By order id —
GET /api/order?id=<order_id>. - Large batches —
GET /api/order?from_tx_hash=…&paginate=1returns an aggregate summary plus a page of recipients instead of the whole array. - Bulk —
POST /api/orders/statuswith{ ids: [...] }. - History —
GET /api/orders?address=<payer>&view=batches. - Wait estimate —
GET /api/orders/load?ref=<txHashOrId>returns your position in the dispense queue. - Push instead of poll —
GET /api/orders/streamis a Server-Sent Events feed that fires whenever one of your orders changes.
Ready-to-run scripts for every variant are in Examples.