Setting up a donation page
A page anybody can pay, any amount, as many times as they like, accumulating toward a total — an appeal, a campaign, a tip jar, a crowdfund, a church offering, a memorial fund.
It is the same primitive as a one-time link with two differences that change everything about how you run it: many payers, and a closing condition that is not "paid".
1. Create the page
curl -X POST $CHAINOS/v1/payments/links \
-H "X-API-Key: $CHAINOS_PAYMENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"mode": "donation",
"addressMode": "per_payer",
"chain": "polygon",
"asset": "USDT",
"slug": "harvest-appeal",
"title": "Harvest appeal",
"description": "Rebuilding the community hall roof",
"thresholdAmount": "50000",
"closesAt": "2026-12-31T23:59:59Z",
"minAmount": "1",
"collectEmail": true
}'
{
"slug": "harvest-appeal",
"url": "https://pay.your-company.example/pay/harvest-appeal",
"mode": "donation",
"addressMode": "per_payer",
"amountRaised": "0",
"paymentCount": 0,
"thresholdAmount": "50000",
"closesAt": "2026-12-31T23:59:59Z",
"address": null,
"status": "active"
}
Custom slugs are honoured here, unlike on a fixed link. A donation page is meant to be
found, printed and said out loud, so harvest-appeal is the point rather than a leak.
One with neither is refused. An open-ended link is an address nobody ever stops watching, and "we will take it down eventually" is not a closing condition any system can act on.
If the appeal genuinely has no end, give it a date a year out and extend it. That is a decision somebody makes annually rather than a resource nothing reclaims.
2. The decision that matters: addressMode
This is the one choice on the page worth thinking hard about, because it cannot be changed afterwards and it decides whether your donors can be thanked by name.
per_payer (default) | shared | |
|---|---|---|
| Address | One derived per donor | One, for the life of the link |
| The donor | Enters an email, gets their own address | Sends to the address printed on the page |
| Receipts | Attributable, with a name | Anonymous unless volunteered |
| Concurrent donors | Told apart exactly | Best effort |
| Printable, postable, QR on a poster | No | Yes — that is the point |
| Invoices | Issued | None at all |
Choose per_payer if donors need receipts
Which is almost always, if the money is tax-deductible, if you send thank-you emails, if you publish a donor list, or if anyone might ask "did my gift arrive?".
The donor flow is one extra step: they enter an email, and
POST /v1/checkout/links/{slug}/donate derives an address that is theirs alone. Every payment
to it is unambiguously theirs.
Choose shared only when nothing ships
One address, printed once. A poster on a wall, a QR code in a printed order of service, a slide behind a stage, an address read out on a broadcast. Nothing has to be personalised and nothing has to be issued at the moment of giving.
The cost is real and stated: two donors paying at the same moment cannot be told apart well
enough to put a name on a receipt, so no invoice is issued at all. Do not choose shared
and then ask how to attribute donations — it is the trade you made, and the answer is to ask
donors to tell you separately.
A shared address plus a memo field is how XRP and Stellar exchanges usually work, and ChainOS deliberately does not do it for customer deposits — a customer who forgets the memo has sent funds nobody can attribute. It is admissible here only because nothing ships and nobody is owed anything in return.
Every payment accumulates toward the link's total in both modes. The difference is only whether you know who sent it.
3. Pick a chain donors can actually use
Donors are not your customers. They will not install anything, they will not learn what a network is, and a page that asks them to will lose most of them.
| Polygon or Base, USDC or USDT | Cheap, widely supported, small gifts are economic |
| TRON, USDT | Very widely held; the default for a lot of the world |
| Bitcoin | For an audience that holds it. Fees make small gifts uneconomic |
| Ethereum | Only for large gifts. A 20 USDC donation can cost more than that to collect |
A gift below the chain's economic minimum is refused at creation. Set minAmount to something
at or above that floor rather than letting donors hit the refusal — a donor who is told "no"
at the moment of giving usually does not come back.
{ "minAmount": "5", "maxAmount": "100000" }
maxAmount is worth setting too, as a typo guard. Somebody meaning 50 and typing 5000
happens, and on an irreversible rail the only cheap moment to catch it is before they send.
4. Show the running total honestly
curl -s $CHAINOS/v1/payments/links/$ID -H "X-API-Key: $KEY"
amountRaised is confirmed money only. A detected payment can still be reorganised out of
existence, and a public thermometer that went backwards would be worse than one that lags by a
few minutes.
If you are rendering the total on your own site, cache it and refresh on a timer rather than on every page load; an appeal that goes well can get a lot of page loads.
5. The close, and the announcement
A donation link closes by itself on whichever condition fires first:
closedReason | |
|---|---|
threshold | It reached its target |
date | closesAt passed |
manual | Somebody closed it |
payment_link_closed carries amountRaised and paymentCount — the two numbers you want for
the announcement, in one event.
if (event.event === 'payment_link_closed') {
await publishResult({
raised: event.data.amountRaised,
donors: event.data.paymentCount,
reason: event.data.closedReason,
});
}
To pause an appeal — a scandal, a duplicate campaign, a change of plan — pause rather than close. Closing is irreversible and freezes the total; pausing keeps it and resuming puts the page back.
The address is monitored until monitorUntil, weeks after the close. A donor who sends on the
day the campaign ended has given real money and it is credited.
Have an answer before it happens: credit it to the same cause, roll it into the next appeal, or return it. All three are defensible; discovering the question during an audit is not.
6. Thanking people
With per_payer and collectEmail, an invoice is issued and emailed on the first confirmation
of each donor's gift. For most appeals you will want your own acknowledgement as well, since
ours is a receipt rather than a thank-you.
if (event.event === 'payment_confirmed' && event.data.linkId === APPEAL_LINK_ID) {
await sendThanks({
email: event.data.customerEmail,
name: event.data.customerName,
amount: event.data.amountPaid,
asset: event.data.asset,
});
}
Fire it on payment_confirmed, not payment_detected. Thanking somebody for a gift that a
reorg then removes is a message you cannot take back.
7. Sweeping, and what it costs
Each donor's address in per_payer mode is an address holding a small balance. A thousand
donors is a thousand addresses.
Set a sweep policy for the (chain, asset) of the appeal, and set the dustFloor with
donations in mind. A 1 USDC gift on Ethereum costs more in gas to consolidate than it is
worth; on Polygon it does not. Run POST /v1/treasury/sweeps/preview and read skipped — if
it shows nine hundred addresses below the floor, the floor is wrong or the chain is.
Sweeps are never billed, so the only cost is the network fee.
8. Practical points that catch people out
One appeal, one link. A second link for the same campaign splits the total and neither page
tells you so. Use metadata to tag a single link if you need to attribute by channel.
Do not put the address in the email. Link to the page. An address in an email is a string somebody can alter in a forward, and an address on a page you control is not.
A printed QR is permanent. In shared mode the address on a poster stays valid for as long
as the poster is on the wall — monitoring continues well past the close. Decide in advance what
you do about gifts arriving from a poster nobody took down.
collectEmail is not consent. Whatever your jurisdiction requires about contacting donors
is yours to handle; the field collects an address so a receipt can be sent.
Next
- Payment links — the reference page, both modes in full.
- One-time payments — the fixed-amount case.
- Sweeps — the dust floor, and why the preview is mandatory.