Разработка платформы в стиле friend.tech
Friend.tech запустился в августе 2023 на Base, сгенерировал $50M+ в fees за первые месяцы и задал шаблон для целого класса SocialFi приложений: токенизированный доступ к людям. Механика: пользователь привязывает Twitter-аккаунт, другие покупают его «ключи» (shares), цена ключей растёт по bonding curve, держатели ключей получают доступ к приватному чату или контенту. Просто, жадно эффективно с точки зрения retention, и технически интересно.
Копировать friend.tech буквально — проигрышная стратегия (аудитория ушла, рынок насыщен). Но механика bonding curve + gated access применима к десяткам вертикалей: expert networks, fan platforms, creator economy, permissioned DAOs. Разберём как это строится правильно.
Bonding Curve: математика и реализация
Цена ключа определяется количеством уже выпущенных ключей по формуле. Friend.tech использовал:
price(n) = n² / 16000 ETH
где n — текущее количество ключей. Это polynomial curve — цена растёт квадратично. Это создаёт сильный FOMO для ранних покупателей и экспоненциально высокую цену при большом supply.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SocialBondingCurve {
// Маппинг субъект → количество выпущенных ключей
mapping(address => uint256) public sharesSupply;
// Маппинг субъект → holder → количество ключей
mapping(address => mapping(address => uint256)) public sharesBalance;
address public protocolFeeDestination;
uint256 public protocolFeePercent; // в basis points
uint256 public subjectFeePercent; // fee идёт субъекту (owner ключей)
event Trade(
address indexed trader,
address indexed subject,
bool isBuy,
uint256 shareAmount,
uint256 ethAmount,
uint256 protocolEthAmount,
uint256 subjectEthAmount,
uint256 supply
);
// Цена n-го ключа по polynomial curve
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 curve: при большом supply ключи становятся astronomically дорогими. Для нишевых создателей — это работает (ключ «звезды» стоит дорого — окей). Для широкого рынка — барьер входа убивает рост. Альтернативы:
Linear curve: price = base_price + supply × slope — предсказуема, но нет FOMO
Sigmoid curve: сначала быстрый рост (FOMO), потом плато — более управляемая экономика для mass market
// Sigmoid-based price (аппроксимация)
function getSigmoidPrice(uint256 supply, uint256 amount) public pure returns (uint256) {
// k = steepness, midpoint = inflection point
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 tokens
Держатели ключей получают доступ к контенту. Верификация 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
}
Fully 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. Более resilient подходы:
Lens Protocol интеграция — децентрализованный social graph на Polygon. Profile = NFT, followers — on-chain. Ключи привязаны к 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);
// fees идут profileOwner
// ...
}
Farcaster интеграция — другой децентрализованный social protocol. Farcaster ID (FID) используется вместо адреса:
mapping(uint256 => mapping(address => uint256)) public fidSharesBalance; // fid → buyer → 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, покупает перед вами, продаёт после, забирает разницу.
Minimum output protection:
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];
// ... выполняем покупку
}
Расширения базовой механики
Subscription модель: помимо ключей — ежемесячная подписка за доступ к контенту. Держатели ключей получают бессрочный доступ, остальные — через recurring payment.
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;
// Distribute: 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;
}
// При распределении fees часть идёт 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 (критично для масс-маркета), активная SocialFi экосистема. Альтернатива — Arbitrum, если нужна более зрелая DeFi экосистема вокруг.
Этапы разработки
| Фаза | Содержание | Срок |
|---|---|---|
| Design | Bonding curve параметры, fee структура, access mechanics | 1–2 нед |
| Core contracts | Bonding curve, access control, fee distribution | 3–4 нед |
| Social integration | Twitter/Farcaster/Lens OAuth или smart wallet | 2–3 нед |
| Backend | API, notifications, encrypted messaging | 3–4 нед |
| Mobile-first frontend | Web app (PWA) + wallet connection | 4–5 нед |
| Anti-MEV & security | Slippage protection, audit | 2–3 нед |
| Launch | Testnet pilot, influencer seeding | 2–3 нед |
Итого: 17–24 недели. Ключевой фактор успеха — не техника, а bootstrap стратегия: первые 20–30 создателей с аудиторией определяют traction. Техническую часть разработаем, с bootstrap нужна команда на стороне клиента.







