How it works, end to end

The app is a front end for one small contract. This page explains what that contract does, how to prepare your recipient list, every function it exposes, and what each error means.

01

What this is

Batch distribution pays many addresses in a single transaction. Without it, sending to 1,000 people means 1,000 transfers, signed one at a time. Here it is a handful of transactions and a handful of signatures.

The contract holds nothing. Tokens move directly from your wallet to each recipient, so there is no balance sitting in it, nothing to withdraw, and nothing that can be stranded. It has no owner, no pause switch, no upgrade path and takes no fee — once deployed, nobody can change how it behaves, including whoever deployed it.

Fee0%
Custodynone
02

Sending a distribution

  1. 01Connect a wallet. Top right. You will be asked to add Apertum if your wallet does not have it yet.
  2. 02Choose what to send. Pick an ERC20 from the list of tokens you hold, paste a token address, or switch to APTM.
  3. 03Upload your recipients. Drop a CSV, or paste a list. The format is below — a template is downloadable from that step.
  4. 04Set the amounts. Split a total equally, give everyone the same fixed amount, or use the amount column from your file.
  5. 05Approve, then send. One approval lets the contract move that total. Then one signature per batch. Each batch is simulated first, so a doomed one fails before it costs you anything.

If a batch fails — a rejected signature, a dropped connection — the run stops and the button becomes Resume distribution. Batches that already landed are not sent again.

03

The CSV format

Column layout does not matter. In each row, the first cell that looks like an address is the recipient, and the first number after it is the amount. Header rows are skipped, blank lines ignored, and anything unusable is reported with its line number instead of being silently dropped.

equal split — no amounts needed
address
0x70997970C51812dc3A010C7d01b50e0d17dc79C8
0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC
a different amount for each address
address,amount,note
0x70997970C51812dc3A010C7d01b50e0d17dc79C8,100,early supporter
0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC,42.5,contributor

Extra columns are ignored, so notes and names can stay in the file. Include an amount on every row or none at all — a half-filled column is rejected rather than guessed at.

Writing amounts

  • 100 — whole numbers
  • 42.5 — decimal point
  • 1000.25 — no thousands separators
  • 1,000.25 — the comma is a column break; this row is rejected

Amounts are written the way you would say them, not in base units — the app converts using the token's decimals. Writing more decimal places than the token supports is refused rather than rounded.

04

Contract functions

Five functions, and nothing else. There is no admin surface to document because there is no admin.

disperseTokenEqual

nonpayable

disperseTokenEqual(address token, address[] recipients, uint256 amountEach)

Equal distribution. Every address in the list receives exactly the same amount.

This is what the app calls when you split a pot equally or set a fixed amount per address. Approve the contract for amountEach × recipients.length first.

token
The ERC20 you are distributing.
recipients
Who gets paid. Listing an address twice pays it twice.
amountEach
What every single recipient receives, in base units.

disperseToken

nonpayable

disperseToken(address token, address[] recipients, uint256[] amounts)

Pays each address its own amount.

The two arrays are index-aligned: recipients[0] receives amounts[0], and so on. This is what runs when your CSV has an amount column. Approve for the sum of amounts.

token
The ERC20 you are distributing.
recipients
Who gets paid.
amounts
One figure per recipient, same length and order.

disperseNativeEqual

payable

disperseNativeEqual(address[] recipients, uint256 amountEach)

The same equal distribution, for APTM instead of a token.

No approval needed — you attach the total as transaction value. Anything you send beyond amountEach × recipients.length is refunded to you in the same transaction.

recipients
Who gets paid.
amountEach
Wei per recipient.

disperseNative

payable

disperseNative(address[] recipients, uint256[] amounts)

Variable amounts of APTM.

Sums the amounts and checks your attached value covers it before moving anything. Excess is refunded.

recipients
Who gets paid.
amounts
One figure per recipient, same length and order.

tokenStatus

view

tokenStatus(address token, address owner) → (uint256 balance, uint256 approved)

Reads a holder's balance and their approval to this contract in one call.

A convenience for interfaces, so checking whether a sender is ready takes one round-trip instead of two. Costs no gas.

token
The ERC20 to inspect.
owner
The address whose balance and allowance you want.

Every one of them is all-or-nothing. If a single transfer in a batch fails, the whole transaction reverts and nobody is paid — there is never a half-finished distribution to reconcile.

05

What the errors mean

The contract uses custom errors, so a failure comes back as a name rather than a string. If your wallet shows a raw selector, this is the list.

NoRecipientsThe recipient list was empty.
LengthMismatchrecipients and amounts are different lengths.
ZeroAmountAn amount was zero. Every recipient must receive something.
ZeroRecipient0x0 appeared in the list.
NotAContractThe token address has no code on this chain — usually a wrong address or wrong network.
TransferFailedA transfer reverted or returned false. Commonly too little balance or allowance.
InsufficientValueAttached value was below the total. Carries the required and provided figures.
RefundFailedThe leftover value could not be returned to you.
ReentrancyA recipient tried to call back into the contract mid-batch.
06

ABI

The interface definition, for building against the contract yourself. Copy it, or save it as a file.

json
[
  {
    "type": "function",
    "name": "disperseTokenEqual",
    "stateMutability": "nonpayable",
    "inputs": [
      {
        "name": "token",
        "type": "address"
      },
      {
        "name": "recipients",
        "type": "address[]"
      },
      {
        "name": "amountEach",
        "type": "uint256"
      }
    ],
    "outputs": []
  },
  {
    "type": "function",
    "name": "disperseToken",
    "stateMutability": "nonpayable",
    "inputs": [
      {
        "name": "token",
        "type": "address"
      },
      {
        "name": "recipients",
        "type": "address[]"
      },
      {
        "name": "amounts",
        "type": "uint256[]"
      }
    ],
    "outputs": []
  },
  {
    "type": "function",
    "name": "disperseNativeEqual",
    "stateMutability": "payable",
    "inputs": [
      {
        "name": "recipients",
        "type": "address[]"
      },
      {
        "name": "amountEach",
        "type": "uint256"
      }
    ],
    "outputs": []
  },
  {
    "type": "function",
    "name": "disperseNative",
    "stateMutability": "payable",
    "inputs": [
      {
        "name": "recipients",
        "type": "address[]"
      },
      {
        "name": "amounts",
        "type": "uint256[]"
      }
    ],
    "outputs": []
  },
  {
    "type": "function",
    "name": "tokenStatus",
    "stateMutability": "view",
    "inputs": [
      {
        "name": "token",
        "type": "address"
      },
      {
        "name": "owner",
        "type": "address"
      }
    ],
    "outputs": [
      {
        "name": "balance",
        "type": "uint256"
      },
      {
        "name": "approved",
        "type": "uint256"
      }
    ]
  },
  {
    "type": "event",
    "name": "TokensDispersed",
    "inputs": [
      {
        "name": "token",
        "type": "address",
        "indexed": true
      },
      {
        "name": "sender",
        "type": "address",
        "indexed": true
      },
      {
        "name": "total",
        "type": "uint256",
        "indexed": false
      },
      {
        "name": "recipientCount",
        "type": "uint256",
        "indexed": false
      }
    ]
  },
  {
    "type": "error",
    "name": "NoRecipients",
    "inputs": []
  },
  {
    "type": "error",
    "name": "LengthMismatch",
    "inputs": []
  },
  {
    "type": "error",
    "name": "ZeroAmount",
    "inputs": []
  },
  {
    "type": "error",
    "name": "ZeroRecipient",
    "inputs": []
  },
  {
    "type": "error",
    "name": "NotAContract",
    "inputs": []
  },
  {
    "type": "error",
    "name": "TransferFailed",
    "inputs": []
  },
  {
    "type": "error",
    "name": "InsufficientValue",
    "inputs": [
      {
        "name": "required",
        "type": "uint256"
      },
      {
        "name": "provided",
        "type": "uint256"
      }
    ]
  },
  {
    "type": "error",
    "name": "RefundFailed",
    "inputs": []
  },
  {
    "type": "error",
    "name": "Reentrancy",
    "inputs": []
  }
]
07

Gas and batching

Apertum caps a block at 12,000,000 gas, and no single transaction may exceed it. That is why a long list is split across several transactions. The slider in the ticket sets how many recipients go in each, from 25 to 300, defaulting to 200.

Cost per recipient is not fixed. Paying an address that holds none of the token writes a fresh storage slot and costs about 25,600 gas; paying one that already holds some costs about 10,200. Because that is a 2.5× swing, the gas figure in the ticket is a real measurement of your actual batch rather than a multiplication.

A practical consequence: the second time you distribute to the same list, it is considerably cheaper than the first.