Розробка платформи у стилі friend.tech
Friend.tech запустився в серпні 2023 на Base, генерував $50M+ в комісіях за перші місяці та задав шаблон для цілого класу SocialFi додатків: токенізований доступ до людей. Механіка: користувач привязує Twitter-аккаунт, інші купують його «ключі» (shares), ціна ключів зростає по bonding curve, власники ключів отримають доступ до приватного чату або контенту. Просто, жадно ефективно з точки зору утримання, та технічно цікаво.
Копіювати friend.tech буквально — програшна стратегія (аудиторія пішла, ринок насичений). Але механіка bonding curve + gated access застосована до десятків вертикалей: expert networks, fan платформи, creator economy, дозволені DAOs. Розберемо як це будувати правильно.
Bonding Curve: математика та реалізація
Ціна ключа визначається кількістю вже виданих ключів за формулою. Friend.tech використовував:
price(n) = n² / 16000 ETH
де n — поточна кількість ключів. Це polynomial крива — ціна зростає квадратично. Це створює сильний FOMO для ранніх покупців та експоненціально високу ціну при великому supply.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SocialBondingCurve {
// Маппінг субʼєкт → кількість виданих ключів
mapping(address => uint256) public sharesSupply;
// Маппінг субʼєкт → власник → кількість ключів
mapping(address => mapping(address => uint256)) public sharesBalance;
address public protocolFeeDestination;
uint256 public protocolFeePercent; // у basis points
uint256 public subjectFeePercent; // комісія йде субʼєкту (власнику ключів)
event Trade(
address indexed trader,
address indexed subject,
bool isBuy,
uint256 shareAmount,
uint256 ethAmount,
uint256 protocolEthAmount,
uint256 subjectEthAmount,
uint256 supply
);
// Ціна n-го ключа за polynomial кривою
function getPrice(uint256 supply, uint256 amount) public pure returns (uint256) {
uint256 sum1 = supply == 0 ? 0 : (supply - 1) * supply * (2 * (supply - 1) + 1) / 6;
uint256 sum2 = (supply + amount - 1) * (supply + amount) * (2 * (supply + amount - 1) + 1) / 6;
uint256 summation = sum2 - sum1;
return summation * 1 ether / 16000;
}
function getBuyPrice(address sharesSubject, uint256 amount) public view returns (uint256) {
return getPrice(sharesSupply[sharesSubject], amount);
}
function getSellPrice(address sharesSubject, uint256 amount) public view returns (uint256) {
return getPrice(sharesSupply[sharesSubject] - amount, amount);
}
function getBuyPriceAfterFee(address sharesSubject, uint256 amount) public view returns (uint256) {
uint256 price = getBuyPrice(sharesSubject, amount);
uint256 protocolFee = price * protocolFeePercent / 1 ether;
uint256 subjectFee = price * subjectFeePercent / 1 ether;
return price + protocolFee + subjectFee;
}
function buyShares(address sharesSubject, uint256 amount) external payable {
uint256 supply = sharesSupply[sharesSubject];
// Перший ключ може купити тільки сам субʼєкт (bootstrap)
require(supply > 0 || sharesSubject == msg.sender, "Only subject can buy first share");
uint256 price = getPrice(supply, amount);
uint256 protocolFee = price * protocolFeePercent / 1 ether;
uint256 subjectFee = price * subjectFeePercent / 1 ether;
require(msg.value >= price + protocolFee + subjectFee, "Insufficient ETH");
sharesBalance[sharesSubject][msg.sender] += amount;
sharesSupply[sharesSubject] = supply + amount;
emit Trade(msg.sender, sharesSubject, true, amount, price, protocolFee, subjectFee, supply + amount);
(bool success1, ) = protocolFeeDestination.call{value: protocolFee}("");
(bool success2, ) = sharesSubject.call{value: subjectFee}("");
require(success1 && success2, "Unable to send funds");
// Повернення перевплати
if (msg.value > price + protocolFee + subjectFee) {
(bool refundSuccess, ) = msg.sender.call{value: msg.value - price - protocolFee - subjectFee}("");
require(refundSuccess, "Refund failed");
}
}
function sellShares(address sharesSubject, uint256 amount) external {
uint256 supply = sharesSupply[sharesSubject];
require(supply > amount, "Cannot sell the last share");
uint256 price = getPrice(supply - amount, amount);
uint256 protocolFee = price * protocolFeePercent / 1 ether;
uint256 subjectFee = price * subjectFeePercent / 1 ether;
require(sharesBalance[sharesSubject][msg.sender] >= amount, "Insufficient shares");
sharesBalance[sharesSubject][msg.sender] -= amount;
sharesSupply[sharesSubject] = supply - amount;
emit Trade(msg.sender, sharesSubject, false, amount, price, protocolFee, subjectFee, supply - amount);
uint256 netAmount = price - protocolFee - subjectFee;
(bool success, ) = msg.sender.call{value: netAmount}("");
require(success, "Unable to send funds");
(bool success1, ) = protocolFeeDestination.call{value: protocolFee}("");
(bool success2, ) = sharesSubject.call{value: subjectFee}("");
require(success1 && success2, "Fee transfer failed");
}
}
Проблема polynomial кривої: при великому supply ключі стають астрономічно дорогими. Для нішевих творців — це окей (ключ «зірки» коштує дорого). Для широкого ринку — бар'єр входу вбиває зростання. Альтернативи:
Лінійна крива: price = base_price + supply × slope — передбачувана, але немає FOMO
Сигмоїдна крива: швидке зростання спочатку (FOMO), потім плато — більш управлювана економіка для mass market
// Сигмоїдна ціна (апроксимація)
function getSigmoidPrice(uint256 supply, uint256 amount) public pure returns (uint256) {
// k = крутизна, midpoint = точка перегину
uint256 k = 100;
uint256 midpoint = 1000; // supply при якому половина max_price
uint256 maxPrice = 1 ether;
// Спрощена апроксимація sigmoid через piece-wise linear
if (supply < midpoint / 4) {
return supply * maxPrice / (4 * midpoint); // нижня частина
} else if (supply < 3 * midpoint / 4) {
return maxPrice / 4 + (supply - midpoint/4) * maxPrice / (2 * midpoint); // середня
} else {
return 3 * maxPrice / 4 + (supply - 3*midpoint/4) * maxPrice / (8 * midpoint); // верхня
}
}
Gated Access: ключі як access токени
Власники ключів отримують доступ до контенту. Off-chain верифікація — найпрактичніший підхід:
// Backend: верифікація доступу через підпис
import { ethers } from 'ethers'
async function verifyAccess(
userAddress: string,
subjectAddress: string,
signature: string,
nonce: string
): Promise<boolean> {
// Верифікуємо підпис (EIP-712)
const message = {
user: userAddress,
subject: subjectAddress,
nonce,
timestamp: Math.floor(Date.now() / 1000),
}
const recoveredAddress = ethers.verifyTypedData(
DOMAIN,
TYPES,
message,
signature
)
if (recoveredAddress.toLowerCase() !== userAddress.toLowerCase()) {
return false
}
// Перевіряємо on-chain баланс ключів
const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, provider)
const balance = await contract.sharesBalance(subjectAddress, userAddress)
return balance > 0n
}
Повністю on-chain gating через modifier:
modifier onlyKeyHolder(address subject) {
require(sharesBalance[subject][msg.sender] > 0, "No key");
_;
}
function sendPrivateMessage(address subject, bytes calldata encryptedContent)
external
onlyKeyHolder(subject)
{
emit PrivateMessage(subject, msg.sender, encryptedContent);
}
Для реального приватного чату — контент шифрується публічними ключами одержувачів off-chain (наприклад, через lit-protocol або кастомний threshold encryption), on-chain тільки события.
Social Graph та верифікація ідентичності
Friend.tech використовував Twitter для верифікації. Це створило проблеми: Twitter міг деавторизувати OAuth — та платформа втрачає social graph. Більш стійкі підходи:
Інтеграція Lens Protocol — децентралізований social graph на Polygon. Profile = NFT, followers — on-chain. Creator token привязаний до Lens Profile ID, не до Ethereum адреси:
// Ключи привязані до Lens profile
mapping(uint256 => mapping(address => uint256)) public profileSharesBalance;
mapping(uint256 => uint256) public profileSharesSupply;
function buyProfileShares(uint256 profileId, uint256 amount) external payable {
// Верифікуємо власника profile через Lens Hub
address profileOwner = lensHub.ownerOf(profileId);
// комісії йдуть profileOwner
// ...
}
Інтеграція Farcaster — інший децентралізований social протокол. Farcaster ID (FID) використовується замість адреси:
mapping(uint256 => mapping(address => uint256)) public fidSharesBalance; // fid → покупець → amount
function verifyFidOwnership(uint256 fid, address claimer, bytes calldata proof) external {
// Верифікуємо через Farcaster key registry
require(farcasterKeyRegistry.isSigner(fid, claimer), "Not FID owner");
fidOwners[fid] = claimer;
}
MEV та захист від front-running
Bonding curve транзакції вразливі до sandwich атак: бот бачить вашу buy у mempool, купує перед вами, продає після, захоплює різницю.
Захист мінімального виходу:
function buySharesWithProtection(
address sharesSubject,
uint256 amount,
uint256 maxPrice // максимальна ціна яку готові заплатити
) external payable {
uint256 price = getBuyPriceAfterFee(sharesSubject, amount);
require(price <= maxPrice, "Price too high (slippage)");
require(msg.value >= price, "Insufficient ETH");
// ... логіка покупки
}
Commit-reveal для великих покупок:
mapping(bytes32 => address) public pendingBuys;
mapping(bytes32 => uint256) public commitBlocks;
function commitBuy(bytes32 commitment) external payable {
pendingBuys[commitment] = msg.sender;
commitBlocks[commitment] = block.number;
}
function revealBuy(
address subject,
uint256 amount,
bytes32 salt
) external {
bytes32 commitment = keccak256(abi.encodePacked(subject, amount, salt, msg.sender));
require(pendingBuys[commitment] == msg.sender, "No commit");
require(block.number > commitBlocks[commitment], "Same block");
require(block.number <= commitBlocks[commitment] + 10, "Expired");
delete pendingBuys[commitment];
// ... виконуємо покупку
}
Розширення базової механіки
Модель підписки: крім ключів — щомісячна підписка за доступ до контенту. Власники ключів отримують довічний доступ, інші — через recurring платіж.
mapping(address => mapping(address => uint256)) public subscriptionExpiry;
function subscribe(address subject) external payable {
uint256 price = getSubscriptionPrice(subject);
require(msg.value >= price, "Insufficient ETH");
// Продовжуємо або встановлюємо підписку
uint256 currentExpiry = subscriptionExpiry[subject][msg.sender];
uint256 newExpiry = max(currentExpiry, block.timestamp) + 30 days;
subscriptionExpiry[subject][msg.sender] = newExpiry;
// Розподіл: 80% subject, 20% protocol
payable(subject).transfer(msg.value * 80 / 100);
}
function hasAccess(address subject, address user) public view returns (bool) {
return sharesBalance[subject][user] > 0 ||
subscriptionExpiry[subject][user] > block.timestamp;
}
Referral механіка — friend.tech давав referral fees. Проста реалізація:
mapping(address => address) public referrers;
function buySharesWithReferral(
address sharesSubject,
uint256 amount,
address referrer
) external payable {
if (referrers[msg.sender] == address(0) && referrer != msg.sender) {
referrers[msg.sender] = referrer;
}
// При розподілі комісій частина йде referrer
address ref = referrers[msg.sender];
if (ref != address(0)) {
uint256 referralFee = protocolFee * referralPercent / 100;
payable(ref).transfer(referralFee);
protocolFee -= referralFee;
}
// ...
}
Вибір мережі
Base — правильний вибір як і для оригінального friend.tech. Дешево ($0.001–0.01 за транзакцію), EVM, Coinbase onramp (критично для mass market), активна SocialFi екосистема. Альтернатива — Arbitrum, якщо потрібна більш зріла DeFi екосистема навколо.
Таймлайн розробки
| Фаза | Вміст | Час |
|---|---|---|
| Design | Параметри bonding curve, структура комісій, механіка доступу | 1–2 тижні |
| Core контракти | Bonding curve, access control, розподіл комісій | 3–4 тижні |
| Social інтеграція | Twitter/Farcaster/Lens OAuth або smart wallet | 2–3 тижні |
| Backend | API, уведомлення, зашифроване месенджер | 3–4 тижні |
| Mobile-first frontend | Web app (PWA) + підключення гаманця | 4–5 тижнів |
| Anti-MEV & безпека | Захист slippage, аудит | 2–3 тижні |
| Launch | Testnet пілот, influencer seeding | 2–3 тижні |
Всього: 17–24 тижні. Ключовий фактор успіху — не техніка, а bootstrap стратегія: перші 20–30 творців з аудиторією визначають traction. Технічну частину розробляємо, з bootstrap потрібна команда на стороні клієнта.







