Overview
CrowdPay currently supports campaigns with a single target asset. A creator running a campaign in Nigeria might want to accept NGN-backed stablecoins, USDC, and XLM simultaneously — with a single funding goal expressed in USD equivalent. This requires: storing per-asset sub-totals, computing USD equivalents using live FX rates, letting contributors choose which asset they contribute in, and correctly handling path payments that convert the contributor's asset to any of the campaign's accepted assets.
What needs to be built
backend/src/services/multiCurrencyGoal.js
-
Campaign creation changes — POST /api/campaigns:
- Add
goalMode: 'single_asset' | 'multi_asset'
- For
multi_asset: acceptedAssets: [{ code, issuer, weight: number }] — weight determines how much of the goal can be filled by each asset (weights must sum to 100)
goalAmountUsd: the campaign goal expressed in USD equivalent
-
GoalTracker — on each contribution event from the Horizon stream:
- Fetch live FX rate for the received asset; compute
contribution_usd_equivalent = amount * rate
- Update
campaigns.total_raised_usd and campaign_asset_totals
- If Fluxa FX rate is unavailable: use Horizon's
order_book mid-price as fallback; log a warn event
-
POST /api/campaigns/:id/contribute — multi-asset contribution:
- Accepts
{ asset, amount } — contributor chooses which accepted asset to send
- Validates
asset is in the campaign's acceptedAssets list
- Builds a
PathPaymentStrictReceive so the campaign wallet receives exactly in the chosen asset
- Returns
{ estimatedRate, estimatedFee, pathHops } before submission for the contributor to review
Frontend
-
Campaign creation form — goalMode toggle; for multi-asset: "Add accepted asset" button to build the asset list with per-asset weight sliders (constrained to sum to 100)
-
Campaign page — goal progress bar shows a single USD-equivalent progress; below the bar, a per-asset breakdown
-
Contribution modal — asset selector dropdown (only accepted assets); shows estimated USD equivalent as they type the amount; calls the Payment Simulator to show the conversion path
-
Currency toggle on the campaign page: switch the progress bar between USD equivalent and each accepted asset's native amount
Database migrations
-
campaign_accepted_assets: id, campaign_id, asset_code, asset_issuer, weight, created_at
-
campaign_asset_totals: campaign_id, asset_code, asset_issuer, total_raised (numeric), total_raised_usd (numeric), last_updated
-
Alter campaigns: add goal_mode, goal_amount_usd, total_raised_usd
Acceptance criteria
Overview
CrowdPay currently supports campaigns with a single target asset. A creator running a campaign in Nigeria might want to accept NGN-backed stablecoins, USDC, and XLM simultaneously — with a single funding goal expressed in USD equivalent. This requires: storing per-asset sub-totals, computing USD equivalents using live FX rates, letting contributors choose which asset they contribute in, and correctly handling path payments that convert the contributor's asset to any of the campaign's accepted assets.
What needs to be built
backend/src/services/multiCurrencyGoal.jsCampaign creation changes —
POST /api/campaigns:goalMode: 'single_asset' | 'multi_asset'multi_asset:acceptedAssets: [{ code, issuer, weight: number }]— weight determines how much of the goal can be filled by each asset (weights must sum to 100)goalAmountUsd: the campaign goal expressed in USD equivalentGoalTracker— on each contribution event from the Horizon stream:contribution_usd_equivalent = amount * ratecampaigns.total_raised_usdandcampaign_asset_totalsorder_bookmid-price as fallback; log awarneventPOST /api/campaigns/:id/contribute— multi-asset contribution:{ asset, amount }— contributor chooses which accepted asset to sendassetis in the campaign'sacceptedAssetslistPathPaymentStrictReceiveso the campaign wallet receives exactly in the chosen asset{ estimatedRate, estimatedFee, pathHops }before submission for the contributor to reviewFrontend
Campaign creation form —
goalModetoggle; for multi-asset: "Add accepted asset" button to build the asset list with per-asset weight sliders (constrained to sum to 100)Campaign page — goal progress bar shows a single USD-equivalent progress; below the bar, a per-asset breakdown
Contribution modal — asset selector dropdown (only accepted assets); shows estimated USD equivalent as they type the amount; calls the Payment Simulator to show the conversion path
Currency toggle on the campaign page: switch the progress bar between USD equivalent and each accepted asset's native amount
Database migrations
campaign_accepted_assets:id,campaign_id,asset_code,asset_issuer,weight,created_atcampaign_asset_totals:campaign_id,asset_code,asset_issuer,total_raised(numeric),total_raised_usd(numeric),last_updatedAlter
campaigns: addgoal_mode,goal_amount_usd,total_raised_usdAcceptance criteria
warnlog entry is writtenacceptedAssetsreturns400 ASSET_NOT_ACCEPTEDacceptedAssetswith weights not summing to 100 returns400 INVALID_ASSET_WEIGHTS