ICO Platform Development
ICO in 2024—not what it was in 2017. Raw website with MetaMask button and PDF whitepaper no longer sells. Today ICO platform is complete infrastructure: KYC/AML verification, multi-chain smart contracts, regulated token structure, and team analytics. Building from scratch makes sense only for platforms serving many projects. For one project, using existing launchpad protocols (Polkastarter, DAO Maker) is simpler. If building platform—let's discuss real architecture.
Multi-Project Platform Architecture
ICO platform consists of several independent layers:
┌─────────────────────────────────────────────────────┐
│ Project Onboarding │ Investor Portal │ Admin │
├─────────────────────────────────────────────────────┤
│ Business Logic Layer │
│ Project Registry │ Sale Engine │ Vesting │ KYC │
├─────────────────────────────────────────────────────┤
│ Smart Contract Layer │
│ SaleFactory │ VestingVault │ AllocationRegistry │
├─────────────────────────────────────────────────────┤
│ Infrastructure │
│ Multichain indexer │ Price feeds │ Compliance API │
└─────────────────────────────────────────────────────┘
Factory Pattern for Contract Deployment
Each project gets own contract set. Factory eliminates manual deployment and reduces errors:
contract ICOFactory {
address public immutable saleImplementation; // EIP-1167 minimal proxy
address public immutable vestingImplementation;
struct DeployedProject {
address saleContract;
address vestingContract;
address projectToken;
uint256 deployedAt;
address projectOwner;
}
mapping(bytes32 => DeployedProject) public projects;
mapping(address => bytes32) public contractToProject;
event ProjectDeployed(
bytes32 indexed projectId,
address saleContract,
address vestingContract,
address projectOwner
);
function deployProject(
bytes32 projectId,
address projectToken,
SaleConfig calldata saleConfig,
VestingConfig calldata vestingConfig
) external onlyVerifiedProject(projectId) returns (address sale, address vesting) {
// EIP-1167 minimal proxy — cloning implementation
// Saves gas: instead of full contract deploy—45-byte proxy
sale = Clones.clone(saleImplementation);
vesting = Clones.clone(vestingImplementation);
// Initialize (instead of constructor, since proxy)
ICOSale(sale).initialize(
projectToken,
saleConfig,
vesting,
address(this) // platform as admin
);
IVestingVault(vesting).initialize(
projectToken,
vestingConfig,
sale // only sale contract can add recipients
);
projects[projectId] = DeployedProject({
saleContract: sale,
vestingContract: vesting,
projectToken: projectToken,
deployedAt: block.timestamp,
projectOwner: msg.sender
});
contractToProject[sale] = projectId;
contractToProject[vesting] = projectId;
emit ProjectDeployed(projectId, sale, vesting, msg.sender);
}
}
EIP-1167 minimal proxy reduces deployment cost from ~2–3M gas to ~45k. For 100 projects yearly, savings add up.
KYC/AML Integration
Regulatory reality: most ICOs today require at least basic KYC for participants, and for US users—accredited investor status or geoblocking.
On-Chain KYC Registry
contract KYCRegistry {
enum KYCStatus {
None,
PendingVerification,
Approved,
Rejected,
Expired
}
struct KYCRecord {
KYCStatus status;
uint8 tier; // 1 = basic, 2 = enhanced
bytes3 countryCode; // ISO 3166-1 alpha-3
uint256 verifiedAt;
uint256 expiresAt; // KYC expires, needs update
bool isAccreditedInvestor; // for US accredited investor rule
}
mapping(address => KYCRecord) public records;
mapping(address => bool) public kycProviders; // Sumsub, Onfido etc
// KYC provider updates status after off-chain verification
function updateKYCStatus(
address user,
KYCStatus status,
uint8 tier,
bytes3 countryCode,
bool isAccredited,
uint256 validityPeriod // usually 1 year
) external onlyKYCProvider {
records[user] = KYCRecord({
status: status,
tier: tier,
countryCode: countryCode,
verifiedAt: block.timestamp,
expiresAt: block.timestamp + validityPeriod,
isAccreditedInvestor: isAccredited
});
emit KYCUpdated(user, status, countryCode);
}
function isEligible(
address user,
uint8 requiredTier,
bytes3[] memory blockedCountries
) external view returns (bool) {
KYCRecord memory record = records[user];
if (record.status != KYCStatus.Approved) return false;
if (block.timestamp > record.expiresAt) return false;
if (record.tier < requiredTier) return false;
// Geoblocking check
for (uint i = 0; i < blockedCountries.length; i++) {
if (record.countryCode == blockedCountries[i]) return false;
}
return true;
}
}
Sumsub Integration (API)
import requests, hmac, hashlib, time
class SumsubClient:
BASE_URL = "https://api.sumsub.com"
def __init__(self, app_token: str, secret_key: str):
self.app_token = app_token
self.secret_key = secret_key
def create_applicant(self, external_user_id: str, level_name: str = "basic-kyc") -> dict:
endpoint = f"/resources/applicants?levelName={level_name}"
payload = {
"externalUserId": external_user_id,
"fixedInfo": {},
"requiredIdDocs": {
"docSets": [{"idDocSetType": "IDENTITY", "types": ["PASSPORT", "ID_CARD"]}]
}
}
return self._make_request("POST", endpoint, payload)
def get_applicant_status(self, applicant_id: str) -> dict:
endpoint = f"/resources/applicants/{applicant_id}/status"
return self._make_request("GET", endpoint)
After KYC approval, provider calls webhook → your backend calls kycRegistry.updateKYCStatus() → user can participate in ICO.
Multi-Chain Architecture
ICO platform should support multiple networks: Ethereum mainnet (large investors), Polygon/Base (retail, low gas), BSC (Asia audience). Same contract address across chains via deterministic deployment.
// Universal deployment script
const salt = ethers.utils.id("ICO_PLATFORM_V1");
const factory = await Factory.deploy({ gasPrice: chainConfig.gasPrice });
console.log(`Deployed to ${chainConfig.name}: ${factory.address}`);
Data Aggregation from Multiple Chains
Frontend shows total raised across all chains. Indexer architecture:
Chain 1 (Ethereum) ──┐
Chain 2 (Polygon) ──┼──→ Event Indexer ──→ PostgreSQL ──→ GraphQL API ──→ Frontend
Chain 3 (Base) ──┘
Ponder or Goldsky Mirror for multi-chain indexing. The Graph supports multi-chain subgraphs, but combining into single API needs separate aggregator.
Platform Management: Fees and Governance
contract PlatformFeeManager {
uint256 public platformFeePercent = 250; // 2.5% of raised
address public feeRecipient; // platform multisig
// Projects can whitelist for fee reduction
mapping(bytes32 => uint256) public projectFeeOverride;
function calculateFee(bytes32 projectId, uint256 amount)
public view returns (uint256)
{
uint256 feePercent = projectFeeOverride[projectId] > 0
? projectFeeOverride[projectId]
: platformFeePercent;
return (amount * feePercent) / 10000;
}
}
Backend: Key Services
Project verification — check team (KYB), audit requirement, tokenomics review. Not every listing-fee-paying project should appear.
Price calculation — multi-currency payments (ETH, BNB, MATIC, USDC) converted to base unit. Chainlink on-chain, CoinGecko API for display.
Notifications — email/telegram: KYC approved, round opened, transaction confirmed, vesting claim available. Without this, participation conversion drops sharply.
Regulatory Considerations
US users — classic Reg D exemption (accredited only) or Reg S (non-US only). IP geoblocking + KYC country. Reg A+ allows retail US, but costs $500k+ in legal.
EU (MiCA) — utility tokens need whitepaper per MiCA format, some categories need authorization. Asset-referenced and e-money tokens need licensing.
Timeline and Phases
| Phase | Content | Duration |
|---|---|---|
| Architecture & legal design | Regulatory structure, contract architecture | 3–4 weeks |
| Smart contracts | Factory, Sale, Vesting, KYC Registry | 5–7 weeks |
| Backend services | Project onboarding, KYC integration, indexer | 6–8 weeks |
| Frontend | Investor portal, project dashboard, admin | 6–10 weeks |
| Security audit | Contracts + backend | 4–6 weeks |
| Testnet & QA | 3–4 weeks | |
| Launch & monitoring | 2 weeks |
Full cycle to production: 7–10 months. Establish regulatory framework before development—retroactive compliance costs more.







