Source: Full executable TypeScript in contract/src/examples/03-campaign-payment-treasury/. This page summarizes the flow — read the source for complete per-step code.
PrerequisitesYour platform must be enlisted with the PaymentTreasury (or TimeConstrainedPaymentTreasury) implementation registered and approved — see Platform Enlistment.
The Story
CeloMarket is an online marketplace where independent artisans sell handcrafted goods. Unlike the crowdfunding scenarios, CeloMarket does not run time-bound campaigns with pledges and rewards. Instead, it processes individual e-commerce transactions — a buyer selects a product, pays with cryptocurrency, and the platform fulfills the order. CeloMarket uses the PaymentTreasury model, which works like a traditional payment processor but entirely on-chain. Every payment is broken down into line items (product price, shipping, tax) and follows a two-step flow for the off-chain path: the buyer pays, the treasury is funded, and the platform confirms after verifying the order. Direct on-chain checkout usesprocessCryptoPayment instead.
In this scenario, a buyer named Sam purchases a handcrafted ceramic vase for $120 with $15 shipping. The payment flows through the treasury, gets confirmed by the platform (off-chain path) or settles in one transaction (processCryptoPayment), and the funds become available for withdrawal after fees are disbursed.
Multi-token support
Every payment record includespaymentToken. The treasury only accepts tokens that CampaignInfo.isTokenAccepted allows. Pending, confirmed, fee, and refund accounting is per ERC-20 contract (amounts in that token’s decimals).
PaymentTreasury vs. TimeConstrainedPaymentTreasury
The SDK’soak.paymentTreasury(address) supports two on-chain variants through the same interface:
| Variant | Behavior |
|---|---|
| PaymentTreasury | Standard payment processing with no time restrictions. Payments can be created and confirmed at any time. |
| TimeConstrainedPaymentTreasury | Adds a launch time and deadline enforced on-chain. Payments can only be created after the launch time and before the deadline. Enables claimExpiredFunds after deadline + claim delay. |
NFT Handling
All pledge/payment NFTs across every treasury type live on theCampaignInfo contract. No treasury contract is an ERC-721 itself. Before calling any refund function that burns an NFT (claimRefundSelf), the NFT owner must approve the treasury contract to manage the NFT via campaignInfo.approve(treasuryAddress, tokenId).
Role Reference
| Function | Who can call | Contract modifier |
|---|---|---|
createPayment / createPaymentBatch | Platform Admin | onlyPlatformAdmin |
processCryptoPayment | Anyone (buyer) | (no role modifier) |
confirmPayment / confirmPaymentBatch | Platform Admin | onlyPlatformAdmin |
cancelPayment | Platform Admin | onlyPlatformAdmin |
claimRefundSelf(paymentId) | Anyone (crypto payments only — refund goes to NFT owner) | (no role modifier) |
claimRefund(paymentId, refundAddress) | Platform Admin (off-chain payments only) | onlyPlatformAdmin |
disburseFees | Anyone | (no role modifier) |
withdraw | Platform Admin or Creator | onlyPlatformAdminOrCampaignOwner |
claimExpiredFunds | Platform Admin | onlyPlatformAdmin |
claimNonGoalLineItems | Platform Admin | onlyPlatformAdmin |
Steps
Step 1: Create Campaign
Before deploying a PaymentTreasury, CeloMarket needs aCampaignInfo contract, which will hold NFT receipts for crypto payments. E-commerce campaigns typically have goalAmount: 0n.
CampaignInfo address can be discovered two ways. Both are shown in the source file — prefer Approach 1 when you have the receipt.
Approach 1 — Decode CampaignCreated from the receipt (recommended): deterministic and works immediately, regardless of RPC indexing lag.
identifierToCampaignInfo (convenience): handy when you only have the identifier and did not keep the receipt. On some RPC providers the mapping may briefly return the zero address right after the transaction, so prefer Approach 1 when the receipt is available.
Step 2: Deploy Treasury
CeloMarket deploys a PaymentTreasury linked to theCampaignInfo from Step 1. Slot 2n is PaymentTreasury; slot 3n is TimeConstrainedPaymentTreasury.
Step 3: Create Payment Record — Flow A (Platform Admin)
For off-chain payments, the Platform Admin creates a payment record on-chain that describes the order: payment ID, line items, external fees, and expiration. This step moves no funds; it records intent. The buyer pays through off-chain rails; the platform later confirms.createPaymentBatch creates multiple records in a single transaction.
Step 4: Process Crypto Payment — Flow B (Buyer)
processCryptoPayment is an independent entry point: it creates the payment record AND transfers ERC-20 tokens to the treasury in a single transaction. It does not complete a prior createPayment. An NFT is minted as proof of payment.
Step 5: Confirm Payment — Flow A (Platform Admin)
After Sam’s tokens arrive in the treasury (off-chain path), CeloMarket runs fraud detection and validates the shipping address, then confirms on-chain. Until confirmation, funds remain in a pending state. Omit this step if using Flow B.confirmPaymentBatch(paymentIds, buyerAddresses) for high-volume platforms.
Step 6: Read Payment Data
All payment and treasury data is publicly readable. Useful for order detail pages, customer support dashboards, and audit reports.Step 7: Handle Refunds
Three distinct paths: A) Cancel an unconfirmed off-chain payment — Platform Admin callscancelPayment(paymentId). Works only on unconfirmed, non-expired, non-crypto payments.
B) Refund a confirmed off-chain payment (no NFT) — Platform Admin calls claimRefund(paymentId, refundAddress).
C) Refund a crypto payment (NFT) — Any caller can claimRefundSelf(paymentId). The contract looks up the NFT owner, burns the NFT, and sends the refundable amount to that owner. Crypto payments are auto-confirmed; no prior cancelPayment is needed (and would revert).
canRefund: true at creation time are included in the refund.
Step 8: Disburse Fees
Transfers accumulated protocol and platform fees to their recipients. Anyone can call it. Can be called multiple times as new payments are confirmed.Step 9: Withdraw Funds
After fees have been disbursed, the Platform Admin or creator withdraws all remaining confirmed funds to the campaign owner’s wallet.Step 10: Claim Expired Funds (TimeConstrainedPaymentTreasury only)
After the campaign deadline plus the platform’sclaimDelay, the Platform Admin sweeps all remaining balances (confirmed, non-goal, refundable, fees).
PaymentTreasuryClaimWindowNotReached.