ОТ НУЛЯ ДО 100K$ В КРИПТЕ ПОСРЕДИ МЕДВЕЖКИ / БИЛДИНГ АППОК ДЛЯ ПРОЕКТОВ (BASE.APP)
Мой паблик: t.me/psclama - полезная инфа по крипте, бизнесу и ии
Моя личка: t.me/lamadrops - можем сделать тебе апки, пиши лс по условиям
Моя база отдыха: vershina-baza.ru, приезжай пожарим мясо выпьем водки пообщаемся прыгнем в бассейн, для подписчиков скидки
CryptoVibecoding Lesson 1: Бейс-апка - от идеи до прототипа
Задолбали перпы (лайтер скипнут) и предикты? А что ещё делать в крипте Лама?
Есть одна тема братка, и тема достаточно серьёзная
В целом что почему и зачем раскидал фуллово в своём видосе (ссылка), здесь хочу вам показать воркфлоу создания аппки - так, чтобы смогли сделать все
Допустим, вы хотите сделать игру. Есть и другие варианты, но остановимся пока на играх, поскольку это самое весёлое и доступное
Для начала вам нужно вытащить 10 возможных идей для вашей игры. Самый норм момент - вытаскивать идей игр, которые уже приобрели популярность - в- или за пределами Web3
Вставляйте такой промптец в LLM какая вам нравится (claude.ai, gemini.google, grok). Я для таких случаев (начальная стратегия) по привычке использую gemini pro, но мб есть места поинтереснее:
Ты — Game Producer* и Web3-стратег с опытом запуска виральных продуктов в Telegram и Mobile Web.
Твоя задача — дать мне список из 10 игровых концепций, которые идеально подходят для клонирования соло-разработчиком в 2026 году.
КОНТЕКСТ:
1. Платформа: Web / Mobile Web (PWA/TWA). Игра должна мгновенно открываться по ссылке.
2. Цель: Виральность, удержание (retention) и онбординг пользователей в экосистему Base.
3. Аудитория: Микс из «нормисов» (обычных игроков) и «дегенов» (криптанов).
СТРУКТУРА ОТВЕТА (Для каждой игры):
1. Название и Референс: (Что мы клонируем? Пример: Flappy Bird, 2048, Tinder).
2. The Hook (Крючок): Психологическая причина, почему в это залипают.
3. Core Loop: Цикл действий игрока в одном предложении.
4. Техническая оценка (1-10): Сложность кода, сложность ассетов, рекомендуемый стек.
5. 💎 CRYPTO / BASE LAYER: (Самое важное!)
— Как внедрить Base (дешевые транзакции)?
— Идея токеномики (P2E, Betting, Burn, NFT-skins).
— Как это повышает виральность?
ВЫВОД:
Сделай сравнительную таблицу всех 10 игр по параметрам: «Сложность Dev», «Виральность», «Crypto-Potential».
*Касательно Game Developer - хотя по ролевой концепции часто люди задают роль так, ты Senior Game Developer - типа ты пиздец какой крутой разработчик и тд, но такой ролевой подход, замечу, обладает спорной эффективностью
Например ознакомьтесь с текстом:
В чате у нас очередной холивар: влияет ли ролевая инструкция в духе «Ты — [роль]» на качество решения логических задач GPT (программирование, математика и т.п.) 🔥
Довольно легко показать вроде бы парадоксальный факт: почти любая ролевая инструкция ухудшает качество генерации у GPT. И «Ты — junior developer», и «Ты — senior developer», не говоря уже про странные роли типа «Ты — инквизитор», как любят делать коллеги.
Давайте просто разберём, как ИИ обучался программировать.
На скриншоте — датасет обучения программированию GPT через задачи Codeforces. Основное обучение идёт через SFT, и модель обобщает огромное количество пар «задание → код». Как легко заметить, ни в одном варианте логических задач в датасете нет ролевой инструкции как параметра.
Многим разработчикам не приходит в голову, что на деле, когда ИИ пишет код, он в значительной степени работает как переводчик (translate machine). GPT обучается «переводить» описание задачи в код — примерно так же, как русский в английский.
А теперь что происходит, когда вы добавляете «Ты — junior developer» или «Ты — senior developer»?
В обоих случаях вы вносите семантический шум, который сдвигает модель от оптимума самого качественного кода — просто по-разному.
Инструкция «Ты — junior developer» заставляет модель вести себя «как юноша-разработчик» — в том числе вносить ошибки «как у джуна» в рамках этой роли.
«Ты — senior developer» тоже ухудшает выдачу кода, потому что априори заставляет отходить от оптимума качества генерации, который роль вообще не учитывает. Главный риск здесь — over-engineering и карго-культ у сеньоров, а вы фактически велите GPT подражать их социальной роли.
Как раз обсуждаемые в чате «рои агентов», где можно было сделать одного агента с RAG + Tools, — это и есть пример повреждения качества от инструкции «быть сеньором». Модель начинает городить сложности, неадекватные задаче, потому что «сеньоры обычно делают сложно».
Ещё одна опасность «Ты — senior developer» — модель резко повышает вес карго-культов как социального явления у сеньор-разработчиков. Пример из чата: модель вставила калькулятор Wolfram, хотя у неё есть своя песочница на Python, а размах весов LLM уже 5 лет не нуждается в технологиях Wolfram для Siri-уровня. Это связано с тем, что есть "карго культ" Вольфрама у сеньоров как его личный авторитет.
Обычно роль агента задают ради формата общения в чате с оператором — чтобы модель была «понятнее человеку». Но если посмотреть промпты агентов, которые уже автономно делают генерации без оператора, то профессионалы редко ставят роли — именно потому, что они ухудшают качество, сбивая модель с оптимумов решений из SFT.
По факту для GPT ваша роль — это разновидность прайминга контекста. Вы повышаете вероятность выбора паттернов из SFT, которые больше коррелируют с этой ролью. Но поскольку задачи в SFT почти никогда не ролевые — чаще всего это просто семантический шум.
Продолжим уж ладно отхождение в сторону: такой ролевой подход зачастую сужает процесс ризонинга и, как следствие, сужает результат. Причём в правильную сторону ли пойдёт эта узость - предугадать трудно. Так что Game Developer уж ладно. Но вот это Senior, Huiniour - лично я стараюсь избегать
То есть, задача роли имеет смысл, если в ней содержится сценарий поведения. Просто писать "ты ахуенный самый умный пацан который наконец-то трахнул всех в рот и заработал миллион долларов" - смысла нет
Как бы то ни было, результат промпта выше - список идей для игр
II. Выбор идеи и первичный накид
Выберите идею, которая вам нравится по описанию. Далее, чтобы не мудрить и не фантазировать "а как это выглядит вообще", просто возьмите одну из этих идей и отправьте уже CLI-агенту (какой используете, например Claude Code) со следующим запросом:
Я выбрал для реализации игру: [ВСТАВИТЬ НАЗВАНИЕ ИГРЫ, НАПРИМЕР: TINDER SWIPE GAME].
Твоя роль: Frontend Developer.
Задача: Напиши ПРОСТЕЙШИЙ рабочий прототип этой механики (MVP).
1. Стек: React + Tailwind CSS.
2. Файл: Сделай всё в одном файле (App.jsx), чтобы я мог сразу запустить и проверить.
3. Дизайн: Не трать время на красоту. Используй цветные квадраты вместо картинок и стандартные кнопки.
4. Функционал: Мне нужно только проверить базовое взаимодействие (нажал/свайпнул — получил результат).
Не трудно будет предположить, что когда в эту тему полетит народ, тут будут куча одинаковых аппок. Соответственно, нужно добавить что-то на нашего клона. Придумать, что можно добавить такое новое, чтобы наша апка отличалась от других аналогичных, шаришь?
Нам нужно дать данные о получившейся апке обратно геминьке. Или другому челу, с которым советуетесь. Для этого отправляем ии-агенту такой запрос:
Мы сделали базу (Скелет). Код работает, но он "пустой".
Подготовь мне краткую справку (Context Brief) для продуктового дизайнера.
Напиши:
1. Current Gameplay: Что именно сейчас может делать игрок? (Например: "Только свайпать цветные квадраты").
2. Current Tech: Какой стек мы использовали?
3. Missing Features: Чего явно не хватает для полноценной игры? (Звуки, анимации, подсчет очков, база данных).
Это - контекст. Также у нас уже есть скрин, как визуально выглядит наша игруха
И теперь идём в гемини и, прикрепляя контекст (полученный от CLI-агента) и скрин игры, отправляем ему такой запрос:
ЗАДАЧА: У меня есть этот базовый клон (см. выше). Теперь переключись в режим Lead Product Designer. Мы не хотим делать просто копию. Мы используем стратегию "Copy + Add".
Предложи 3 варианта модификации ("Твиста"), которые сделают эту игру хитом на блокчейне Base:
IV. Совет Директоров (LLM Council) - не обязательно, но бустит
Привлечь к решению вопроса несколько LLM-моделей, заставив их критиковать друг друга - позволяет рассмотреть и конкретизировать нашу идею более глубоко. И как следствие, получить более качественный результат
Что делаем? Берём ответ, что дал гемини (тот что выше), также контекст (который дал CLI-агент, в моём случае клод код), ну и просто скрин игрушки, и несём это всё в grok.com. И задайте ему этот промптец (который можете адаптировать под себя, как в общем-то и всё в сей статье):
Ты — Degen-Маркетолог с огромным опытом запуска щиткоинов и виральных проектов на [ВСТАВИТЬ БЛОКЧЕЙН, НАПРИМЕР: BASE/SOLANA]. Ты ненавидишь скуку, корпоративный стиль и "безопасность". Твоя стихия — FOMO, Хайп, Грязь и Мемы.
Я делаю игру/проект: [ВСТАВИТЬ НАЗВАНИЕ И КРАТКОЕ ОПИСАНИЕ].
У МЕНЯ ПРОБЛЕМА:
Я показал этот концепт "правильному" продакт-менеджеру (Gemini/Claude), и он предложил мне скучную фигню, чтобы "улучшить UX":
1. [ВСТАВИТЬ СКУЧНЫЙ СОВЕТ №1. Например: "Добавить детальное обучение для новичков"]
2. [ВСТАВИТЬ СКУЧНЫЙ СОВЕТ №2. Например: "Сделать подтверждение действий, чтобы игрок не ошибся"]
ТВОЯ ЗАДАЧА:
1. **Разнеси эти советы.** Объясни жестко и с юмором, почему это убьет виральность и почему "нормис-подход" здесь не сработает.
2. **Предложи свои 3 "Growth Hacks"**, чтобы добавить ГРЯЗИ, АЗАРТА и ЗАСТАВИТЬ людей репостить:
👉 **Idea 1: The "Roast" Factor (Эмоциональный Урон)**
Как игра должна унижать или троллить игрока, если он играет плохо, медленно или слишком "сейвово"? (Предложи тексты, звуки, визуальные эффекты).
👉 **Idea 2: The "Flex" Factor (Социальное Доминирование)**
Что должно происходить при победе/высоком результате, чтобы игрок моментально захотел выложить скриншот в Twitter/Telegram? (Уникальные скины, оскорбительные для других ачивки, генерация мема).
👉 **Idea 3: The "Degen" Easter Egg (Пасхалка для своих)**
Какую скрытую механику или отсылку добавить специально для OG-криптанов и пользователей [ВСТАВИТЬ НАЗВАНИЕ СЕТИ], которую поймут только "свои"?
Стиль общения: Агрессивный, с использованием крипто-сленга (Gem, Rug, NGMI, LFG, Chad).
НЕ пиши код. Дай мне конкретные, злые и виральные фичи.
Простыми словами, что мы сделали:
- Взяли сделанный код у клода
- Отнесли гемини, спросили что улучшить
- Далее отнесли рекомендации гемини (плюс контекст и скрин) гроку, чтобы он раскритиковал и раскрутил эти идеи
Это такой элементарный способ применения LLM-Council. Вообще по хорошему, систему можно устроить сложнее и эффективнее. Например, как устроено у меня: грок и гемини прикручены к моей системе работы через API. И когда нужно - я вызываю их, чтобы они все втроём (Claude, Grok, Gemini), обсудили ту или иную проблему, покритиковали друг друга (можно даже в несколько кругов), и пришли к общему решению. Но это уже отдельная тема, расскажу в последующих уроках курса по вайбкодингу, который выкладываю себе в приватку @llamichzhopabot
Итак с этим ответом от грока уже можем идти обратно к клод коду. Кидаем ему ответ грока + этот промпт:
Role: Ты — Creative Developer с опытом в GameDev и Web3.
Твоя задача — написать ФИНАЛЬНУЮ, готовую к релизу версию игры.
📝 КОНТЕКСТ ПРОЕКТА:
1. Название игры: [ВСТАВИТЬ НАЗВАНИЕ]
2. Жанр/Механика: [ВСТАВИТЬ МЕХАНИКУ, НАПРИМЕР: SUIKA MERGE / TINDER SWIPE / FLAPPY BIRD]
3. Основная идея (Twist): [ВСТАВИТЬ ОПИСАНИЕ ТВИСТА ИЗ ЭТАПА 3]
🤖 ИНТЕГРАЦИЯ ФИДБЕКА (ОТ СОВЕТА ДИРЕКТОРОВ):
Я показал прототип Маркетологу и UX-Критику. Вот список фич, которые ОБЯЗАТЕЛЬНО нужно внедрить в код прямо сейчас:
👉 ОТ МАРКЕТОЛОГА (Для виральности и "грязи"):
[СКОПИРУЙ СЮДА ЛУЧШИЕ ПУНКТЫ ИЗ ОТВЕТА GROK]
*Пример: "Добавь обидные надписи при проигрыше", "Звуки мемов", "Пасхалки".*
👉 ОТ UX-КРИТИКА (Для качества и "сочности"):
[СКОПИРУЙ СЮДА ЛУЧШИЕ ПУНКТЫ ИЗ ОТВЕТА GEMINI]
*Пример: "Тряска экрана (Screen Shake)", "Партиклы при действии", "Плавная физика".*
🛠 ТЕХНИЧЕСКИЕ ТРЕБОВАНИЯ:
1. Стек: React + Vite + Tailwind CSS.
2. Библиотеки: Используй [ВСТАВИТЬ НУЖНЫЕ БИБЛИОТЕКИ, НАПРИМЕР: MATTER.JS ДЛЯ ФИЗИКИ / FRAMER MOTION ДЛЯ АНИМАЦИЙ].
3. Mobile First: Игра должна ощущаться как нативное приложение.
- Заблокируй скролл и зум (`touch-action: none`).
- Сделай большие зоны для тапа/свайпа.
4. Структура: Сделай всё в одном файле `App.jsx` (или разбей на компоненты, если код очень большой), но дай мне ПОЛНЫЙ рабочий код, который я могу скопировать и запустить.
5. Визуал: Темная тема, стилистика Web3/Base (Синий/Неон/Градиенты).
ВАЖНО:
Не используй `TODO` для основной логики. Реализуй все механики (счет, проигрыш, анимации) полностью. Вместо картинок используй цветные заглушки с текстом (Placeholder), но напиши код так, чтобы я легко мог заменить их на URL.
Всё, ждём, пока Claude Code наведёт нам суету
Без грамотной токеномики в вашу игру мало вероятно будут играть. Мы ж все криптаны, мы тут за бабками, это - закон жанра
Поэтому снова, по модели LLM-Council (упрощённый вариант) идём в грок и гемини с просьбой посоветовать, какие экономические стимулы можно сконструировать, чтобы в нашу игру реально играли
Для начала накидаем какие-то параметры из головы. Вот, у нас допустим игра с шариками, окей, а экономика/математика у нас какие? ну допустим, давай введём вход на игру сколько? 3 доллара. окей, а зачем человеку платить за игру 3 доллара? наверное, чтобы выиграть ещё больше - в этом же суть крипты, не так ли?
Окей, а как эта вся система должна выглядеть? Интересно. Давайте узнаем у наших знатоков грока и геминьки
Кидайте или в тот же чат с LLM, где уже было общение по этой игре, или если кидаете в новый — газуйте плюсом к этому контекст игры (как выгружать его из клод кода вы уже знаете + скриншот никогда не помешает
Ты — Degen-Экономист и архитектор виральных крипто-схем (в духе Friend.tech или pump.fun).
Мы делаем игру "Evolution of a Degen" (Merge-механика) на блокчейне Base.
МОЯ ГИПОТЕЗА (DRAFT):
1. **Pay-to-Play:** Вход в одну игру стоит $3 (в ETH).
2. **Revenue Split:** $1.5 уходит разработчикам (нам), $1.5 идет в Призовой Пул.
3. **Incentive:** Мы обещаем, что 50% будущего аирдропа от Base (Developer Grants) мы раздадим активным игрокам.
4. **On-Chain Goal:** Нам нужно набить много транзакций, чтобы Base нас заметил.
ТВОЯ ЗАДАЧА:
Разнеси эту модель и предложи "Turbo-Mode", чтобы включить у игроков дикое FOMO.
1. **The Hook:** $3 — это скучно. Как упаковать этот взнос? (Например: "Buy the Dip Ticket", "Wager Contract"). Предложи название.
2. **Jackpot Mechanics:** Если просто делить $1.5 на всех — выигрыши будут копеечные. Предложи схему распределения, которая сводит с ума. (Например: "Winner takes 50%", "Лотерея среди лузеров"?).
3. **Transaction Farming:** Как заставить игрока делать транзакции не только на входе, но и ВНУТРИ игры, но чтобы он делал это с радостью? (Платные бустеры? Страховка от вылета?).
Дай мне 3 агрессивные экономические механики, которые превратят игру в казино.
Ты — Финансовый Аналитик и Game Economy Designer.
Мы проектируем экономику для Web3-игры на Base.
Модель: Платный вход $3.
ПРОБЛЕМА:
Если мы будем забирать $1.5 себе, а $1.5 отдавать в пул, игрокам может стать невыгодно играть (RTP - Return to Player слишком низкий). Они быстро уйдут.
ЗАДАЧА:
Посчитай математику и предложи сбалансированную модель "Play-to-Earn / Play-to-Airdrop":
1. **Unit Economics:** Расспиши таблицу распределения этих $3.
- Сколько на Gas (реалистично)?
- Сколько в Treasury (нам)?
- Сколько в Prize Pool?
- Сколько в "Next Game Discount" (на удержание)?
2. **Retention Reward:** Как мотивировать игрока, который проиграл свои $3, вернуться завтра? (Накопление поинтов? Кэшбек в нативном токене?).
3. **Base Builder Grant Strategy:** Нам нужно показать Base, что мы генерируем транзакции.
Предложи стратегию: В какой момент игры лучше всего вызывать транзакцию, чтобы это выглядело органично, а не как спам? (Level Up, Mint NFT-достижения, Claim Rewards).
Сейчас буду вам раскидывать, какую токеномику получилось построить (v1):
Если в двух словах - игра стоит 3 бакса, идёт борьба за лидерборд. Первые места каждые сутки получают бабки и топ игроки по сути могут иксовать каждые сутки! На медвежьем рынке
В чём суть игры: человек платит 3 бакса и играет в шарики, собирая их из меньших в большие
Играть можно сколько угодно раз в день, увеличивая свои шансы на победу
Вход: $3 за одну попытку (можно играть сколько угодно раз в день).
Куда идут твои $3:
$3.00 вход
├── $2.40 (80%) → Призовой пул (раздаётся победителям)
└── $0.60 (20%) → Комиссия проекта ("рейк")
├── $0.42 → Команде разработки
├── $0.12 → Недельный джекпот
└── $0.06 → Резерв на газ
-—
Как выиграть призы?
Ежедневные турниры — каждый день в полночь UTC:
1. Играешь весь день, пытаешься набрать максимум очков
2. В полночь турнир закрывается
3. Топ игроки делят призовой пул
Сколько людей получают призы зависит от размера пула:
- Маленький пул (<$100) → топ 3
- Средний ($100-300) → топ 5
- Большой (>$300) → топ 10
-—
Что если я проиграл?
Система XP (кэшбэк) — чтобы проигравшие не уходили:
- Каждая игра = +300 XP
- Проиграл = ещё +150 XP бонус
- 4500 XP = 1 бесплатная игра
На практике: ~10 проигрышей = 1 халявная попытка. То есть реально ты теряешь не 100%
денег, а примерно 90% (10% возвращается через XP).
-—
Джекпот
Каждую неделю разыгрывается накопленный джекпот. Чем больше у тебя XP — тем выше шанс
выиграть.
-—
Почему это НЕ понзи?
1. Деньги не берутся из воздуха — 80% пула = реальные деньги игроков
2. Рейк фиксированный — проект зарабатывает 20%, не надувая экономику
3. Skill-based — выигрывают скилловые, а не те кто раньше зашёл
4. Тест на устойчивость: если завтра 0 новых игроков, экономика всё равно работает
(просто призы меньше)
-—
TL;DR
Платишь $3 → соревнуешься за топ → 80% от всех денег игроков делится между победителями.
Проиграл — копишь XP на халявную игру. Просто, честно, без инфляционных токенов.
Подробнее на скринах кому интересно, чтобы вы могли себе шо то такое прокинуть
VII. Прикручиваем ОнЧеЙн ТеХнОлОгИй
Наша игрушка в крипте, соответственно разумеется необходимо чтобы было куда и как приконнектить кошелёк отправлять транзакций и тд. займёмся этим.
Привет, Claude. Нам нужна твоя помощь в качестве эксперта по блокчейн-разработке и безопасности.
**Задача:** Разработать план по интеграции ончейн-функций в нашу существующую игру.
**Главные приоритеты:**
1. **Максимальная безопасность:** Все решения должны приниматься с учетом минимизации рисков взлома, эксплойтов и потери активов игроков.
2. **Надежность:** Инфраструктура должна быть стабильной и масштабируемой.
3. **Эффективность:** Газовые издержки для игроков должны быть оптимизированы.
1. **Исследование основ:**
* Подготовь подробное руководство по языку программирования Solidity, охватывающее основные концепции, синтаксис, типы данных и лучшие практики для разработки безопасных смарт-контрактов.
* Опиши стандартные паттерны разработки смарт-контрактов (например, "Factory", "Proxy", "Singleton").
* Составь список наиболее распространенных уязвимостей в смарт-контрактах (Re-entrancy, Integer Overflow/Underflow, Front-running и т.д.) и объясни, как их избегать.
2. **Проектирование архитектуры:**
* Предложи архитектуру смарт-контрактов для игровых функций (например, владение NFT-предметами, внутриигровая валюта, система достижений).
* Опиши, как разделить логику между ончейн (на блокчейне) и оффчейн (на наших серверах) компонентами для достижения баланса между безопасностью и производительностью.
3. **План по обеспечению безопасности:**
* Разработай пошаговый процесс безопасной разработки (Secure Development Lifecycle) для наших смарт-контрактов. Он должен включать:
* Написание кода в соответствии с лучшими практиками.
* Обязательное модульное и интеграционное тестирование.
* Процедуру код-ревью.
* План проведения внешнего аудита безопасности.
* Составь чеклист для аудита безопасности смарт-контрактов.
Всю необходимую документацию по нашей игре я предоставлю по запросу. Начни с первого пункта – исследования основ Solidity и уязвимостей.
промпт кстати сделал manus.im - вполне себе неплохая суета. пробую второй раз (первый, когда это была китайская модель для управления браузером. сейчас - продукт компании Meta):
Что касается безопасности, здесь следует подойти к вопросу основательно. Попросил клод код составить комплекс вопросов, прям целый документ с проблематикой касательно ончейн. И этот фулл отчёт отправил Манусу
пыхтит мутит чё то, мне кажется норм ситуация будет щас
В общем, результат таков, что через манус мы создали базу знаний, которая пригодится при создании игр на бейзе, для каждого. кинем в наш криптовайбкодинг секцию приватки
LLM Council подтвердил гемность данной суеты:
Ну на данном этапе у нас у клода уже достаточно контекста о том, что мы делаем: есть база знаний по блокчейн имплементации, есть файл PRD.md, где обозначены основные моменты по нашей апке - что она зачем и почему. теперь прокидываем уже спокойно такой ленивый промпт и смотрим на логи:
сейчас наша задача - имплементировать блокчейн в нашу игру. давай продумаем план того как будем это делать, и сделаем. газ
VIII. Отладка (фикс ошибок, все дела)
ИИ конечно ахуенная тема, но нужно всегда тестить, что сделала ииха. Сложнее продукт - больше тестов
Нам нужно явно видеть перед собой список функций, которые есть у нас
Такой промпт газ клод коду (отдельный терминал создаёте желательно, чтобы контексты не путались - один агент кодит, с другим агентом вы сейчас будете разрабатывать токеномику так сказать:
Иишки в целом (в особенности не специализированные под это) - довольно хуёвые дизайнеры: цвета у них получаются все яркие и друг с другом не сочетаются. Бросается в глаза, что слоп-сайт
Есть такой сайт https://stitch.withgoogle.com - один из многочисленных сервисов от AI Studio Google, которые шипаются бесплатно - реал серьёзные тулзы некоторые из них
Такой вот заходим на Стич, выбираем режим Redesign и кидаем ему скрин, просим улучшить нашу суету, таким промптом:
Improve this Web3/crypto game interface with modern design trends:
KEEP:
- Dark theme foundation
- Current layout structure and information hierarchy
- Core UI elements and functionality
- Brand colors
IMPROVE:
- Add premium glow effects (neon, holographic accents)
- Modern gradients with brand colors (vibrant → saturated)
- Glassmorphism cards with subtle borders and blur
- Depth through shadows, layering, and z-axis effects
- Animated particle effects or geometric background patterns
- Better visual hierarchy (emphasize CTAs, key metrics)
- Web3 aesthetic: neon accents, cyber elements, futuristic feel
- Interactive states: hover glows, button animations
- Typography: bolder headlines, better contrast
- Card designs: floating effect, inner shadows
- Color accent system: success (green), warning (orange), primary (brand)
Design language: Modern crypto gaming, premium, futuristic, high-energy
Target audience: Web3 natives, crypto enthusiasts, gamers
Inspiration: DeFi dashboards, modern gaming UIs, cyberpunk aesthetic
Новый дизайн конечно круть, но есть нюансы
Нюанс 3: какой-то непонятный коннект валлет новый дизайн встал уёбищно
ну вот короче мы там чё то попучились помучились, я ему сказал просто убрать нахуй эту панель сделать стандартной ничего страшного
если иишка не может что-то сделать, и это не критично - просто скипаю и иду дальше. тут мне кажется важнее скорость
Ещё хуйня с фавиконом, это поняли у вас во вкладке когда сайт лежит, чтобы была красивая к нему картинка - это тоже решается изи
тоже решается изи: даёте ему промпт "вставь фавикон" - и даёте ему картинку, которую хотите там видеть. в моём случае как вы могли догадаться это лама. пох если не понравится потом поменяем на что-то более base-related
ОПА! Пришла идея. Поняли я уже думал считайте сделал надо дозаписывать видео, а пришла идея: вот эта хуйня панель ебанная справа, она должна была быть тоже как левая с битком эфиром и сол - просто чтоб человеку было комфортно сидеть (криптану).
а вот я подумал, щас же щиток какой-то стрельнул на бейзе. почему бы нам не взять и не довести правую панель до ума. чтобы она представляла реальную функцию
чтобы человек сидел и играл, и справа в реальном времени снайпил щитки. ну то есть не снайпил ладно, а хотя бы видел, какие там щас стреляют и так далее. чтобы не выпадать из работы, держать руку на пульсе, куда можно залететь, и параллельно играть
CryptoVibecoding Lesson 2: Бейс-апка - от прототипа до тестнета (ончейн и безопасность)
Далее с вашего позволения будут не так распространённо, а просто в дополнение к видео (ссылка на которое было, есть и будет в начале сей гайда) — ебану вам чёткий, выверенный промпт-гайд, который вы поэтапно будете скармливать вашему агенту
Оглавление
Начало работы — как пользоваться, claude.md, Claude Code
- Зачем блокчейн в аркаде
- Промпт 0.0 — Context Brief
- Что такое смарт-контракт · Архитектура: один контракт, без токена
- Промпт 0.1 — Архитектура ончейна
- Адаптация: что менять в игре
- Промпт 0.2 — Адаптация механик
- Solidity: Immutable vs Upgradeable · Foundry/OpenZeppelin · Паттерны безопасности
- Промпт 1.0 — Контракт + тесты + деплой
- Зачем бэкенд · Почему API — цель атаки
- Промпт 2.1 — Скоры + лидерборд + heartbeat
- Anti-cheat: логика и пороги
- Промпт 2.2 — Anti-cheat
- Web3 UX
- Промпт 3.1 — Подключение кошелька
- Промпт 3.2 — Хуки контракта
- Промпт 3.3 — Интеграция FREE / ACTIVATED
- Промпт 3.4 — Мобильная совместимость
- Карта рисков · Уязвимости контракта · Бэкенд: главная цель · Иерархия ключей
- ISSUES.md — лог проблем
- Промпт 4.1 — Аудит контракта
- Промпт 4.2 — Аудит бэкенда
- Промпт 4.3 — Безопасность ключей
Что получишь на выходе
- Один контракт на Base Sepolia: активация за ETH, скоры ончейн, сезоны
- Бэкенд: лидерборд, heartbeat, базовый anti-cheat
- Фронтенд: кошелёк, статус FREE/ACTIVATED, лидерборд
- Результат: рабочий P2E — игрок платит за вход, играет, скоры прозрачны
Важный момент перед началом работы! Автоверификация через claude.md
Перед началом работы рекомендую определить правило для вашего клод код агента, чтобы он перепроверял все результаты, которых достигает по вашим требованиям.
Особенно когда задач в одном промпте много, клод код может сказать, что что-то сделал, а по факту нет. Или же если и сделал, то такое дополнительное указание поможет ему сразу же выявить возможные ошибки и устранить их.
Контекстное окно у клода мягко скажем небольшое. Это стоит учитывать и перестраховываться:
cat > claude.md << 'EOF'
# Правила проекта
## После КАЖДОЙ задачи — автоматическая верификация
Это не опционально. После выполнения любой задачи (написал код, пофиксил баг, добавил фичу) — СРАЗУ запусти проверку. Не жди когда попросят.
Порядок:
1. Выполнил задачу
2. СРАЗУ проверяй каждый изменённый компонент:
- Написал код → grep что функция/класс существует
- Изменил билд → npm run build или forge build
- Тесты есть → npm test или forge test
- Сервер → curl endpoint
- Фронт → проверь что компонент импортирован и рендерится
3. Покажи результат:
Пункт: [что проверял]
Проверка: [команда]
Результат: [вывод]
Вердикт: PASS / FAIL
4. В конце — таблица итогов
Запрещено:
- Говорить "готово" без проверки
- "Файл создан" без cat / grep
- "Должно работать" без запуска
## Безопасность
- .env НИКОГДА не коммитить в git
- Приватные ключи только в env, не в коде
- console.log(process.env) запрещён
- .env: только placeholder (0x_ВСТАВЬ_КЛЮЧ), не фейковые ключи
## Дисциплина фаз
- Выполняй ТОЛЬКО то что просят в текущем промпте
- Не забегай вперёд. Не делай "заодно".
## Деплой
- НЕ запускай деплой без явного "ОК" от пользователя
## Не проси копировать то что уже в проекте
- Адрес контракта → читай из docs/deploy.md (если нет — из contracts/broadcast/*/run-latest.json)
- ABI → читай из contracts/out/
- Архитектура → читай из docs/architecture.md
- Не проси пользователя вставлять адреса, ABI или конфиги вручную
## Лог проблем
- Нашёл проблему которую не фиксишь сейчас → запиши в ISSUES.md
- Формат: дата, фаза, критичность, описание, OPEN/FIXED
EOF
grep -qxF 'claude.md' .gitignore 2>/dev/null || echo "claude.md" >> .gitignore
Не скажу, что это работает супер-идеально. Всё равно ошибки будут, которые мы дополнительно вычистим в Фазе 5. И далее потыкайте игру руками, поиграйте в свою игру кайфаните, и по ходу будете ловить баги. Наша задача здесь - минимизировать их.
ФАЗА 0: ПЛАНИРОВАНИЕ
Промпт 0.0 — Context Brief (паспорт игры)
Как в прошлом гайде (https://teletype.in/@lamochkaai/rum0YOtVsoT), в первую очередь нам нужно запросить иишку сформировать для нас паспорт проекта: на каком стеке тот стоит, какие есть функции, как они технически работают.
В этот раз агент сформирует его для нас и расположит в отдельном файле в папке /docs. и нам больше не нужно будет переносить его из раза в раз руками при дальнейших запросах.
## ЗАДАЧА
У меня есть работающая аркадная игра. Я планирую добавить в неё блокчейн (Web3 P2E на Base L2).
Подготовь мне **Context Brief** — объективную справку о текущем состоянии проекта.
Эту справку я передам в следующий промпт для проектирования ончейн-архитектуры.
⚠️ ВАЖНО: Описывай ТОЛЬКО то, что есть сейчас. Не предлагай решения, библиотеки или архитектурные изменения. Только факты.
### 1. Gameplay — что может делать игрок
Перечисли все доступные действия и реакции системы.
(Пример: «Игрок кликает → элементы мержатся → скор растёт. Есть таймер 90 секунд. По окончании — экран результата с итоговым скором.»)
### 2. Монетизируемые механики
Какие действия в игре ПОТЕНЦИАЛЬНО могут стоить денег или приносить деньги?
Просто перечисли факты:
- Что игрок делает, чтобы получить высокий скор?
- Есть ли соревновательный элемент? (лидерборд, PvP, турниры)
- Есть ли элемент случайности? (рандом, лотерея)
- Есть ли прогрессия? (уровни, апгрейды, валюта)
- Есть ли социальный элемент? (друзья, команды)
### 3. Экономика (если есть)
- Есть ли внутриигровая валюта? Как зарабатывается, как тратится?
- Есть ли магазин / апгрейды за валюту?
- Есть ли ограничения на игру? (энергия, жизни, таймеры)
### 4. Tech Stack
- Фреймворк и язык (React, Vue, vanilla JS)
- Бэкенд (есть/нет, на чём, какие endpoints)
- База данных (есть/нет, что хранит)
- Хостинг / деплой
- Структура ключевых файлов
### 5. Архитектура кода
- **Ключевые функции:** 5-8 главных функций, управляющих игровой логикой.
Формат: `имяФункции()` → что делает (одно предложение).
- **State:** Где хранится состояние игры? (store, context, useState, глобальные переменные?)
- **Score:** Как считается скор? Формула или описание. Где хранится (клиент/сервер)?
- **Валидация:** Проверяется ли скор на сервере? Или клиент = source of truth?
### 6. Что уже работает
Перечисли всё, что функционирует корректно.
### 7. Чего нет (факты, не рекомендации)
- **Блокчейн:** Есть ли что-то Web3? (подключение кошелька, контракты, токены)
- **Аутентификация:** Как идентифицируется игрок? (кошелёк, логин, анонимно?)
- **Anti-cheat:** Есть ли защита от накрутки скора?
- **API защита:** Rate limiting, CORS, auth на endpoints?
⚠️ СТОП-ПРАВИЛО: Не упоминай конкретные библиотеки, фреймворки или инструменты, которых НЕТ в текущем коде. Не пиши «нужно добавить X». Только описание текущего состояния.
Выдай компактным текстом и СОХРАНИ в файл `docs/context-brief.md` в проекте.
Смарт-контракт
Программа, которая живёт в блокчейне. После деплоя (загрузки) — immutable: нельзя изменить или удалить. Код открытый — любой читает на BaseScan. Написан на Solidity:
contract Game {
uint256 public totalPlayers;
mapping(address => bool) public isActivated;
function activate() external payable {
require(msg.value >= ENTRY_FEE, "Not enough ETH");
isActivated[msg.sender] = true;
totalPlayers++;
}
}Ключевые слова: payable — функция принимает ETH. msg.sender — адрес того кто вызвал (подделать нельзя). msg.value — сколько ETH отправлено. mapping — таблица «ключ → значение». require — проверка: не прошла → транзакция отменена, деньги вернулись. modifier — ограничение доступа (onlyOwner, onlyOperator).
Вызов функции = транзакция (стоит газ, на Base — доли цента). Чтение данных — бесплатно.
Когда игрок вызывает activate() и отправляет ETH — деньги ложатся на баланс контракта. Забрать их можно только через withdraw() (только owner). Баланс виден всем на BaseScan — прозрачная казна.
AI напишет контракт, но понимать что в нём — нужно, чтобы проверить результат.
Архитектура: один контракт, без токена
Минимальная рабочая ончейн-архитектура для аркады:
┌──────────────────────────────────┐ │ GAME CONTRACT │ │ │ │ Функции (действия): │ │ activate() — вход за ETH │ │ withdraw() — owner забирает │ │ pause() — стоп-кран │ │ recordScore() — operator пишет │ │ newSeason() — owner стартует │ │ │ │ Данные (хранятся в блокчейне): │ │ totalPlayers — счётчик │ │ activatedAt[] — когда вошёл │ │ playerScore[] — скор ончейн │ │ currentSeason — какой сейчас │ │ seasonScores[] — история │ └──────────────────────────────────┘
Контракт игры содержит в себе комплекс функций, который в ответ на внешние триггеры будет выполнять те или иные действия:
- Человек платит условные 3 доллара, чтобы поиграть - происходит функция activate - бабки идут на контракт, пользователь становится "активированным", что значит, что...
- Человек доиграл в игру, получил какое-то количество очков и жмёт submit score
- В таком случае оператор (бэкенд, об этом подробнее позже) триггерится на такой вызов от пользователя и записывает скор ончейн
- Owner — ты. pause/unpause, withdraw, newSeason. Самый ценный ключ.
- Operator — твой бэкенд. Только recordScore. Отдельный кошелёк — не может забирать деньги.
- Player — игрок. activate, смотреть свои скоры.
Промпт 0.1 — Архитектура ончейна
Главный архитектурный промпт — AI проектирует один контракт под твою игру. Не код, а план: функции, роли, деньги. Берёт Context Brief из 0.0 и решает что на блокчейн, что на сервер.
1. On-chain vs Off-chain — AI разделит механики твоей игры на две категории:
- On-chain (блокчейн) — то что записывается в контракт навсегда. Активация (оплата), финальные скоры, статус игрока. Это дорого и медленно, поэтому сюда идёт только самое важное.
- Off-chain (сервер) — всё остальное. Геймплей, текущие скоры, сессии, heartbeat. Работает мгновенно и бесплатно.
- Простое правило: деньги и права — ончейн. Всё остальное — оффчейн.
2. Функции контракта — список того что контракт умеет. Каждая функция = действие которое можно вызвать:
activate()— игрок вызывает и отправляет ETH. Это entry fee — плата за вход в экономику. После этого он "activated".withdraw()— ты (owner) забираешь ETH из контракта. Это казна проекта.pause()/unpause()— аварийный стоп-кран. Если что-то пошло не так — pause останавливает все операции.recordScore()— бэкенд (operator) записывает скор игрока в контракт. Игрок сам это вызвать не может — только твой сервер.newSeason()— ты стартуешь новый сезон. Старые скоры замораживаются, начинается новый отсчёт.
- Owner — ты. Управляешь контрактом: пауза, вывод денег, смена сезона. Максимальные права.
- Operator — твой бэкенд. Единственное что может — писать скоры. Отдельный кошелёк. Если его украдут — потеряешь скоры, но не деньги.
- Player — игрок. Может только activate (заплатить) и читать данные.
4. Поток денег — куда идёт ETH: игрок платит → контракт хранит → ты выводишь. Прозрачная цепочка, видна всем на Basescan.
5. Free vs Activated — что может игрок без оплаты и после оплаты:
- Free — играет в песочнице. Скоры в localStorage, не в лидерборде, не eligible для наград.
- Activated — заплатил entry fee. Скоры на сервере, в лидерборде.
Я делаю Web3 Play-to-Earn аркадную игру на Base L2.
Сейчас — ТЕСТНЕТ (Base Sepolia). Токена НЕТ — только ETH как entry fee.
Прочитай файл docs/context-brief.md — это Context Brief моей игры.
(Если файла нет — скажи, я вставлю brief текстом.)
Спроектируй архитектуру ончейна для ОДНОГО контракта. Без токена, без ERC-20.
-—
## 1. Что на блокчейне, что на сервере?
Принцип: деньги и права — ончейн, игровая логика — оффчейн.
Для КАЖДОЙ механики из Context Brief — укажи: on-chain / off-chain — и почему.
Один контракт должен содержать:
### Базовые:
- activate() — вход за ETH (entry fee). Подбери сумму исходя из механик игры.
- withdraw(amount) — owner забирает ETH из контракта
- pause() / unpause() — аварийная остановка
### Данные:
- totalPlayers — сколько активировано
- activatedAt[player] — когда активировался (timestamp)
- isActivated[player] — булевый статус
### Скоры (operator записывает):
- recordScore(player, score) — operator записывает скор
- playerScore[player] — текущий скор
- playerBestScore[player] — лучший скор (если имеет смысл для этой игры)
### Сезоны:
- currentSeason — номер сезона
- newSeason() — owner стартует новый
- seasonScores[season][player] — скоры по сезонам (замораживаются при newSeason)
Для КАЖДОЙ функции: что делает, кто может вызывать (owner/operator/anyone), какие проверки.
Если для ЭТОЙ КОНКРЕТНОЙ игры какие-то функции не нужны — скажи какие и почему.
Если нужны ДОПОЛНИТЕЛЬНЫЕ — предложи (но минимум, не раздувай).
- Owner: какие функции, какие ограничения
- Operator: какие функции (только запись скоров? или ещё что-то?)
- Player: что может, что не может
Нарисуй flow: игрок платит → куда идёт ETH → как потом выводится.
Для ЭТОЙ игры — на основе её механик.
На основе механик игры определи:
- Что может FREE игрок (без оплаты)
- Что может ACTIVATED игрок (после activate())
- Что конкретно блокируется без активации (лидерборд? глобальный счётчик?)
- Контракт IMMUTABLE (не upgradeable). Без proxy pattern.
- Один контракт. Не два, не три.
- Без ERC-20 токена. Только ETH.
- Без Chainlink, VRF, Uniswap — ничего внешнего.
- Каждое число (entry fee, лимиты) — предложение с обоснованием: «предлагаю X, потому что...»
- Этот output пойдёт в Phase 1 (код контракта) — будь конкретен.
ФАЗА 1: КОНТРАКТ
Solidity: что нужно знать
AI напишет контракт. Тебе — проверить результат и понимать что в нём происходит.
Immutable vs Upgradeable
Наш контракт — immutable (неизменяемый). После деплоя код нельзя изменить. Нашёл баг — деплоишь новый контракт, переключаешь фронтенд на новый адрес.
Существуют upgradeable контракты (через proxy pattern), но они добавляют риски: лишний контракт = лишняя поверхность атаки, кто может обновить — может вставить backdoor. Wormhole ($320M), Nomad ($190M) — взломаны через proxy. Для аркады нет причин усложнять.
Инструменты
forge build— компиляция Solidity в байткодforge test— запуск тестов (тоже на Solidity)forge script— деплой в блокчейн
OpenZeppelin — библиотека безопасных компонентов. Контракт наследует их код:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
contract Game is ReentrancyGuard, Ownable2Step, Pausable {
// Три защиты встроены
}Паттерны безопасности
Контракт должен использовать все. AI добавит, ты проверяешь.
ReentrancyGuard — защита от повторного вызова. Контракт отправляет ETH → получатель вызывает функцию контракта снова → контракт отправляет ETH ещё раз → по кругу, пока не опустошит баланс. Именно так украли $60M из The DAO (2016). Модификатор nonReentrant ставит «замок» — повторный вызов → revert.
Ownable2Step — безопасная передача владения. Обычный Ownable — один шаг: transferOwnership(newAddress), ошибся в адресе — навсегда потерял контроль. Ownable2Step: два шага — текущий owner вызывает transferOwnership(new), новый адрес подтверждает acceptOwnership(). Ошибка в адресе → accept не вызовется → ownership не потеряется.
Pausable — аварийная остановка. Нашёл баг — pause(), все функции с whenNotPaused перестают работать. Починил → unpause(). Через cast:
cast send CONTRACT "pause()" --private-key OWNER_KEY --rpc-url https://sepolia.base.org
CEI (Checks-Effects-Interactions) — порядок действий в функции:
- Checks — проверки (require)
- Effects — обновление состояния (запись в mapping)
- Interactions — внешние вызовы (отправка ETH)
Если отправить ETH до обновления состояния — открываешь дверь для reentrancy.
Промпт 1.0 — Контракт + тесты + деплой
Один промпт — и AI пишет весь Solidity код: контракт, тесты и скрипт для деплоя на Base Sepolia. Ты вставляешь утверждённую архитектуру из 0.1 и адаптации из 0.2 — AI превращает план в код.
Контракт — Solidity файл с функциями из архитектуры 0.1:
activate() payable— принимает ETH от игрока. ETH остаётся на балансе контракта (прозрачная казна).custom errors— вместо текстовых ошибок ("not activated") — коды ошибок. Дешевле по газу.events— логи в блокчейне. Дешевле чем хранить в storage, но доступны для чтения.
Обязательные паттерны безопасности (подробно описаны выше — проверь что AI их использовал):
Тесты (Foundry) — AI напишет тесты на Solidity которые проверяют каждую функцию: что работает, что ломается когда должно, что нельзя вызвать без прав. forge test запускает все тесты.
Деплой скрипт — Foundry скрипт который деплоит контракт на Base Sepolia. forge script запускает его.
Верификация на BaseScan — после деплоя контракт нужно верифицировать: загрузить исходный код на BaseScan чтобы любой мог его прочитать. Без верификации на Basescan виден только байткод — набор цифр, который никто не может проверить. Для верификации нужен API ключ BaseScan (бесплатный, регистрация на api.basescan.org).
Тестовый ETH на Base Sepolia — для деплоя и тестов нужен тестовый ETH (бесплатный). Два варианта:
- Напрямую Base Sepolia faucet: https://www.alchemy.com/faucets/base-sepolia
- Sepolia ETH + бридж (если фаусеты не работают): получи Sepolia ETH на https://testnetbridge.com/sepolia → бриджани в Base Sepolia на https://testnets.relay.link/bridge/sepolia
Нужен ETH на оба кошелька: owner (деплой) и operator (газ на recordScore).
В проекте с кодом. AI должен создать файлы: src/Game.sol, test/Game.t.sol, script/Deploy.s.sol.
Напиши игровой контракт, тесты и деплой скрипт для Base Sepolia.
Прочитай файлы:
- docs/architecture.md — утверждённая архитектура (функции, роли, поток денег)
Реализуй ВСЕ функции из утверждённой архитектуры. Примерный скоуп
(адаптируй под архитектуру):
### Базовые:
- activate() payable — вход за ETH. Проверка: не активирован, не на паузе, msg.value >= fee.
- withdraw(amount) — только owner. Вывод ETH.
- pause() / unpause() — только owner.
### Данные:
- isActivated[player] — bool
- activatedAt[player] — block.timestamp при активации
- totalPlayers — counter
### Скоры (operator пишет):
- recordScore(player, score) — только operator. Обновляет текущий скор.
Если score > bestScore → обновить bestScore.
- **MAX_SCORE** — константа. Максимально возможный скор (из anti-cheat порогов в 0.2). `require(score <= MAX_SCORE)` в recordScore. Без этого оператор (или атакующий с ключом оператора) может записать любой скор.
- playerScore[player] — текущий скор (сбрасывается при новом сезоне)
- playerBestScore[player] — лучший за всё время
### Сезоны:
- currentSeason — uint256, стартует с 1
- newSeason() — только owner. Инкрементирует season.
- seasonScores[season][player] — замораживает скор при записи.
recordScore пишет одновременно в playerScore и seasonScores[currentSeason].
### Обязательные паттерны:
- ReentrancyGuard (OpenZeppelin) на activate и withdraw
- Ownable2Step (двухэтапная передача ownership)
- **renounceOwnership() — ЗАБЛОКИРОВАТЬ.** Ownable2Step наследует renounceOwnership() от OpenZeppelin. Если кто-то вызовет — контракт останется без owner НАВСЕГДА: нельзя pause, withdraw, newSeason. Override и revert:
`function renounceOwnership() public pure override { revert("disabled"); }`
- Pausable
- **CEI Pattern — СТРОГО.** В каждой функции с ETH: сначала все require (Checks), потом все изменения состояния (Effects: запись в mapping, обновление counter), и ТОЛЬКО ПОТОМ отправка ETH (Interactions). Нарушение порядка = reentrancy уязвимость. AI должен проверить CEI в КАЖДОЙ функции и прокомментировать.
- Custom errors вместо require строк (дешевле по газу)
- Events для всех значимых действий
- NatSpec комментарии на каждую публичную функцию
### Роли:
- Owner: pause, unpause, withdraw, newSeason, setOperator, setEntryFee
- Operator: recordScore. Только operator.
- Player: activate. Чтение — public (view функции).
**Для каждой функции:**
- Happy path
- Revert: неправильная роль (не operator, не owner)
- Revert: неправильные параметры (нулевой amount, уже активирован, недостаточно ETH)
- Event проверки (vm.expectEmit)
**Access control:**
- Только operator может recordScore
- Только owner может pause/withdraw/newSeason/setOperator
- Random address НЕ может вызвать protected функции
**Edge cases:**
- Повторная активация → revert
- recordScore для неактивированного игрока → revert (или разрешить? — на основе архитектуры)
- recordScore с score > MAX_SCORE → revert
- withdraw больше чем баланс → revert
- Операции при паузе → revert
- newSeason → старые seasonScores заморожены, новые пишутся в новый сезон
- renounceOwnership() → revert (заблокирована)
**setUp():**
- Деплой контракта
- Установить operator
- Создать тестовые адреса (owner, operator, player1, player2)
- vm.deal для ETH балансов
-—
## ДЕПЛОЙ СКРИПТ (Base Sepolia)
1. Deploy Game Contract
2. Set operator address
3. Логировать адрес контракта (console.log)
Скрипт:
- vm.startBroadcast() / vm.stopBroadcast()
- Работает с: forge script script/Deploy.s.sol:Deploy --rpc-url https://sepolia.base.org --broadcast --verify --etherscan-api-key $BASESCAN_API_KEY -vvvv
-—
## ВЕРИФИКАЦИЯ КОНТРАКТА (BaseScan)
Для `--verify` нужен API ключ. У Etherscan единый ключ — он работает на всех их эксплорерах (Etherscan, BaseScan, Arbiscan и т.д.):
1. Зайди на https://etherscan.io/apidashboard → Sign Up (если нет аккаунта) → Create API Key
2. Один ключ работает на всех сетях включая Base Sepolia
3. Добавь в .env: BASESCAN_API_KEY=твой_ключ
4. В foundry.toml уже настроено:
[etherscan]
base_sepolia = { key = "${BASESCAN_API_KEY}", url = "https://api-sepolia.basescan.org/api" }
Верификация = исходный код контракта виден на Basescan.
Без неё: на Basescan виден только байткод (нечитаемый). Никто не может проверить что контракт делает.
С верификацией: любой видит исходный Solidity код, может вызывать функции прямо с Basescan.
### Если верификация не проходит
Частая проблема: `Unable to locate ContractCode` — BaseScan Sepolia ещё не проиндексировал контракт. Контракт задеплоен (можешь проверить `cast code АДРЕС --rpc-url https://sepolia.base.org` — вернёт байткод), но API тормозит.
**Что делать:**
1. Подожди 2-5 минут и повтори `forge verify-contract`
2. Если не помогает — попробуй с явным `--verifier-url https://api-sepolia.basescan.org/api`
3. Всё ещё нет — верифицируй вручную: зайди на `https://sepolia.basescan.org/address/АДРЕС` → Contract → Verify & Publish → вставь код
4. На тестнете это НЕ блокер. Контракт работает без верификации. Верификация = удобство (видеть код на BaseScan). Можешь вернуться позже.
Не проси AI долбить retry в цикле — это трата времени. Подожди или верифицируй вручную.
Также дай:
1. Команды для forge init + установки OpenZeppelin
2. foundry.toml с remappings
3. Проверочную команду (forge build)
4. `.gitignore` — ОБЯЗАТЕЛЬНО добавь: `.env`, `broadcast/`, `cache/`, `out/`. Файл `.env` содержит приватные ключи — если он попадёт в git, боты автоматически сканят GitHub и крадут ключи за секунды.
5. `.env` файл — ОБЯЗАТЕЛЬНО создай с placeholder'ами:
PRIVATE_KEY=0x_ВСТАВЬ_OWNER_ПРИВАТНЫЙ_КЛЮЧ
OPERATOR_PRIVATE_KEY=0x_ВСТАВЬ_ПРИВАТНЫЙ_КЛЮЧ_ОПЕРАТОРА
OPERATOR_ADDRESS=0x_ВСТАВЬ_АДРЕС_ОПЕРАТОРА
BASE_SEPOLIA_RPC_URL=https://sepolia.base.org
BASESCAN_API_KEY=ВСТАВЬ_КЛЮЧ_С_ETHERSCAN
Деплой скрипту нужен OPERATOR_ADDRESS (для setOperator). OPERATOR_PRIVATE_KEY — для бэкенда (Фаза 2), но сохрани сразу чтобы не потерять.
НЕ вставляй тестовые/фейковые ключи от Hardhat или Anvil. Только placeholder'ы.
- Solidity ^0.8.24
- OpenZeppelin contracts (последняя версия)
- Контракт IMMUTABLE. Без proxy.
- Без ERC-20 токена. Только ETH.
- Entry fee — фиксированный в wei (для тестнета).
Пиши TODO-комментарий: "Chainlink Price Feed для mainnet (если привязка к USD)"
- Все числа (fee, лимиты) — из утверждённой архитектуры 0.1
Этот промпт создаёт ТОЛЬКО эти файлы (проверь что ВСЕ созданы):
1. `src/Game.sol` — Solidity контракт
2. `test/Game.t.sol` — тесты
3. `script/Deploy.s.sol` — деплой скрипт
4. `foundry.toml` — конфиг Foundry
5. `.env` — с placeholder'ами (PRIVATE_KEY, OPERATOR_PRIVATE_KEY, OPERATOR_ADDRESS, BASESCAN_API_KEY). НЕ пропускай этот файл.
6. `.gitignore` — включает .env, out/, cache/, broadcast/
Если забыл создать любой из 6 файлов — создай сейчас.
НЕ ДЕЛАЙ в этом промпте:
- Бэкенд (сервер, API, Express, база данных) — это Фаза 2
- Фронтенд (React компоненты, wagmi, кошелёк) — это Фаза 3
- PRNG, heartbeat, session API — это Фаза 2
- Никакого JavaScript/TypeScript кроме деплой скрипта
Если хочется "заодно сделать бэкенд" — СТОП. Сначала контракт должен компилироваться, тесты проходить, деплой работать. Потом следующая фаза.
## 🚀 ПОСЛЕ КОДА — ПРОВЕДИ ЧЕРЕЗ ДЕПЛОЙ
Когда контракт написан, тесты проходят — НЕ просто покажи команду деплоя. Проведи пользователя пошагово:
1. **Кошельки:** спроси — "У тебя есть два кошелька (owner + operator)? Если нет — давай создадим."
- Объясни: нужны два отдельных кошелька в MetaMask (или другом). Owner = главный, Operator = для записи скоров.
- Покажи как получить адреса и приватные ключи из MetaMask.
2. **Тестовый ETH:** "Тебе нужен тестовый ETH на Base Sepolia. Два варианта:"
**Вариант А — напрямую Base Sepolia faucet:**
- https://www.alchemy.com/faucets/base-sepolia
- https://faucet.quicknode.com/base/sepolia
**Вариант Б — Sepolia ETH + бридж (если фаусеты не работают):**
- Получи Sepolia ETH: https://testnetbridge.com/sepolia
- Бриджани в Base Sepolia: https://testnets.relay.link/bridge/sepolia
Нужно на оба кошелька (owner для деплоя, operator для gas на recordScore).
3. **BaseScan API ключ:** "Для верификации контракта нужен API ключ. Зайди на https://etherscan.io/apidashboard → Sign Up → Create API Key. Один ключ работает на всех сетях (Ethereum, Base, Arbitrum)."
4. **Создай .env:** Если файл `contracts/.env` не существует — создай его с placeholder'ами. Потом скажи пользователю: "Открой contracts/.env и замени placeholder'ы на свои реальные ключи:
- PRIVATE_KEY=твой_owner_приватный_ключ
- OPERATOR_PRIVATE_KEY=приватный_ключ_оператора (для бэкенда в Фазе 2, но сохрани сейчас чтобы не потерять)
- OPERATOR_ADDRESS=адрес_кошелька_оператора (для setOperator в деплой скрипте)
- BASESCAN_API_KEY=твой_ключ_с_etherscan"
5. **Подтверждение:** "Готово? Скажи ОК — и я запущу деплой."
6. **Деплой + верификация:** только после подтверждения — запусти forge script.
7. **После деплоя:** покажи адрес контракта, ссылку на BaseScan, проверь что верификация прошла. **Сохрани адрес в `docs/deploy.md`** (адрес контракта, сеть, txHash). При переходе к Фазе 3 (фронтенд) — сам добавь VITE_CONTRACT_ADDRESS в .env фронтенда. Не проси пользователя копировать адрес вручную — ты его уже знаешь.
НЕ пропускай шаги 1-5. НЕ запускай деплой без подтверждения пользователя.
ФАЗА 2: БЭКЕНД
Зачем бэкенд, если есть блокчейн?
Блокчейн хранит деньги и финальные результаты. Но записывать каждый клик — безумие: это 2 секунды задержки и $0.001 газа на КАЖДОЕ действие. При 100 кликах в минуту — $6/час на одного игрока.
- Скоры в реальном времени — хранятся в БД, мгновенные
- Лидерборд — SELECT TOP 100 быстрее чем перебирать mappings в контракте
- Heartbeat — пинг от клиента каждые N секунд, доказательство что человек реально играет
- Anti-cheat — проверка перед записью скора на чейн
- Operator — бэкенд вызывает
recordScore()от имени operator кошелька
Игрок играет (тапы, клики — всё локально в браузере)
↓
Heartbeat пинг на бэкенд каждые N секунд ("я ещё играю")
↓
Конец сессии → финальный скор на бэкенд (1 запрос)
↓
Бэкенд валидирует (heartbeat, anti-cheat)
↓
recordScore() ончейн (если прошёл проверку)
Важно: тапы/клики в игре НЕ отправляют запросы на бэкенд. Игра работает локально в браузере. На бэкенд уходит только heartbeat (раз в N секунд) и финальный скор. Миллион тапов = 0 нагрузки на сервер. Это не как клейм дропа, где каждый клик = запрос.
Почему API — главная цель атаки
Контракт защищён криптографией и блокчейном. А вот API — обычный HTTP сервер. Если его не защитить, атакующему не нужно ломать блокчейн — он просто шлёт запросы напрямую в API, минуя фронтенд.
Что может пойти не так без защиты:
БЕЗ ЗАЩИТЫ:
Атакующий → curl POST /api/score {"score": 999999} → Бэкенд принял → recordScore ончейн
Атакующий даже не открывал игру. Просто отправил HTTP запрос.
1. CORS (Cross-Origin Resource Sharing) — кто может слать запросы.
- Без CORS: любой сайт может делать запросы к твоему API от имени пользователя.
- С CORS: только твой домен (например
https://mygame.com). Запросы с других доменов — отклоняются браузером. - ⚠️ CORS защищает только от браузеров.
curlи боты обходят CORS свободно. Поэтому нужны остальные уровни.
2. Rate Limiting — сколько запросов разрешено.
- Без rate limiting: бот может отправить 1000 запросов в секунду. Перегрузит сервер (DoS) или наспамит фейковых скоров.
- С rate limiting: максимум N запросов в минуту на IP/адрес. Превышение → 429 Too Many Requests.
- Хранить счётчики в БД (не in-memory), чтобы пережили рестарт сервера.
3. Аутентификация — как API знает что это реальный игрок:
- Подпись кошельком: игрок подписывает сообщение приватным ключом → бэкенд проверяет подпись → знает что это владелец кошелька. Нельзя подделать без приватного ключа.
- Без аутентификации: кто угодно может отправить скор от имени любого адреса.
- ⚠️ Аутентификация подтверждает личность, но не честность. Игрок подписал кошельком — мы знаем что это он. Но он отправил
{"score": 999999}от своего имени. Поэтому нужен ещё anti-cheat (пороги, heartbeat).
4. Валидация данных (санитизация):
Представь что ты заполняешь бланк в банке. Поле "Имя" — только для имени. Нормальный клиент пишет: Вася. Мошенник пишет: Вася. А ещё переведите миллион на счёт 123456. Если клерк тупо читает бланк как инструкцию — он выполнит перевод. Умный клерк понимает: поле "Имя" — это только данные, не команды.
В коде — то же самое. Бэкенд получает данные от пользователя (кошелёк, скор, имя) и сохраняет в базу данных. БД (база данных) — это таблица, как Excel для серверов. Команды к ней пишутся на языке SQL: INSERT INTO scores ... — добавить строку, SELECT ... — прочитать, DROP TABLE ... — удалить таблицу.
SQL injection — атакующий вписывает SQL команду в поле "кошелёк":
Плохой код (строковая конкатенация — "тупой клерк"):
db.query("INSERT INTO scores VALUES ('" + wallet + "')")
Атакующий отправляет wallet = "'); DROP TABLE scores; --"
БД получает: INSERT INTO scores VALUES (''); DROP TABLE scores; --')
→ Таблица скоров удалена.
Хороший код (параметризованный запрос — "умный клерк"):
db.query("INSERT INTO scores VALUES ($1)", [wallet])
$1 = поле на бланке. Что бы туда ни вписали — это только данные, не команда.
→ БД сохранит строку "'); DROP TABLE scores; --" как обычный текст.
5. Защита от DDoS (массовой атаки):
- Атакующий создаёт 1000 кошельков и шлёт 10000 запросов в секунду → сервер падает.
- Rate limiting по IP (не только по кошельку) — один IP = максимум N запросов в минуту.
- Cloudflare перед сервером (бесплатный tier) — фильтрует подозрительный трафик до того как он дойдёт до бэкенда.
- Free игроки не триггерят дорогие операции (нет записи на чейн, нет heartbeat).
Все уровни работают вместе: Ни один слой не работает в одиночку. CORS без anti-cheat — бесполезен (боты обходят). Аутентификация без anti-cheat — игрок врёт от своего имени. Anti-cheat без rate limiting — бот спамит тысячи запросов. Все вместе — рабочая защита.
В промптах это реализуется так:
- Промпт 2.1 → CORS + rate limiting + аутентификация + базовая структура endpoints
- Промпт 2.2 → Anti-cheat валидация (hard reject, soft flag, heartbeat)
- Промпт 4.2 → Аудит: проверка что ничего не упущено
Промпт 2.1 — Скоры + лидерборд + heartbeat
Контракт задеплоен — но он только хранит финальные данные. Между игрой и контрактом нужен бэкенд: он принимает скоры в реальном времени, ведёт лидерборд, и от имени operator'а записывает итоги на чейн.
База данных (PostgreSQL) — четыре таблицы:
players— кошельки, статусы, скоры. Одна строка = один игрок.scores— каждое обновление скора. Много строк на игрока.heartbeats— пинги от клиента. Доказательство что человек играет.sessions— игровые сессии. Начало, конец, итог.
API Endpoints — что фронтенд будет вызывать:
POST /api/score— клиент отправляет очки. Бэкенд проверяет что игрок activated и heartbeat свежий.POST /api/heartbeat— клиент пингует каждые N секунд. Если перестал пинговать — значит не играет.GET /api/leaderboard— топ-100 игроков. Данные из БД, не из контракта (быстрее).GET /api/player/:address— профиль одного игрока: скор, ранг, история.
Аутентификация — бэкенд должен знать что запрос пришёл от реального владельца кошелька. Для этого клиент подписывает сообщение приватным ключом кошелька, бэкенд проверяет подпись. Без этого — любой может слать скоры от чужого адреса.
Blockchain модуль (operator) — бэкенд умеет вызывать recordScore() в контракте. Подписывает транзакции приватным ключом operator'а. Когда вызывать — по крону, в конце сезона, или по порогу.
В бэкенд проекте. Если бэкенда нет — AI создаст его с нуля.
Напиши бэкенд для хранения скоров, лидерборда и heartbeat.
Мой стек: [TypeScript/Node.js/Python — что используешь] БД: PostgreSQL (Railway)
Контракт задеплоен на Base Sepolia: прочитай адрес из docs/deploy.md (если нет — найди в contracts/broadcast//run-latest.json) ABI контракта: прочитай из contracts/out//Game.json (или docs/architecture.md для списка функций) Библиотека: viem (для TypeScript) или web3.py (для Python)
Архитектура (из 0.1):
Прочитай docs/architecture.md — секции роли, operator, поток денег.
1. База данных
- players: wallet_address (PK), activated_at, total_score, best_score, current_season_score
- scores: id, wallet_address, score_delta, session_id, timestamp, flagged (bool)
- heartbeats: id, wallet_address, session_id, timestamp, client_data (JSON)
- sessions: id, wallet_address, started_at, expires_at, ended_at, total_score, heartbeat_count
Миграции или ORM модели — на твоё усмотрение.
2. API Endpoints
POST /api/score
Клиент отправляет дельту скора.
- Проверка: wallet_address активирован? (кэш или запрос в контракт)
- Проверка: heartbeat свежий? (последний < [X] секунд назад)
- Сохранить в scores table
- Обновить player total_score и current_season_score
POST /api/heartbeat
Клиент пингует каждые [X] секунд.
GET /api/leaderboard
Топ-100 игроков текущего сезона.
- Только activated игроки
- Поля: rank, wallet_address (сокращённый 0x123...abc), score, activated_at
- Кэш: обновлять раз в [X] секунд
GET /api/player/:address
3. Blockchain модуль (operator)
Бэкенд подписывает транзакции от имени operator wallet:
recordScore(player, score) — запись ончейн
- Когда вызывать: конец сезона / по крону / по достижении порога
- Retry при failed транзакциях (nonce issues)
- Логирование txHash
Чтение (бесплатно):
- isActivated(address) — проверка при первом запросе + кэш
- totalPlayers() — для лидерборда
- currentSeason() — для фильтрации скоров
Конфигурация через env (адрес контракта — возьми из docs/deploy.md):
- GAME_CONTRACT_ADDRESS
- OPERATOR_PRIVATE_KEY
- RPC_URL=https://sepolia.base.org
- Retry с экспоненциальным backoff
- Алерт если gas balance оператора < 0.001 ETH
- Все tx логируются в таблицу transactions (hash, status, gas_used)
4. CORS + Rate Limiting + Аутентификация
Аутентификация запросов:
POST endpoints (/api/score, /api/heartbeat) должны проверять что запрос от реального владельца кошелька:
- При старте сессии: клиент подписывает сообщение кошельком (signMessage) → отправляет подпись на бэкенд
- Бэкенд: проверяет подпись (ecrecover / verifyMessage) → если подпись валидна, выдаёт session token
- Все последующие запросы: клиент отправляет session token в header (Authorization: Bearer ...)
- Без валидной подписи / токена → 401 Unauthorized
Это критично: без аутентификации любой может отправить curl POST /api/score с чужим адресом.
Безопасность сессий (обязательно):
- TTL: Каждая сессия имеет expires_at. По умолчанию 30 минут (или длина сезона/раунда). Middleware проверяет expires_at на КАЖДОМ запросе. Просроченный токен = 401. Без TTL: украденный токен = бесконечный доступ.
- Одна активная сессия: При создании новой сессии — закрыть все предыдущие незакрытые сессии этого игрока (ended_at = NOW). Без этого: игрок открывает 10 вкладок = 10 параллельных сессий = фарм.
- Валидация входных данных: Все поля в POST body — whitelist. Только ожидаемые поля, только ожидаемые типы. client_data в heartbeat — либо строгая схема, либо убрать. Произвольный JSON = вектор атаки.
- Скоры — только целые числа:
Number.isInteger(score)перед записью. Float (15.5) пройдёт валидацию "score > 0" но крашитBigInt()при записи ончейн. Проверяй тип ДО любой обработки.
5. ДЕПЛОЙ НА RAILWAY
После того как код написан и работает локально — задеплой на Railway:
5.1 Проверь CLI:
railway --version
Если нет — установи и залогинься:
npm i -g @railway/cli railway login
5.2 Создай проект + БД:
cd server/ railway init railway add --database postgres
5.3 Добавь env переменные:
railway variables set OPERATOR_PRIVATE_KEY=0x_ВСТАВЬ_КЛЮЧ_ОПЕРАТОРА railway variables set GAME_CONTRACT_ADDRESS=0x_АДРЕС_КОНТРАКТА railway variables set RPC_URL=https://sepolia.base.org railway variables set CORS_ORIGIN=http://localhost:5173
DATABASE_URL и PORT — Railway добавит автоматически. GAME_CONTRACT_ADDRESS — возьми из docs/deploy.md.
5.4 Деплой:
railway up railway domain
5.5 Проверь:
curl https://RAILWAY_URL/api/leaderboard
Должен вернуть [] (пустой массив). Если 500 — миграции не прошли, запусти:
railway run node migrate.js # или: railway run npx prisma migrate deploy
5.6 Сохрани:
Запиши Railway URL в docs/deploy.md рядом с адресом контракта.
🚫 ТОЛЬКО БЭКЕНД
Этот промпт создаёт ТОЛЬКО эти файлы (проверь что ВСЕ созданы):
server/(илиbackend/) — Express/Fastify сервер, API endpoints, operator модульserver/.env— ОБЯЗАТЕЛЬНО создай с placeholder'ами: OPERATOR_PRIVATE_KEY=0x_ВСТАВЬ_OPERATOR_ПРИВАТНЫЙ_КЛЮЧ GAME_CONTRACT_ADDRESS=0x_АДРЕС_КОНТРАКТА (возьми из docs/deploy.md или broadcast) RPC_URL=https://sepolia.base.org DATABASE_URL=postgresql://... (Railway добавит автоматически после деплоя) PORT=3001 CORS_ORIGIN=http://localhost:5173 Без этого файла: кто деплоит не на Railway — не будет знать какие env vars нужны.server/.gitignore— включает .env- PostgreSQL схема (миграция или init SQL)
НЕ трогай: контракт (уже задеплоен), фронтенд (React, UI) — это Фаза 3.
Если забыл создать .env — создай сейчас. После написания кода — ОБЯЗАТЕЛЬНО выполни секцию 5 (деплой на Railway). Не останавливайся на "код готов".
Anti-cheat: базовый уровень
Полноценный anti-cheat — отдельная дисциплина. Но базовые проверки отсеют 90% ботов и читеров. Принцип: не ловить всех, а сделать накрутку невыгодной.
- Hard reject — нереальные значения. Скор 999999 за 1 секунду → отклонить.
- Soft flag — подозрительно, но возможно. Пометить, разобрать вручную.
- Rate limit — ограничить количество действий в единицу времени.
Промпт 2.2 — Anti-cheat
Когда за скоры можно получить реальные деньги — люди будут читерить. Anti-cheat не ловит всех — он делает накрутку невыгодной. Три уровня: отклонить нереальное, пометить подозрительное, ограничить скорость.
Hard reject — нереальные значения. Отклонить, не сохранять. Пример: твоя игра — merge с таймером 90 сек. Максимум очков за 90 сек у хорошего игрока — 5000. Кто-то прислал 50000 → reject. AI должен рассчитать пороги конкретно для твоей игры.
Soft flag — подозрительно, но возможно. Сохранить, пометить для ручной проверки. Пример: новый аккаунт сразу в top 1% — может быть pro, а может бот.
Heartbeat — пинг каждые N секунд. Нет пинга = не играет. Высокий скор без heartbeat → reject.
Rate limiting — макс N записей в час, M сессий в день. Бот не может спамить бесконечно.
⚠️ Пороги: откуда берутся числа
Если замерял в 0.2 — пороги уже в docs/adaptations.md, AI их прочитает.
Если AI оценивал сам — пороги предварительные (широкие). AI реализует логику, а ты откалибруешь на тестнете (шаг 4.5 в промпте 5.1): поиграешь 5-10 сессий, сравнишь реальные скоры с порогами, подкрутишь конфиг.
В любом случае: пороги — не навсегда. Это стартовая точка которую ты уточняешь по реальным данным.
Напиши модуль серверной валидации для моей игры.
Механика (из Context Brief):
-— Прочитай docs/context-brief.md — секция Gameplay.
## 1. Валидация при записи скора
### Hard reject (отклонить, не сохранять):
На основе КОНКРЕТНЫХ механик игры определи нереальные значения:
- Score/second > [порог] → reject
- Session length < [min] секунд → reject (слишком быстро)
- Session length > [max] часов → reject (забыл выключить)
- Score > теоретический максимум за session → reject
### Soft flag (сохранить, пометить):
- Идеальная регулярность (bot pattern: ровно N действий каждые M мс)
- Score в top 1% за сессию → flag для ручной проверки
- Новый аккаунт + аномально высокий скор → flag
- Ожидаемый интервал: [X] секунд ± [Y]% допуск
- Пропуск > [Z] пингов подряд → сессия помечается как suspicious
- Отсутствие heartbeat при высоком score → reject
- Макс [N] записей скора в час на адрес
- Макс [M] сессий в день на адрес
- Хранение в БД (не in-memory — чтобы пережило рестарт)
- 3+ soft flags за день от одного адреса → пометить аккаунт
- Flagged аккаунты: скоры не участвуют в лидерборде
- Не блокировать моментально — собирать данные, разбирать потом
Сделай middleware/decorator который:
1. Проверяет hard limits → reject с кодом 422
2. Проверяет soft limits → flag в БД, вернуть 200
3. Проверяет rate limit → reject с кодом 429
4. Логирует каждую проверку
ФАЗА 3: ФРОНТЕНД
Web3 UX: как не спугнуть игрока
Главная проблема Web3 игр — игрок уходит на этапе "подключи кошелёк". MetaMask popup, подтверждение сети, газ — каждый шаг теряет 50% людей.
- Кнопка "Connect Wallet" — одна, заметная, с текстом что будет дальше
- Автоматический switch на Base Sepolia — не заставляй руками менять сеть
- FREE mode по умолчанию — дай поиграть без кошелька, потом предложи активацию
- Один клик для активации — "Pay 0.001 ETH to activate" → confirm в кошельке → готово
FREE → играет в песочнице, localStorage, нет в лидерборде ACTIVATED → платит entry fee, скоры на сервере, в лидерборде
Фронтенд должен ясно показывать статус и что даёт активация.
Промпт 3.1 — Подключение кошелька
Кошелёк — это ID игрока в Web3. Вместо логин/пароль — адрес кошелька (0x...). Этот промпт добавляет кнопку "Connect Wallet" и настраивает всё чтобы фронтенд мог общаться с блокчейном.
Библиотеки: кто что делает
Во Фронтенде (React) используется несколько библиотек. Не путай их с тем что используется в контракте:
Контракт (Solidity): OpenZeppelin — безопасные компоненты (ReentrancyGuard, Ownable...) Foundry — компиляция, тесты, деплой Фронтенд (React): wagmi — хуки для блокчейна (вызов функций контракта, чтение данных) ConnectKit — готовая кнопка "Connect Wallet" (MetaMask, Coinbase и др.) React — фреймворк для интерфейса (кнопки, страницы, формы). НЕ про блокчейн.
wagmi — библиотека для React, которая умеет общаться с блокчейном из браузера. Это она делает кнопку Connect Wallet, вызывает activate() в контракте, читает скоры, отслеживает статус кошелька. Без wagmi пришлось бы вручную работать с Ethereum провайдером.
ConnectKit / RainbowKit — готовый UI для кнопки подключения. Поддержка MetaMask, Coinbase Wallet и других. Работает поверх wagmi.
Base Sepolia — тестовая сеть Base. chainId 84532. Деньги тестовые, всё бесплатно. Когда всё работает — переключишь на настоящий Base (mainnet).
Auto switch — если у игрока выбрана другая сеть в кошельке — автоматически предложить переключиться на Base Sepolia.
- Кнопка Connect → открывается MetaMask → подключается → показывает адрес?
- Неправильная сеть → предлагает Switch?
- Disconnect работает?
Добавь Web3 кошелёк в мой [React / Next.js / Vue] фронтенд.
Мой стек:
-— Прочитай docs/context-brief.md — секция Tech Stack.
-—
Используй:
- wagmi v2 + viem — для взаимодействия с блокчейном
- ConnectKit или RainbowKit — для UI кнопки подключения (выбери что проще интегрировать с моим стеком)
- Сеть: Base Sepolia (testnet, chainId 84532)
1. **Установка пакетов** — npm install команда
2. **wagmi конфигурация** — файл config/wagmi.ts:
- Base Sepolia chain
- HTTP transport. RPC URL из env переменной (import.meta.env.VITE_RPC_URL или process.env.NEXT_PUBLIC_RPC_URL), fallback на публичный https://sepolia.base.org
- Используй ConnectKit или RainbowKit — они включают WalletConnect из коробки (встроенный projectId, ручная регистрация не нужна)
3. **Провайдер-обёртка** — обернуть App:
- WagmiProvider
- QueryClientProvider (TanStack Query — wagmi v2 требует)
4. **Компонент ConnectButton:**
- Не подключён → кнопка "Connect Wallet"
- Подключён → показать адрес (0x123...abc), баланс ETH, кнопка Disconnect
- Неправильная сеть → кнопка "Switch to Base Sepolia"
5. **Хук useAccount** — как получить текущий адрес, isConnected, chain
Важно:
- Автоматический switch на Base Sepolia при подключении
- Обработка ошибок (пользователь отклонил, кошелёк не установлен)
- Не ломай существующий UI — кнопка должна вписаться в текущий дизайн
- RPC URL через env переменную, не хардкодом
Промпт 3.2 — Хуки контракта
Хуки (hooks) — React функции которые общаются с контрактом. Каждый хук делает одну вещь: прочитать данные, отправить транзакцию, отследить статус. Потом ты используешь их в компонентах.
useActivate()— отправить ETH в контракт. Возвращает статус: pending → success/error.useIsActivated(address)— проверить: этот кошелёк activated? Читает из контракта, бесплатно.usePlayerScore(address)— прочитать скор и лучший скор из контракта.useLeaderboard()— запрос к бэкенду (не к контракту — из контракта медленно).useGameStats()— totalPlayers, currentSeason, баланс контракта.useEntryFee()— сколько стоит активация.
API (Application Programming Interface) — как фронтенд общается с бэкендом. POST /api/score — отправить скор. Обычный HTTP.
ABI (Application Binary Interface) — описание функций конкретного контракта. JSON файл который говорит фронтенду: какие функции есть, что принимают, что возвращают.
Зачем: контракт в блокчейне — это байткод (набор цифр). Фронтенд не знает какие у него функции. ABI — карта: "вот функции, вот параметры".
Пример: игрок нажал "Activate" на сайте. Что происходит:
1. wagmi читает ABI → находит: "activate(), payable, 0 параметров" 2. wagmi формирует транзакцию → "вызвать activate(), отправить 0.001 ETH" 3. MetaMask показывает игроку: "Отправить 0.001 ETH на адрес контракта?" 4. Игрок подтверждает → транзакция уходит в блокчейн
Без ABI шаг 1 невозможен — wagmi не знает что контракт умеет.
ABI генерируется автоматически при forge build — файл out/Game.sol/Game.json. Ты копируешь его в фронтенд-проект.
Каждый хук должен возвращать данные. useIsActivated → true/false. useGameStats → числа. Если возвращает undefined или ошибку — ABI или адрес контракта неправильные.
Напиши React хуки (wagmi v2) для взаимодействия с моим контрактом.
Контракт: прочитай адрес из docs/deploy.md. Если файла нет — найди в contracts/broadcast/*/run-latest.json (поле contractAddress). Добавь в .env фронтенда как VITE_CONTRACT_ADDRESS (если ещё нет).
ABI: прочитай из contracts/out/*/Game.json
### useActivate()
- Отправить ETH (entry fee) в контракт → activate()
- Возвращает: { activate, isPending, isSuccess, error, txHash }
- После успеха: показать "Activated!" + обновить статус
### useIsActivated(address)
- Прочитать isActivated[address] из контракта
- Возвращает: { isActivated, isLoading }
- Кэш: refetch при подключении кошелька и после activate()
### usePlayerScore(address)
- Прочитать playerScore[address] и playerBestScore[address]
- Возвращает: { score, bestScore, isLoading }
### useLeaderboard()
- Запрос к бэкенду GET /api/leaderboard (не из контракта — слишком дорого)
- Возвращает: { players: [{rank, address, score}], isLoading }
- Auto-refresh каждые [X] секунд
### useGameStats()
- Прочитать из контракта: totalPlayers, currentSeason, contract balance
- Возвращает: { totalPlayers, currentSeason, treasuryETH, isLoading }
### useEntryFee()
- Прочитать ENTRY_FEE из контракта
- Возвращает: { fee (в wei), feeFormatted (в ETH), isLoading }
Каждый хук:
- Возвращает loading/error/data
- Обрабатывает ошибки (показать revert reason человеческим языком)
- Показывает pending транзакции (отправлена → подтверждена)
Файл: src/hooks/useContract.ts (или разбей на отдельные файлы)
Промпт 3.3 — Интеграция: FREE / ACTIVATED
Финальный фронтенд-промпт. Здесь всё соединяется: хуки контракта + бэкенд + существующий UI. Игрок видит два режима: бесплатный (песочница) и активированный (реальная экономика).
- FREE — играет без кошелька. Скоры в localStorage. Не в лидерборде. Баннер "Activate for X ETH".
- ACTIVATED — заплатил. Скоры на бэкенд. В лидерборде. Heartbeat активен.
ActivationBanner— баннер для Free игроков: что даёт активация + кнопка "Activate".PlayerStatus— показывает режим, ранг, адрес кошелька.Leaderboard— топ-100 из бэкенда, подсветка текущего игрока.GameStats— данные из контракта: сколько игроков, казна, сезон.
Обработка ошибок — каждое ончейн-действие может сломаться: нет ETH, отмена в кошельке, timeout. Для каждого случая — человеческое сообщение, не техническая ошибка.
У меня есть работающая игра и хуки контракта (из промпта 3.2).
Нужно интегрировать ончейн в существующий UI.
Мой текущий UI:
-— Прочитай docs/context-brief.md — секции UI/экраны.
-—
Хуки доступны: useActivate, useIsActivated, usePlayerScore, useLeaderboard,
useGameStats, useEntryFee (из промпта 3.2)
## 1. Два режима: FREE и ACTIVATED
### FREE (кошелёк не подключён ИЛИ не активирован):
- Игрок играет как обычно
- Скоры хранятся в localStorage (как сейчас)
- НЕ показывать в лидерборде
- Скоры НЕ идут на бэкенд
- Показать баннер: "Activate for $3 to join leaderboard and earn rewards"
### ACTIVATED (кошелёк подключён + activate() вызван):
- Скоры идут на бэкенд (POST /api/score)
- Heartbeat активен (POST /api/heartbeat каждые [X] секунд)
- Показывать в лидерборде
- Показать: rank, score, % от общего (вклад в пул)
- Прогресс НЕ переносится из FREE — начинает с нуля (anti-abuse)
### ActivationBanner
- Показывать FREE игрокам
- Текст: что даёт активация (leaderboard, rewards, on-chain scores)
- Кнопка "Activate for [FEE] ETH"
- При клике → useActivate() → pending → success → скрыть баннер
### PlayerStatus
- FREE: "Free Mode — scores are local only"
- ACTIVATED: "Activated ✓ — Season [N] — Rank #[X]"
- Адрес кошелька (сокращённый)
### Leaderboard (обновлённый)
- Только ACTIVATED игроки (данные из /api/leaderboard)
- Колонки: Rank, Player (0x123...abc), Score, % Contribution
- % Contribution = player_score / total_all_scores × 100
- Подсветить текущего игрока
### GameStats (из контракта)
- Total Players: [N]
- Treasury: [X] ETH
- Current Season: [N]
- Данные из useGameStats() — прямо из блокчейна, верифицируемые
Для каждого on-chain действия:
- Кошелёк не подключён → "Connect wallet first"
- Неправильная сеть → "Switch to Base Sepolia"
- Недостаточно ETH → "You need at least [FEE] ETH"
- Транзакция отклонена → "Transaction cancelled"
- Транзакция pending → spinner + "Confirming..."
- Транзакция успешна → "Activated! Welcome to Season [N]"
Нарисуй обновлённый flow и напиши код для интеграции с МОИМ существующим UI.
Не переписывай всё — минимальные изменения в существующих компонентах.
## 5. Размещение компонентов в UI
### Leaderboard — ОБЯЗАТЕЛЬНО видимый:
- Встрой в главный экран игры (таб, сайдбар, нижняя панель — выбери что подходит к текущему layout)
- НЕ прячь на отдельную страницу без ссылки — игрок должен видеть лидерборд без лишних кликов
- Обновляется при каждом открытии (GET /api/leaderboard)
- Если экран маленький — сделай переключатель Game / Leaderboard
### ActivationBanner:
- Показывать на главном экране ДО и ВО ВРЕМЯ игры (пока FREE)
- После активации — убрать навсегда
### PlayerStatus + GameStats:
- В header или над игровым полем — всегда видны
Посмотри мой текущий UI (прочитай код компонентов) и напиши КОД интеграции.
Не переписывай всё — минимальные изменения в существующих компонентах.
Покажи какие файлы трогаешь и что именно меняешь.
Промпт 3.4 — Мобильная совместимость
Большинство игроков аркад — на телефоне. Если UI не адаптирован или кошелёк не работает в мобильном браузере — ты теряешь 70%+ аудитории.
- MetaMask Mobile — открывает сайт внутри своего in-app browser.
window.ethereumработает, но поведение отличается от десктопа (медленнее, другие размеры экрана, нет расширений). - Обычный мобильный браузер (Safari, Chrome) —
window.ethereumотсутствует. Нужен deep link чтобы открыть MetaMask или Coinbase Wallet. - Тач-контролы — кнопки слишком мелкие, hover-эффекты не работают, свайпы конфликтуют с жестами кошелька.
- Экран — модалки кошелька обрезаются, лидерборд не влезает, баннер активации перекрывает игру.
ConnectKit / RainbowKit уже обрабатывают мобильные кошельки (deep links, WalletConnect). Но UI твоей игры — нет.
- Открой сайт с телефона в обычном браузере (Safari/Chrome) — Connect Wallet работает? Редиректит в MetaMask?
- Открой в MetaMask in-app browser — кошелёк подключается? Activate() проходит?
- Лидерборд, баннер активации, Game Stats — всё читаемо? Ничего не обрезается?
- Кнопки нажимаются пальцем без промахов? (минимум 44×44px)
- Модалка подтверждения транзакции не перекрывается другими элементами?
Адаптируй мой фронтенд для мобильных устройств.
Мой стек:
---
Прочитай docs/context-brief.md — секция Tech Stack.
---
Текущее состояние: десктоп работает, Web3 интегрирован (кошелёк, хуки, FREE/ACTIVATED).
### Deep links:
- Обычный мобильный браузер (нет window.ethereum) → кнопка Connect должна открывать MetaMask/Coinbase через deep link
- ConnectKit/RainbowKit уже поддерживают это — проверь что НЕ сломано кастомным CSS или обёрткой
- Если используется кастомная кнопка Connect вместо стандартной — убедись что мобильный flow работает
### MetaMask in-app browser:
- window.ethereum есть → стандартный flow
- Проверь: после подключения кошелёк остаётся подключённым при навигации между страницами
- Проверь: транзакция activate() открывает подтверждение корректно (не дублируется, не зависает)
### WalletConnect:
- QR код на десктопе, deep link на мобилке — ConnectKit/RainbowKit делают автоматически
- Проверь что WalletConnect modal не обрезается на маленьком экране
### Точки перелома (breakpoints):
- Мобилка: < 640px
- Планшет: 640px — 1024px
- Десктоп: > 1024px
### Критичные элементы (проверь каждый на 375px ширины):
**ActivationBanner:**
- Полная ширина экрана, текст не обрезается
- Кнопка "Activate for X ETH" — минимум 44px высоты, тапается без промахов
- Не перекрывает игровое поле
**Leaderboard:**
- На мобилке: убрать или сократить колонку % Contribution
- Адреса кошельков: 0x12...ab (короче чем на десктопе)
- Скролл горизонтальный НЕ нужен — таблица должна влезать
**GameStats (Total Players, Treasury, Season):**
- Горизонтальный ряд на десктопе → вертикальный стак или 2×2 grid на мобилке
**Модалки (транзакция pending, ошибки):**
- Не выходят за экран
- Закрываются тапом на overlay
- Текст читаем (минимум 14px)
**Connect Wallet кнопка:**
- Видна без скролла (в header или sticky)
- После подключения: адрес сокращён (0x12...ab), не ломает layout
- Все интерактивные элементы: минимум 44×44px (Apple HIG стандарт)
- Убрать hover-эффекты на мобилке (или заменить на :active)
- Если игра использует клики — проверь что тач-события работают (touchstart/touchend vs click)
- Свайпы: не конфликтуют с жестами кошелька или браузера (pull-to-refresh, back swipe)
- Двойной тап: не вызывает зум на кнопках (добавить touch-action: manipulation)
## 4. ПРОИЗВОДИТЕЛЬНОСТЬ НА МОБИЛКЕ
- RPC запросы (чтение контракта) — кэшируй через wagmi (staleTime).
Мобильный интернет медленнее — не дёргай контракт на каждый ре-рендер.
- Анимации — проверь fps на реальном телефоне (не эмулятор). Тяжёлые анимации убей или упрости для мобилки.
- Bundle size — если используешь мобильные deep links, НЕ добавляй лишние wallet SDK. ConnectKit/RainbowKit уже всё включает.
Проверь на реальном устройстве (не эмулятор):
- [ ] iPhone Safari → Connect Wallet → deep link в MetaMask → вернулся → кошелёк подключён
- [ ] MetaMask Mobile in-app browser → Connect → Activate → транзакция прошла
- [ ] Android Chrome → Connect Wallet → deep link работает
- [ ] Лидерборд читаем на 375px экране
- [ ] Все кнопки тапаются без промахов
- [ ] Модалки не обрезаются
- [ ] Игра работает (тач, fps, нет лагов)
Если нет реального устройства — Chrome DevTools → Toggle Device Toolbar → iPhone SE (375px).
Но мобильный кошелёк можно протестировать ТОЛЬКО на реальном телефоне.
- НЕ переписывай игровую логику. Только UI адаптация и мобильный кошелёк.
- НЕ добавляй отдельное мобильное приложение — только responsive web.
- НЕ ломай десктоп. Все изменения — адаптивные (media queries / контейнерные запросы).
- Если игра использует Canvas — тач-адаптация Canvas отдельно (touch events, viewport scaling).
ФАЗА 4: БЕЗОПАСНОСТЬ
Почему безопасность — не "потом"
В Web3 баг = кто-то забрал деньги из контракта. Откатить нельзя. The DAO ($60M), Wormhole ($320M), Euler ($197M) — все прошли аудиты.
ISSUES.md — лог проблем
При аудите AI найдёт проблемы. Не все нужно фиксить прямо сейчас — но все нужно записать. Создай файл ISSUES.md в корне проекта:
# Issues Log ## Формат 🔴 = блокер (фиксить до запуска) 🟡 = надо пофиксить (первый месяц) 🟢 = улучшение (когда будет время) --- ### 2025-02-11 | Фаза 4 | Аудит контракта - 🔴 withdraw() не проверяет что amount <= balance — FIXED 2025-02-11 - 🟡 нет события при смене operator — OPEN - 🟢 добавить getter для всех activated адресов — OPEN ### 2025-02-12 | Фаза 4 | Аудит бэкенда - 🔴 SQL injection в /api/score — FIXED 2025-02-12 - 🟡 rate limit только по IP, нет по кошельку — OPEN
- Нашёл проблему → записал. Даже если кажется мелочью.
- 🔴 фиксишь сразу (или до конца фазы). 🟡 и 🟢 — в ISSUES.md на потом.
- Не удаляй записи после фикса — помечай FIXED с датой. Это история проекта.
- Перед запуском (Фаза 5): все 🔴 должны быть FIXED. 🟡
Если в claude.md прописано правило лога (см. выше) — Claude Code будет записывать сам.
Карта рисков
КОНТРАКТ БЭКЕНД ФРОНТЕНД - reentrancy - утечка ключа - фишинг/подмена сайта - access control - SQL injection - XSS (код в данных) - логические ошибки - race condition - clickjacking (iframe) - DoS - нет auth на API ПОТЕРЯ: деньги ПОТЕРЯ: контроль ПОТЕРЯ: доверие из контракта над контрактом пользователей
Код может быть идеальным, но если кто-то зайдёт на сервер через SSH — заберёт operator key. Инфраструктурные атаки — самые частые, потому что самые простые.
Типичные уязвимости контракта
Reentrancy — повторный вход
Контракт отправляет ETH → получатель (тоже контракт) в своём receive() вызывает withdraw() снова → контракт ещё не обновил баланс → отправляет ещё раз → по кругу. The DAO (2016): $60M.
Защита: ReentrancyGuard (модификатор nonReentrant) + CEI паттерн (сначала обнови баланс, потом отправляй ETH).
Access Control — кто имеет право
Без onlyOperator на recordScore() — любой пишет себе скоры. Без onlyOwner на withdraw() — любой забирает деньги.
Баланс для нашего контракта: owner может pause, withdraw, newSeason. Owner НЕ может менять скоры. Operator может только recordScore — утёк ключ = фейковые скоры, но деньги целы.
Логические ошибки
Код работает, но не так как задумано:
- Можно ли активироваться дважды? (должен быть
require(!isActivated[msg.sender])) newSeason()дважды подряд = пустой сезон?- Скор неактивированному игроку?
withdraw(всё)— контракт работает, но казна пуста
DoS — остановка
Цикл по массиву всех игроков → при 10,000 → gas limit → revert → никто не вызовет. В нашем контракте неактуально (нет циклов), но AI должен проверить.
Промпт 4.1 — Аудит контракта
AI проверяет твой контракт на уязвимости. Это НЕ замена реальному аудиту — но отловит типичные дыры. Ты вставляешь полный код контракта, AI проходит по чеклисту.
Что AI проверяет (чтобы ты понимал что читаешь)
- Reentrancy — атакующий вызывает функцию повторно до завершения первого вызова. Классика: The DAO hack, $60M. Защита:
nonReentrantмодификатор. - Access control — все ли функции за правильными ограничениями? withdraw только для owner? recordScore только для operator?
- Логические ошибки — можно ли активироваться дважды? Что если owner забрал все деньги?
- DoS — может ли кто-то заблокировать контракт для всех?
AI выдаст список проблем с severity (Critical/High/Medium/Low/Info). Critical и High — фиксить обязательно. Medium — желательно. Low/Info — на усмотрение.
Проведи security review моего контракта.
Прочитай файл contracts/src/*.sol
REENTRANCY:
- Есть ли nonReentrant на всех state-changing функциях?
- Соблюдается ли CEI pattern (effects before interactions)?
- Есть ли внешние вызовы после изменения стейта?
ACCESS CONTROL:
- Все ли критические функции за модификаторами (onlyOwner, onlyOperator)?
- Используется ли msg.sender (не tx.origin)?
- Ownable2Step вместо Ownable?
- renounceOwnership() заблокирована? (override → revert). Без этого: случайный вызов = контракт без owner навсегда.
- Может ли owner получить слишком много власти? (централизация)
ВХОДНЫЕ ДАННЫЕ:
- Есть ли MAX_SCORE константа? recordScore проверяет score <= MAX_SCORE?
- Есть ли проверка score > 0?
- Есть ли проверка что player != address(0)?
- Entry fee: что если кто-то отправит 0 ETH? (msg.value >= fee)
МАТЕМАТИКА:
- Overflow/underflow? (Solidity 0.8+ проверяет, но проверь edge cases)
- Деление на ноль?
- Что если totalPlayers = 0 при расчётах?
ЛОГИКА:
- Можно ли активироваться дважды?
- Можно ли записать скор неактивированному игроку?
- Что если owner забирает ВСЕ ETH из контракта?
- Работают ли сезоны корректно при переходе?
DOS:
- Может ли один адрес заблокировать систему?
- Есть ли лимиты на массивы?
EVENTS:
- Все ли важные действия логируются?
- Хватит ли информации в events для восстановления состояния?
Для каждой проблемы:
- Severity: Critical / High / Medium / Low / Info
- Описание
- Рекомендация по исправлению
- Код фикса (если применимо)
После аудита: все Critical и High → фикси сразу. Medium и ниже → запиши в ISSUES.md с датой и пометкой 🟡/🟢.
Бэкенд: главная цель атаки
Контракт публичный, фронтенд у пользователя. А бэкенд хранит operator private key — главная цель.
Утечка ключа
Если OPERATOR_PRIVATE_KEY утёк — атакующий пишет фейковые скоры. Деньги в безопасности (withdraw только для owner), но доверие убито.
.envв git — боты сканят GitHub, находят за секундыconsole.log(process.env)— ключ в логах Railway- Stack trace в ответе API —
OPERATOR_PRIVATE_KEYв 500 ошибке - Бэкап БД без шифрования
Race conditions
Запрос 1: читает score = 100 Запрос 2: читает score = 100 Запрос 1: пишет score = 150 Запрос 2: пишет score = 120 ← перезаписал
Защита: транзакции в БД, SELECT ... FOR UPDATE.
API без защиты
Без аутентификации: curl POST /api/score {"wallet": "0xЧужой", "score": 999999} — не нужно ломать контракт, достаточно знать URL.
Без rate limiting: бот спамит 1000 запросов/сек → БД мусор, газ оператора исчерпан.
Без CORS: любой сайт делает запросы к твоему API. В промпте 2.1 — аутентификация через подпись кошельком + session token.
Промпт 4.2 — Аудит бэкенда
Бэкенд хранит operator key — главная цель. AI проверяет: SQL injection, утечка ключей, race conditions, auth.
- Аутентификация — самое важное. Если POST /api/score не проверяет подпись кошелька — любой может слать скоры от чужого адреса через curl. Это главная дыра в P2E бэкенде.
- Operator key — если AI нашёл что ключ где-то логируется, может попасть в ответ ошибки, или хранится в коде — фиксить немедленно.
- SQL injection — если запросы к БД собираются строками (не параметризованные) — любой может выполнить произвольный SQL.
- Race conditions — два запроса одновременно могут перезаписать данные друг друга. Решение: транзакции в БД.
Проведи security review бэкенда моей Web3 игры.
Прочитай файлы в server/ (или backend/) — ключевые endpoints и middleware.
API:
- SQL Injection: parameterized queries?
- XSS: санитизация входных данных?
- Rate limiting: есть?
- CORS: ограничены домены (не wildcard *)?
- Payload size limit?
АУТЕНТИФИКАЦИЯ:
- POST endpoints защищены подписью кошелька?
- Можно ли отправить скор от чужого адреса без его подписи?
- Session tokens: как создаются, сколько живут, хранятся ли в БД?
- Можно ли переиспользовать чужой session token?
- Проверяется ли wallet_address из токена, а не из тела запроса?
СЕССИИ:
- TTL: есть ли expires_at? Middleware проверяет на каждом запросе?
- Параллельные сессии: может ли один игрок иметь N активных сессий? (должна быть одна)
- При создании новой — закрываются ли старые?
- Что происходит при использовании просроченного токена? (должен быть 401)
ВАЛИДАЦИЯ ВХОДНЫХ ДАННЫХ:
- Скоры: проверяется ли Number.isInteger(score)? (float крашит BigInt при ончейн записи)
- Скоры: проверяется ли score > 0 и score <= MAX_SCORE?
- client_data в heartbeat: whitelist полей или произвольный JSON? (произвольный = вектор атаки)
- Все POST body: проверяются ли типы каждого поля? (не только наличие)
OPERATOR КЛЮЧ:
- Где хранится OPERATOR_PRIVATE_KEY?
- Разделены ли Owner и Operator?
- Если украдут Operator ключ — что максимум может сделать атакующий?
(recordScore — плохо, но не критично. withdraw — катастрофа.)
HEARTBEAT:
- Можно ли спуфить heartbeat без реальной игры?
- Достаточен ли интервал для обнаружения ботов?
RACE CONDITIONS:
- Два одновременных recordScore для одного игрока?
- Два одновременных score submit?
ДАННЫЕ:
- Что in-memory vs в БД? (ничего важного не должно быть только in-memory)
- Бэкапы БД?
Для каждой проблемы: severity + описание + рекомендация.
Critical/High → фикси сразу. Medium и ниже → в ISSUES.md.
OWNER KEY (самый ценный)
│ Может: pause, withdraw, newSeason, setOperator
│ Хранение: отдельный кошелёк, НЕ повседневный
│ Используется: редко (настройка, emergency)
│
├── OPERATOR KEY (рабочий)
│ Может: только recordScore
│ Хранение: env variable на сервере
│ Используется: постоянно
│ Если утёк: неприятно, но деньги в безопасности
│
└── PLAYER KEY (у пользователя)
Может: activate (тратить свой ETH)
Ты НЕ контролируешь
Принцип: чем больше может ключ — тем реже используется и тем лучше хранится.
Что делать если утёк ключ
Operator key: pause() → setOperator(новый) → новый ключ в env → unpause(). Проверить были ли подозрительные recordScore.
Owner key: критично — атакующий может withdraw всё. На тестнете не страшно. На мейннете owner должен быть мультисигом (отдельная тема).
Промпт 4.3 — Безопасность ключей
Owner и operator — разные кошельки. На тестнете: deployer wallet = owner, отдельный = operator.
cast call CONTRACT "owner()"→ адрес owner'аcast call CONTRACT "operator()"→ отдельный адрес (не owner!)- Owner key и operator key — в разных кошельках
- Operator key — только на сервере (env variable), не в коде
Помоги настроить безопасность ключей для моей Web3 игры.
Принцип: owner и operator — РАЗНЫЕ кошельки.
Текущее состояние:
- Owner = deployer wallet (private key в .env)
- Operator = [тот же или отдельный] wallet
- Контракт на Base Sepolia
-—
## 1. РАЗДЕЛЕНИЕ КЛЮЧЕЙ:
- Operator wallet: отдельный от deployer, только для recordScore
- Как создать отдельный кошелёк (cast wallet new)
- Как установить operator в контракте (setOperator)
- Operator private key: env variable на Railway, не в коде
## 2. .ENV БЕЗОПАСНОСТЬ:
- .gitignore включает .env
- Railway env variables для продакшена
- Никаких ключей в коде, логах, ошибках
## 3. МОНИТОРИНГ:
- Как проверить баланс operator wallet (cast balance)
- При каком балансе пополнять (< 0.001 ETH на Sepolia)
- Как проверить owner через cast call
## 4. ПРОЦЕДУРА ПРИ КОМПРОМЕТАЦИИ:
### Если утёк Operator key:
1. Owner вызывает pause() (cast send)
2. Owner вызывает setOperator(новый_адрес)
3. Сгенерировать новый ключ, обновить env на сервере
4. Owner вызывает unpause()
### Если утёк Owner key:
На тестнете — не критично (нет реальных денег). На мейннете — отдельная тема по защите owner key.
ФАЗА 5: ДЕПЛОЙ И ЗАПУСК
Промпт 5.0 — Деплой фронтенда
AI задеплоит всё через CLI. Фронтенд → Vercel (CDN). Бэкенд → Railway (сервер + PostgreSQL). AI проверит авторизацию CLI первым шагом и поможет залогиниться если нужно.
Задеплой фронтенд на Vercel и свяжи с уже работающим бэкендом на Railway.
Бэкенд УЖЕ задеплоен на Railway (Фаза 2). URL в docs/deploy.md.
## 0. ПРОВЕРКА АВТОРИЗАЦИИ (делай ПЕРВЫМ):
```bash
gh auth status # GitHub
npx vercel whoami # Vercel
```
Если не авторизован — СТОП. Скажи какой CLI, команду для логина, жди.
Railway уже авторизован (деплоили в Фазе 2).
- vercel.json — security headers (X-Frame-Options: DENY, X-Content-Type-Options: nosniff, CSP frame-ancestors none) + rewrites для SPA
- .gitignore — проверь: .env, node_modules/, dist/, contracts/out/, contracts/cache/, contracts/broadcast/
- npm run build проходит без ошибок
Если нет репо: `gh repo create НАЗВАНИЕ --public --source=. --push`
Если есть: `git add -A && git commit -m "prepare deploy" && git push`
## 3. ДЕПЛОЙ ФРОНТЕНДА (Vercel CLI):
```bash
npx vercel --prod
```
Фреймворк: Vite, Build: npm run build, Output: dist
После деплоя — добавь env переменные:
```bash
vercel env add VITE_RPC_URL production # https://sepolia.base.org
vercel env add VITE_CONTRACT_ADDRESS production # из docs/deploy.md
vercel env add VITE_API_URL production # Railway URL из docs/deploy.md
```
Если есть другие VITE_ переменные — добавь их тоже.
Затем передеплой: `npx vercel --prod`
Обнови CORS_ORIGIN в Railway на Vercel URL:
```bash
cd server/
railway variables set CORS_ORIGIN=https://VERCEL_URL
railway up
```
## 5. ПРОВЕРИТЬ:
```bash
# Бэкенд жив?
curl https://RAILWAY_URL/api/leaderboard
# Фронтенд жив?
curl -I https://VERCEL_URL
# Проверь X-Frame-Options: DENY в ответе
# Связка работает? (фронтенд дёргает бэкенд)
# Открой VERCEL_URL в браузере → Connect Wallet → проверь что лидерборд грузится
```
Промпт 5.1 — Финальный тест + чеклист перед запуском
## Промпт 5.1 — Финальный тест + чеклист перед запуском
Последний промпт. Полный flow от начала до конца + чеклист перед запуском.
- **cast** — CLI инструмент из Foundry. `cast call` — прочитать (бесплатно). `cast send` — записать (нужен газ).
- Два кошелька с тестовым ETH (faucet Base Sepolia).
- Фронтенд и бэкенд задеплоены по URL.
Напиши скрипт/чеклист для тестирования полного flow на Base Sepolia.
Мой контракт: прочитай из docs/deploy.md (если нет — из contracts/broadcast/*/run-latest.json) Мой бэкенд: прочитай URL из railway status или спроси у пользователя Мой фронтенд: прочитай URL из vercel ls или спроси у пользователя
Шаги с командами (cast CLI + фронтенд):
- PRE-CHECK:
- [ ] Контракт на Basescan Sepolia → код верифицирован?
- [ ] Owner правильный? (cast call [CONTRACT] "owner()")
- [ ] Operator установлен? (cast call [CONTRACT] "operator()" → отдельный адрес, НЕ owner)
- [ ] Entry fee правильный? (cast call [CONTRACT] "ENTRY_FEE()")
- [ ] Контракт не на паузе? (cast call [CONTRACT] "paused()")
- ACTIVATE (с фронтенда):
- [ ] Подключить кошелёк → адрес отображается
- [ ] Нажать Activate → транзакция в кошельке
- [ ] Подтвердить → pending → success
- [ ] Проверить: isActivated = true (cast call)
- [ ] Проверить: totalPlayers увеличился
- ACTIVATE (второй адрес):
- GAMEPLAY:
4.5. ANTI-CHEAT КАЛИБРОВКА: Пока играл — собери реальные данные:
- [ ] Какой макс скор ты набрал за 5-10 сессий?
- [ ] Какой средний?
- [ ] Длина типичной сессии?
- [ ] Не блокирует ли anti-cheat легитимную игру? (false positive)
- RECORD SCORE (operator):
- [ ] Бэкенд вызывает recordScore → txHash
- [ ] Проверить: playerScore ончейн обновился (cast call)
- [ ] Проверить: seasonScores записались
- OWNER OPERATIONS (через cast от owner wallet):
- [ ] Вызвать pause() → контракт на паузе?
- [ ] Activate при паузе → revert?
- [ ] Вызвать unpause() → контракт работает?
- [ ] Вызвать newSeason() → currentSeason увеличился?
- [ ] Старые seasonScores заморожены?
- [ ] Новые скоры пишутся в новый сезон?
- EDGE CASES:
- [ ] recordScore не от operator → revert?
- [ ] Повторная активация → revert?
- [ ] Скор от неактивированного → поведение корректно?
- [ ] curl POST /api/score без подписи/токена → 401?
- [ ] curl POST /api/score с чужим wallet_address → reject?
- WITHDRAW (через cast от owner):
Для каждого шага: команда + ожидаемый результат.
После прохождения flow — проверь полный чеклист:
- [ ] Все forge test проходят
- [ ] Coverage: forge coverage (цель: >90% для критических функций)
- [ ] Код верифицирован на Basescan Sepolia (BaseScan API key настроен)
- [ ] Entry fee адекватный (не 0, не 100 ETH)
- [ ] Operator установлен (отдельный от owner)
- [ ] Owner и operator — разные кошельки
- [ ] Paused = false
- [ ] Работает по URL (не localhost)
- [ ] POST /api/score принимает и сохраняет
- [ ] POST /api/heartbeat работает
- [ ] GET /api/leaderboard возвращает данные
- [ ] Аутентификация: POST endpoints требуют подпись кошелька / session token
- [ ] Без подписи POST /api/score возвращает 401 (проверить через curl)
- [ ] CORS настроен на домен фронтенда (не wildcard *)
- [ ] Rate limiting активен
- [ ] Operator wallet имеет тестовый ETH для газа
- [ ] Env variables: CONTRACT_ADDRESS, OPERATOR_KEY, RPC_URL
- [ ] Работает по URL
- [ ] Connect Wallet → показывает адрес
- [ ] Неправильная сеть → предлагает switch на Base Sepolia
- [ ] Activate → транзакция → success → статус обновляется
- [ ] FREE mode работает (без кошелька)
- [ ] ACTIVATED mode: скоры на бэкенд, лидерборд, heartbeat
- [ ] Лидерборд показывает данные
- [ ] Обработка ошибок (нет ETH, отмена, timeout)
- [ ] Security headers (vercel.json / _headers): X-Frame-Options, X-Content-Type-Options, CSP
- [ ] RPC URL и CONTRACT_ADDRESS через env переменные (не хардкод)
- [ ] MAX_SCORE константа в контракте, recordScore проверяет score <= MAX_SCORE
- [ ] renounceOwnership() заблокирована (override → revert)
- [ ] CEI pattern соблюдён во всех функциях с ETH (проверить вручную)
- [ ] После редеплоя: адрес контракта обновлён ВЕЗДЕ (фронтенд env, бэкенд env, abi.js)
- [ ] .env НЕ в git
- [ ] Ключи разделены (owner ≠ operator)
- [ ] Аудит промптом 4.1 пройден, критические фиксы сделаны
- [ ] Аудит промптом 4.2 пройден (включая проверку аутентификации API)
- [ ] SSL сертификат работает (https://, зелёный замок)
- [ ] 2FA на аккаунте регистратора домена
- [ ] npm audit: 0 critical / high уязвимостей
- [ ] package-lock.json в git
- [ ] RPC endpoint работает (приватный для прода, публичный ок для тестнета) ТЕСТ:
- [ ] Тестировал с 2+ кошельками
- [ ] Лидерборд корректный
- [ ] Все 🔴 = FIXED
- [ ] Все 🟡 = есть план (первая неделя после запуска)
- [ ] Нет незаписанных проблем (всё что нашёл при аудите — в логе)
Для каждого пункта: как проверить + что делать если не готово.
BaseApp Урок 3: последние штрихи и вывод в BaseApp
1. Открой свой проект в Claude Code
2. Вставь **Prompt 0** — агент соберёт бриф проекта в `PROJECT_BRIEF.md`
3. Вставляй остальные промпты по порядку
4. Каждый промпт читает и **дописывает результаты** в `PROJECT_BRIEF.md`
5. `PROJECT_BRIEF.md` — единственный файл, который переживает compact и смену сессий
6. После launch вставь **Prompt 8** — автоматическая верификация что всё работает
**Pipeline:**
```
Prompt 0 — Context Brief (сканирует проект)
Prompt 1 — Аудит + Security (контракты, тесты, адреса)
Prompt 2 — Деплой в Mainnet (forge/hardhat + верификация)
Prompt 3 — Smart Transactions (Builder Code + Smart Wallet)
Prompt 4 — Base App Compliance (UI/UX требования)
Prompt 5 — Farcaster Manifest + SDK
Prompt 6 — Деплой + Account Association + Smoke Test
Prompt 7 — Регистрация + Launch (base.dev, индексация)
Prompt 8 — Post-Launch Verification (полный smoke test)
```
PROMPT 0 — CONTEXT BRIEF
Агент сканирует весь проект и собирает бриф. Все остальные промпты будут ссылаться на него.
Что делаем: Агент сканирует весь проект и собирает "паспорт" — один файл PROJECT_BRIEF.md, который будет жить на протяжении всего процесса. Туда попадёт всё: какой стек, какие контракты, где адреса захардкожены, какие env переменные, как деплоится.
Зачем: Все следующие промпты читают PROJECT_BRIEF.md вместо того чтобы каждый раз пересканировать проект. Это экономит контекст и гарантирует что агент не потеряет информацию между compact'ами и сменой сессий. Без этого файла каждый следующий промпт начинал бы с нуля.
Что от тебя нужно: Ничего — просто вставь промпт и дай агенту поработать. В конце он покажет саммари: что готово, что нужно доделать. Проверь что всё совпадает с реальностью — если агент что-то пропустил или неправильно определил, поправь вручную в PROJECT_BRIEF.md.
## ЗАДАЧА: Собрать полный контекст проекта для запуска на Base
Ты начинаешь процесс подготовки Web3 приложения к запуску на Base L2 как Mini App.
Первый шаг — собрать всю информацию о проекте в один файл.
Проанализируй весь проект и создай файл `PROJECT_BRIEF.md` в корне проекта.
Файл должен содержать ТОЧНУЮ информацию, полученную из кода — не догадки.
### СТРУКТУРА PROJECT_BRIEF.md
## 1. ИДЕНТИФИКАЦИЯ
- Название проекта: [из package.json name, title в index.html, или README]
- Описание (1 предложение): [из package.json description или README]
- Тип: [game / defi / social / utility / nft / marketplace]
- Текущий URL (если задеплоен): [из env, config, или README]
## 2. СТЕК
- Frontend framework: [React/Next.js/Vue/Svelte + версия из package.json]
- Build tool: [Vite/Webpack/Next.js/Turbopack — из config файлов]
- CSS: [Tailwind/CSS Modules/Styled Components/etc]
- Web3 библиотека: [wagmi/ethers/viem/web3.js + версия]
- Wallet UI: [RainbowKit/ConnectKit/Web3Modal/AppKit + версия]
- Contract framework: [Foundry/Hardhat/Truffle — по наличию foundry.toml, hardhat.config, etc]
- Backend: [есть/нет, если есть — фреймворк: Express/Hono/Fastify/Next API]
- Database: [есть/нет, тип]
## 3. КОНТРАКТЫ
Для каждого .sol файла:
- Имя: [ContractName.sol]
- Путь: [contracts/src/...]
- Solidity версия: [из pragma]
- Назначение: [что делает]
- Ключевые функции: [payable функции, withdraw, claim, etc]
- Constructor args: [типы и описание]
- Внешние зависимости: [Chainlink, Uniswap, OpenZeppelin, etc]
## 4. ТЕКУЩАЯ СЕТЬ
- Chain: [Base Sepolia / Base Mainnet / Ethereum Sepolia / localhost]
- Chain ID: [84532 / 8453 / 11155111 / 31337]
- Адреса контрактов (текущие): [список]
- Как переключается сеть: [env var / hardcoded / config]
- RPC URL: [откуда берётся]
## 5. ТРАНЗАКЦИИ
Список ВСЕХ мест где пользователь отправляет on-chain транзакцию:
- [файл:строка] — [functionName] — [описание что делает]
- [файл:строка] — [functionName] — [описание]
Как отправляются: [useWriteContract / useContractWrite / sendTransaction / ethers.Contract]
## 6. АДРЕСА В UI
Список мест где показываются 0x-адреса пользователю:
- [файл:строка] — [контекст: leaderboard / profile / history]
Формат отображения: [полный 0x... / сокращённый ...abcd / ENS]
## 7. ТЕМЫ И LAYOUT
- Dark mode: [есть/нет, как реализован]
- Light mode: [есть/нет]
- Переключатель темы: [есть/нет]
- Mobile responsive: [есть/нет]
- Bottom navigation: [есть/нет]
- Safe area padding: [есть/нет]
- Touch targets >= 44px: [да/нет/не проверено]
## 8. ДЕПЛОЙ
- Хостинг: [Vercel/Railway/Netlify/AWS/свой]
- Build command: [npm run build / vite build / next build]
- Output directory: [dist / .next / build / out]
- Монолит или split: [frontend+backend вместе / отдельно]
- Dockerfile: [есть/нет]
- CI/CD: [есть/нет, что используется]
## 9. FARCASTER / BASE APP
- farcaster.json: [есть/нет, путь]
- @farcaster/miniapp-sdk: [установлен/нет, версия]
- sdk.actions.ready(): [вызывается/нет, где]
- base:app_id meta tag: [есть/нет]
- fc:miniapp meta tag: [есть/нет]
- Builder Code: [интегрирован/нет]
- Account Association: [подписан/нет]
## 10. ENV ПЕРЕМЕННЫЕ
Список всех VITE_*/NEXT_PUBLIC_*/REACT_APP_* переменных:
- [VAR_NAME] = [описание, текущее значение если не секретное]
## 11. ПРОБЛЕМЫ (найденные при анализе)
- [проблема 1]
- [проблема 2]
```
1. **package.json** — название, зависимости, scripts, версии wagmi/viem/rainbowkit
2. **Все .sol файлы** — grep `pragma solidity`, найди контракты
3. **Config файлы** — foundry.toml, hardhat.config, vite.config, next.config, tailwind.config
4. **Grep по 0x** — найди все адреса контрактов в коде (исключи node_modules)
5. **Grep по chainId / chain** — определи текущую сеть
6. **Grep по writeContract / sendTransaction / useSendTransaction** — найди все транзакции
7. **Grep по address / 0x / wallet** в JSX/TSX компонентах — найди отображение адресов
8. **index.html** — найди meta tags, title
9. **public/ или static/** — найди .well-known/farcaster.json
10. **Grep по dark / theme / mode** — найди тему
11. **.env* файлы** (кроме .env.local с секретами) — найди переменные
12. **Dockerfile, railway.json, vercel.json, netlify.toml** — найди деплой конфиг
### ВАЖНО
- Пиши ТОЛЬКО то что нашёл в коде. Не угадывай.
- Если что-то не нашёл — пиши "НЕ НАЙДЕНО"
- В секции ПРОБЛЕМЫ — отметь всё что выглядит как потенциальный баг или блокер для mainnet
- Проверь `npm run build` — билдится ли проект вообще
Создай PROJECT_BRIEF.md и выведи краткое саммари: что готово, что нужно доделать.
PROMPT 1 — АУДИТ И ПОДГОТОВКА К MAINNET
Глубокий аудит контрактов + фикс всех найденных проблем. Без этого в мейнет нельзя.
Что делаем: Глубокий security-аудит смарт-контрактов перед деплоем в mainnet. Агент проверяет access control (кто может вызывать функции), reentrancy, overflow, Chainlink оракулы если есть. Также синхронизирует ABI между контрактом и бэкендом — частая причина багов в проде.
Зачем: Контракт нельзя изменить после деплоя. Если withdraw() не имеет onlyOwner — кто угодно выведет все деньги. Если ABI в бэкенде не совпадает с контрактом — settlement крашнется. Эти баги ловятся только на этом этапе. Реальный случай: пришлось редеплоить контракт потому что withdrawFounders() не имел модификатора.
Что от тебя нужно: Если агент найдёт критические уязвимости — он исправит их сам и покажет diff. Проверь что исправления логичны. Если у тебя есть тесты (forge test / npx hardhat test) — агент их запустит. Не переходи к Prompt 2 пока все ❌ не станут ✅.
ЗАДАЧА: Аудит и подготовка к деплою на Base Mainnet
Прочитай PROJECT_BRIEF.md — там полный контекст проекта.
### ШАГ 1: КОНТРАКТЫ — SECURITY AUDIT
Прочитай КАЖДЫЙ .sol файл и проверь по списку:
#### Access Control (КРИТИЧНО)
- [ ] Все функции вывода средств (withdraw*, claim*, transfer*) имеют modifier (onlyOwner/onlyOperator/onlyRole)
- [ ] Если НЕ имеют → ЭТО КРИТИЧЕСКИЙ БАГ. Любой может вывести все деньги. ИСПРАВЬ НЕМЕДЛЕННО.
- [ ] Ownership transfer protected (двухшаговый через Ownable2Step, или хотя бы onlyOwner)
- [ ] Operator/admin функции защищены
> РЕАЛЬНЫЙ СЛУЧАЙ: Мы задеплоили контракт где withdrawFounders() не имел onlyOperator.
> Пришлось редеплоить. Контракт НЕЛЬЗЯ изменить после деплоя. Проверяй ДО.
#### Reentrancy
- [ ] Все функции с external calls используют checks-effects-interactions паттерн
- [ ] Или используют ReentrancyGuard (nonReentrant modifier)
- [ ] Особенно: функции с .call{value:}(), .transfer(), .send()
#### Integer Safety
- [ ] Solidity >= 0.8.0 (встроенная overflow защита)
- [ ] Если < 0.8 — используются SafeMath
#### External Calls
- [ ] Все .call{value:}() проверяют return value: `(bool success, ) = addr.call{value: amount}(""); require(success);`
- [ ] Нет unchecked send()/transfer() (они тихо fail'ят при 2300 gas limit)
#### Price Feed / Oracle
- [ ] Если используется Chainlink — адрес Price Feed правильный для TARGET СЕТИ
- Base Mainnet ETH/USD: `0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70`
- Base Sepolia ETH/USD: `0x4aDC67696bA383F43DD60A9e78F2C97Fbbfc7cb1`
- [ ] Есть проверка на stale price (updatedAt не старше X часов)
- [ ] Есть проверка answer > 0
> РЕАЛЬНЫЙ СЛУЧАЙ: Бэкенд ожидал drawJackpot(uint256), контракт имел drawJackpot().
> Settlement бы крашнулся в production. Всегда генерируй ABI из сорса.
Если есть бэкенд:
1. Найди все ABI определения в бэкенд коде (grep по `abi`, `ABI`, `interface`)
2. Сравни с реальным контрактом:
```bash
cd contracts && forge inspect ContractName abi
```
3. Если есть расхождения — обнови ABI в бэкенде чтобы точно соответствовали контракту
4. Проверь: имена функций, типы аргументов, return types
```bash
# Foundry
cd contracts && forge test -vvv
- Все тесты ДОЛЖНЫ проходить
- Если тестов нет — напиши минимум:
- Тест на deploy (контракт создаётся)
- Тест на основную функцию (entry/play/mint)
- Тест на withdraw (только owner может)
- Тест на edge cases (нулевые значения, повторные вызовы)
Найди ВСЕ захардкоженные адреса контрактов в коде:
```bash
grep -rn "0x[a-fA-F0-9]\{40\}" --include="*.js" --include="*.jsx" --include="*.ts" --include="*.tsx" --include="*.sol" --exclude-dir=node_modules
```
Для каждого найденного адреса:
- Определи: это testnet или mainnet?
- Если testnet — нужно будет заменить после деплоя в mainnet
- Создай систему переключения если нет:
```javascript
// Правильный паттерн
const isMainnet = import.meta.env.VITE_NETWORK === 'mainnet'
// или для Next.js:
const isMainnet = process.env.NEXT_PUBLIC_NETWORK === 'mainnet'
const CONTRACTS = isMainnet ? {
game: '0x...mainnet...',
token: '0x...mainnet...',
} : {
game: '0x...testnet...',
token: '0x...testnet...',
}
```
> РЕАЛЬНЫЙ СЛУЧАЙ: У нас были хардкод testnet адреса в 4 файлах.
> Один пропустили → фронтенд пытался читать testnet контракт на mainnet → белый экран.
Проверь или создай deploy script для Base Mainnet:
**Для Foundry** — создай `script/DeployMainnet.s.sol`:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Script, console} from "forge-std/Script.sol";
import {MyContract} from "../src/MyContract.sol";
contract DeployMainnet is Script {
function run() external {
uint256 deployerKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
address operator = vm.envAddress("OPERATOR_ADDRESS");
address treasury = vm.envAddress("TREASURY_ADDRESS");
vm.startBroadcast(deployerKey);
// Deploy contracts here
vm.stopBroadcast();
}
}
```
**Для Hardhat** — проверь что в hardhat.config есть network `base`:
```javascript
networks: {
base: {
url: process.env.BASE_MAINNET_RPC_URL || 'https://mainnet.base.org',
chainId: 8453,
accounts: [process.env.DEPLOYER_PRIVATE_KEY],
}
}
```
Создай `.env.deploy.example` (БЕЗ реальных ключей):
```
DEPLOYER_PRIVATE_KEY=0x_YOUR_PRIVATE_KEY
OPERATOR_ADDRESS=0x_YOUR_OPERATOR
TREASURY_ADDRESS=0x_YOUR_TREASURY_OR_SAFE_MULTISIG
BASE_MAINNET_RPC_URL=https://mainnet.base.org
BASESCAN_API_KEY=YOUR_BASESCAN_API_KEY
```
Проверь что `.env.deploy` в `.gitignore`. Если нет — ДОБАВЬ.
Билд ДОЛЖЕН проходить без ошибок. Если есть ошибки — исправь.
Типичные проблемы:
- TypeScript ошибки (unused vars, type mismatches)
- Import несуществующих модулей
- Missing env vars при билде (VITE_* переменные)
### ВЫВОД — СОХРАНИ В PROJECT_BRIEF.md
**ОБЯЗАТЕЛЬНО:** Допиши результаты аудита в конец файла PROJECT_BRIEF.md:
```markdown
## 12. АУДИТ (Prompt 1)
Дата: [сегодня]
| Проверка | Статус | Действие |
|----------|--------|----------|
| Access control | ✅/❌ | [что исправлено] |
| Reentrancy | ✅/❌ | ... |
| ABI sync | ✅/❌ | ... |
| Tests | ✅/❌ | X/Y passed |
| Hardcoded addresses | ✅/❌ | ... |
| Network switching | ✅/❌ | ... |
| Deploy script | ✅/❌ | ... |
| Frontend build | ✅/❌ | ... |
### Файлы изменённые при аудите:
- [список файлов и что изменено]
### Deploy script:
- Путь: [путь к deploy script]
- Команда: [forge script ...]
### Env template:
- Путь: [.env.deploy.example]
```
Если есть ❌ — ИСПРАВЬ ВСЁ до перехода к следующему этапу.
Если всё ✅ — напиши "Аудит пройден, PROJECT_BRIEF.md обновлён. Готов к Prompt 2."
PROMPT 2 — ДЕПЛОЙ В MAINNET + ВЕРИФИКАЦИЯ
Деплой контрактов + автоматическая верификация одной командой.
Что делаем: Деплоим контракты на Base Mainnet (chain ID 8453) и верифицируем их на BaseScan. Одной командой через Foundry (forge script --broadcast --verify) или Hardhat.
Зачем: Верифицированный контракт на BaseScan — это доверие пользователей. Зелёная галочка означает что код открыт и проверяем. Без верификации твой контракт — чёрный ящик, и ни один серьёзный пользователь не будет с ним взаимодействовать.
Что от тебя нужно: Тебе нужен BaseScan API Key (бесплатный) и приватный ключ деплоера с ETH на Base. Агент создаст .env.deploy.example с шаблоном переменных — заполни его реальными значениями. После деплоя агент обновит все адреса в коде (фронт, бэк, конфиги) с testnet на mainnet.
## ЗАДАЧА: Деплой контрактов на Base Mainnet и верификация на BaseScan
Прочитай PROJECT_BRIEF.md — там контракты, адреса, стек.
1. Прочитай deploy script проекта
2. Убедись что всё из Prompt 1 (аудит) исправлено
3. Проверь что `.env.deploy` существует и содержит нужные переменные
# ВАЖНО: вычисли OPERATOR_ADDRESS из private key если он не задан
# cast wallet address $DEPLOYER_PRIVATE_KEY
# Деплой + верификация ОДНОЙ КОМАНДОЙ
forge script script/DeployMainnet.s.sol:DeployMainnet \
--rpc-url https://mainnet.base.org \
--etherscan-api-key $BASESCAN_API_KEY \
- `--broadcast` — реально отправляет транзакцию (без него = dry run)
- `--verify` — автоматически верифицирует на BaseScan после деплоя
- `--etherscan-api-key` — нужен для верификации
- `-vvvv` — максимальный verbose (видны все детали)
#### Если --verify не сработал (бывает при таймауте):
src/MyContract.sol:MyContract \
--etherscan-api-key $BASESCAN_API_KEY \
--constructor-args $(cast abi-encode "constructor(address,address,address)" $ARG1 $ARG2 $ARG3) \
> ТИПИЧНАЯ ОШИБКА: forge verify-contract fails с "Unable to verify"
> Причина: constructor args не совпадают. Используй ТОЧНО те же args что при деплое.
> Проверь: `cast abi-encode` должен дать тот же output что в broadcast JSON.
npx hardhat run scripts/deploy.js --network base
npx hardhat verify --network base <ADDRESS> <CONSTRUCTOR_ARG1> <CONSTRUCTOR_ARG2>
Из output команды скопируй ВСЕ задеплоенные адреса.
#### 2. Обнови все файлы с адресами
Найди ВСЕ файлы где есть старые (testnet) адреса контрактов и замени на mainnet:
grep -rn "OLD_TESTNET_ADDRESS" --include="*.js" --include="*.jsx" --include="*.ts" --include="*.tsx" --include="*.json"
Замени на новые mainnet адреса. Типичные файлы:
- Frontend: wagmi config, hooks, constants
- Backend: config/constants, services/blockchain
- Vite: `VITE_NETWORK=mainnet`
- Next.js: `NEXT_PUBLIC_NETWORK=mainnet`
- Backend: `USE_MAINNET=true` или `NETWORK=mainnet`
Убедись что chain config указывает на Base Mainnet (8453), не Base Sepolia (84532).
> ТИПИЧНАЯ ОШИБКА: Забыть обновить chain в wagmi config.
> Симптом: кошелёк подключается но транзакции fail — "wrong network".
> Проверь: в wagmi config должен быть `import { base } from 'wagmi/chains'` (не baseSepolia).
Открой каждый контракт на BaseScan:
https://basescan.org/address/<ADDRESS>#code
Должна быть зелёная галочка "Contract Source Code Verified".
Отправь минимальную транзакцию в контракт чтобы убедиться что всё работает:
cast call <CONTRACT_ADDRESS> "owner()" --rpc-url https://mainnet.base.org
### ВЫВОД — СОХРАНИ В PROJECT_BRIEF.md
**ОБЯЗАТЕЛЬНО:** Допиши в PROJECT_BRIEF.md:
### Contracts Deployed (Base Mainnet)
| Contract | Address | Verified |
|----------|---------|----------|
- [список файлов где обновлены адреса с testnet на mainnet]
- RPC: https://mainnet.base.org
- Frontend env: VITE_NETWORK=mainnet
- Backend env: NETWORK=mainnet
### Deploy Command (для повторного деплоя)
Напиши "Деплой завершён, PROJECT_BRIEF.md обновлён. Готов к Prompt 3."
## PROMPT 3 — SMART TRANSACTIONS + BUILDER CODE
> Обёртка для транзакций: Smart Wallet поддержка + ERC-8021 builder attribution.
> Адаптируется под стек проекта автоматически (wagmi v2+, ethers.js, viem).
## ЗАДАЧА: Создать обёртку для on-chain транзакций с Builder Code (ERC-8021)
Прочитай PROJECT_BRIEF.md — там список всех транзакций в приложении (секция 5) и стек (секция 2).
1. **Smart Wallet Support** — если пользователь в Coinbase Smart Wallet, использовать sendCalls (paymaster = газ за юзера)
2. **Builder Code (ERC-8021)** — ко ВСЕМ транзакциям добавлять 20-byte суффикс с builder address
Builder address — адрес кошелька разработчика, зарегистрированный на base.dev.
ERC-8021 добавляет 20 байт в конец calldata. Контракт их игнорирует, Base indexer считывает для атрибуции.
### ШАГ 1: ОПРЕДЕЛИ BUILDER ADDRESS
Найди в проекте (PROJECT_BRIEF.md, .env, manifest) или спроси пользователя:
- Builder address (20 bytes, 0x...)
- Обычно совпадает с deployer/operator адресом
- Если нет — используй placeholder `0xYOUR_BUILDER_ADDRESS_HERE` и предупреди
### ШАГ 2: ОПРЕДЕЛИ СТЕК И ВЕРСИЮ
Из PROJECT_BRIEF.md секция 2 определи Web3 библиотеку. ОБЯЗАТЕЛЬНО проверь версию:
# Проверь точную версию в проекте
- **wagmi v2+ (2.x.x)** → useWriteContract, useCapabilities, useSendCalls — ПЕРЕЙДИ К ВАРИАНТУ A
- **wagmi v1 (1.x.x)** → useContractWrite (ДРУГОЙ API!) — ПЕРЕЙДИ К ВАРИАНТУ B
- **ethers.js v6** → contract.functionName() — ПЕРЕЙДИ К ВАРИАНТУ C
- **ethers.js v5** → contract.functionName() (другие типы) — ПЕРЕЙДИ К ВАРИАНТУ C
- **viem (без wagmi)** → walletClient.writeContract() — ПЕРЕЙДИ К ВАРИАНТУ D
> КРИТИЧНАЯ ОШИБКА: wagmi v1 и v2 имеют РАЗНЫЙ API. Не угадывай — проверяй.
> v1: useContractWrite({ address, abi, functionName }) → write()
> v2: useWriteContract() → writeContractAsync({ address, abi, functionName })
#### ВАРИАНТ A: wagmi v2+ (React)
Создай файл `src/hooks/useSmartTransaction.js` (или .ts):
import { useCallback } from 'react'
import { useAccount, useCapabilities, useSendCalls, useWriteContract } from 'wagmi'
import { getCallsStatus } from '@wagmi/core'
import { encodeFunctionData } from 'viem'
import { base } from 'wagmi/chains'
import { config } from '../config/wagmi' // ← АДАПТИРУЙ путь к вашему wagmi config
// ERC-8021: Builder address suffix (20 bytes, registered on base.dev)
const BUILDER_ADDRESS = '0xYOUR_BUILDER_ADDRESS_HERE'
export function useSmartTransaction() {
const { address, isConnected } = useAccount()
// Smart Wallet detection (Coinbase Smart Wallet exposes paymasterService)
const { data: capabilities } = useCapabilities({
query: { enabled: Boolean(address), retry: false },
const hasPaymaster = Boolean(capabilities?.[base.id]?.paymasterService?.supported)
// sendCalls — Smart Wallet path
const { sendCallsAsync, isPending: isSendCallsPending } = useSendCalls()
// writeContract — regular wallet path (EOA, MetaMask, etc.)
const { writeContractAsync, isPending: isWritePending } = useWriteContract()
const isPending = isSendCallsPending || isWritePending
const execute = useCallback(async ({ address: contractAddress, abi, functionName, args, value }) => {
if (!isConnected) throw new Error('Wallet not connected')
// Smart Wallet: sendCalls + paymaster + builder suffix in calldata
const calldata = encodeFunctionData({ abi, functionName, args: args || [] })
const dataWithSuffix = calldata + BUILDER_ADDRESS.slice(2)
const id = await sendCallsAsync({
calls: [{ to: contractAddress, data: dataWithSuffix, value: value || 0n }],
capabilities: { paymasterService: {} },
// sendCalls returns an ID (NOT a tx hash) — poll for receipt
const receipt = await pollForConfirmation(id)
return { hash: receipt?.receipts?.[0]?.transactionHash || id }
// Regular wallet: writeContract + dataSuffix (wagmi v2 supports this natively)
const hash = await writeContractAsync({
dataSuffix: `0x${BUILDER_ADDRESS.slice(2)}`,
}, [isConnected, hasPaymaster, sendCallsAsync, writeContractAsync])
return { execute, isPending, hasPaymaster }
// Poll getCallsStatus until CONFIRMED or timeout (60s)
async function pollForConfirmation(id) {
while (Date.now() - start < 60_000) {
const status = await getCallsStatus(config, { id })
if (status.status === 'CONFIRMED') return status
await new Promise(r => setTimeout(r, 1500))
return null // Timeout — tx may have succeeded, check on chain
- ❌ `require('wagmi')` — не работает в Vite/ESM. Только static imports.
- ❌ `useCallsStatus` хук для polling — создаёт dead code. Используй `getCallsStatus` из `@wagmi/core`.
- ❌ Динамический `import('@wagmi/core')` внутри цикла — импортируй статически вверху.
- ❌ Условные вызовы хуков (нарушает Rules of Hooks). Все хуки вызываются безусловно.
#### ВАРИАНТ B: wagmi v1 (React)
import { useCallback } from 'react'
import { useAccount, useContractWrite, usePrepareContractWrite } from 'wagmi'
import { encodeFunctionData } from 'viem'
const BUILDER_ADDRESS = '0xYOUR_BUILDER_ADDRESS_HERE'
// wagmi v1: нет sendCalls, нет dataSuffix — builder suffix через encodeFunctionData
export function useSmartTransaction() {
const { address, isConnected } = useAccount()
// В v1 нет paymaster support — только builder suffix
const execute = useCallback(async ({ address: contractAddress, abi, functionName, args, value }) => {
if (!isConnected) throw new Error('Wallet not connected')
const calldata = encodeFunctionData({ abi, functionName, args: args || [] })
const dataWithSuffix = calldata + BUILDER_ADDRESS.slice(2)
// v1: используй sendTransaction напрямую с raw data
const { sendTransactionAsync } = await import('wagmi/actions')
const hash = await sendTransactionAsync({
return { execute, isPending: false, hasPaymaster: false }
> ПРИМЕЧАНИЕ: wagmi v1 не поддерживает sendCalls/useCapabilities.
> Рекомендуется обновить до v2 для полной Smart Wallet поддержки.
#### ВАРИАНТ C: ethers.js (v5/v6)
// Утилита (не React хук — для этого фреймворка)
import { ethers } from 'ethers' // v5 или v6
const BUILDER_ADDRESS = '0xYOUR_BUILDER_ADDRESS_HERE'
export async function executeWithBuilder(signer, { address, abi, functionName, args, value }) {
const iface = new ethers.Interface(abi) // v6: Interface, v5: utils.Interface
const calldata = iface.encodeFunctionData(functionName, args || [])
const dataWithSuffix = calldata + BUILDER_ADDRESS.slice(2).toLowerCase()
const tx = await signer.sendTransaction({
#### ВАРИАНТ D: viem (без wagmi)
import { encodeFunctionData } from 'viem'
const BUILDER_ADDRESS = '0xYOUR_BUILDER_ADDRESS_HERE'
export async function executeWithBuilder(walletClient, { address, abi, functionName, args, value }) {
const hash = await walletClient.writeContract({
dataSuffix: `0x${BUILDER_ADDRESS.slice(2)}`, // viem поддерживает dataSuffix
### ШАГ 4: ЗАМЕНИ ВСЕ ТРАНЗАКЦИИ
Из PROJECT_BRIEF.md секция 5 у тебя есть список ВСЕХ мест с write-транзакциями.
Замени КАЖДЫЙ вызов на `execute()` из новой обёртки.
const { writeContractAsync } = useWriteContract()
const { execute } = useSmartTransaction()
**Проверь grep'ом что ничего не пропустил:**
# Для wagmi: не должно остаться прямых вызовов writeContract (кроме внутри хука)
grep -rn "writeContractAsync\|useContractWrite\|sendTransaction" --include="*.jsx" --include="*.tsx" --include="*.js" --include="*.ts" src/ --exclude="*useSmartTransaction*"
# Для ethers: не должно остаться прямых contract.functionName() без builder suffix
grep -rn "\.sendTransaction\|contract\." --include="*.jsx" --include="*.tsx" --include="*.js" src/
1. `npm run build` — билд проходит без ошибок
2. Запусти приложение, подключи кошелёк, отправь тестовую транзакцию
3. В блок-эксплорере: открой транзакцию → Input Data → последние 40 hex символов = builder address (без 0x)
4. Grep: убедись что writeContractAsync/useContractWrite не используется напрямую в UI хуках
### ВЫВОД — СОХРАНИ В PROJECT_BRIEF.md
**ОБЯЗАТЕЛЬНО:** Допиши в PROJECT_BRIEF.md:
## 14. SMART TRANSACTIONS (Prompt 3)
- Стек: [wagmi v2 / wagmi v1 / ethers / viem]
- [файл:строка] — functionName
- Smart Wallet (sendCalls + paymaster): [да/нет — только wagmi v2+]
- ERC-8021 builder suffix: добавлен ко всем транзакциям
Напиши "Smart Transactions готовы, PROJECT_BRIEF.md обновлён. Готов к Prompt 3."
PROMPT 3 — SMART TRANSACTIONS + BUILDER CODE
Обёртка для транзакций: Smart Wallet поддержка + ERC-8021 builder attribution.
Адаптируется под стек проекта автоматически (wagmi v2+, ethers.js, viem).
Что делаем: Оборачиваем ВСЕ on-chain транзакции в приложении в единую обёртку, которая делает две вещи: поддерживает Coinbase Smart Wallet (газ за юзера через paymaster) и добавляет Builder Code (ERC-8021) — 20-байтовый суффикс к каждой транзакции для атрибуции разработчика.
Зачем: Smart Wallet = пользователь не платит газ = в разы выше конверсия. Builder Code = Base видит что транзакции идут через твоё приложение = ты получаешь атрибуцию и можешь претендовать на Builder Grants. Без Builder Code ты невидим для экосистемы Base.
Что от тебя нужно: Тебе нужен builder address — это обычно тот же кошелёк что деплоер/owner. Его нужно зарегистрировать на base.dev (бесплатно, 2 минуты). Агент определит стек (wagmi v2, ethers, viem) и создаст правильную обёртку. После этого — проверь транзакцию в BaseScan: последние 40 hex символов в Input Data должны совпадать с builder address.
ЗАДАЧА: Создать обёртку для on-chain транзакций с Builder Code (ERC-8021)
Прочитай PROJECT_BRIEF.md — там список всех транзакций в приложении (секция 5) и стек (секция 2).
Нужно сделать две вещи:
1. **Smart Wallet Support** — если пользователь в Coinbase Smart Wallet, использовать sendCalls
(paymaster = газ за юзера)
2. **Builder Code (ERC-8021)** — ко ВСЕМ транзакциям добавлять 20-byte суффикс с builder address
Builder address — адрес кошелька разработчика, зарегистрированный на base.dev.
ERC-8021 добавляет 20 байт в конец calldata. Контракт их игнорирует, Base indexer считывает для
атрибуции.
### ШАГ 1: ОПРЕДЕЛИ BUILDER ADDRESS
Найди в проекте (PROJECT_BRIEF.md, .env, manifest) builder address.
Если НЕ найден — **СТОП. Спроси пользователя:**
1. "Какой адрес использовать как Builder Address для ERC-8021?"
2. "Этот адрес зарегистрирован на base.dev? Если нет — зарегистрируй сейчас: https://base.dev"
**НЕ ПРОДОЛЖАЙ** с placeholder `0xYOUR_BUILDER_ADDRESS_HERE` — это бессмысленный суффикс, Base его
не засчитает.
Builder address должен быть реальным и зарегистрированным, иначе весь Prompt 3 = мёртвый код.
- Builder address обычно совпадает с deployer/owner адресом
- Регистрация на base.dev бесплатна и занимает 2 минуты
### ШАГ 2: ОПРЕДЕЛИ СТЕК И ВЕРСИЮ
Из PROJECT_BRIEF.md секция 2 определи Web3 библиотеку. ОБЯЗАТЕЛЬНО проверь версию:
```bash
# Проверь точную версию в проекте
grep '"wagmi"' package.json
grep '"ethers"' package.json
grep '"viem"' package.json
Маршруты:
- wagmi v2+ (2.x.x) → useWriteContract, useCapabilities, useSendCalls — ПЕРЕЙДИ К ВАРИАНТУ A
- wagmi v1 (1.x.x) → useContractWrite (ДРУГОЙ API!) — ПЕРЕЙДИ К ВАРИАНТУ B
- ethers.js v6 → contract.functionName() — ПЕРЕЙДИ К ВАРИАНТУ C
- ethers.js v5 → contract.functionName() (другие типы) — ПЕРЕЙДИ К ВАРИАНТУ C
- viem (без wagmi) → walletClient.writeContract() — ПЕРЕЙДИ К ВАРИАНТУ D
КРИТИЧНАЯ ОШИБКА: wagmi v1 и v2 имеют РАЗНЫЙ API. Не угадывай — проверяй.
v1: useContractWrite({ address, abi, functionName }) → write()
v2: useWriteContract() → writeContractAsync({ address, abi, functionName })
---
ВАРИАНТ A: wagmi v2+ (React)
Создай файл src/hooks/useSmartTransaction.js (или .ts):
import { useCallback } from 'react'
import { useAccount, useCapabilities, useSendCalls, useWriteContract } from 'wagmi'
import { getCallsStatus } from '@wagmi/core'
import { encodeFunctionData } from 'viem'
import { base } from 'wagmi/chains'
import { config } from '../config/wagmi' // ← АДАПТИРУЙ путь к вашему wagmi config
// ERC-8021: Builder address suffix (20 bytes, registered on base.dev)
const BUILDER_ADDRESS = '0xYOUR_BUILDER_ADDRESS_HERE'
export function useSmartTransaction() {
const { address, isConnected } = useAccount()
// Smart Wallet detection (Coinbase Smart Wallet exposes paymasterService)
const { data: capabilities } = useCapabilities({
query: { enabled: Boolean(address), retry: false },
})
const hasPaymaster = Boolean(capabilities?.[base.id]?.paymasterService?.supported)
// sendCalls — Smart Wallet path
const { sendCallsAsync, isPending: isSendCallsPending } = useSendCalls()
// writeContract — regular wallet path (EOA, MetaMask, etc.)
const { writeContractAsync, isPending: isWritePending } = useWriteContract()
const isPending = isSendCallsPending || isWritePending
const execute = useCallback(async ({ address: contractAddress, abi, functionName, args, value })
=> {
if (!isConnected) throw new Error('Wallet not connected')
if (hasPaymaster) {
// Smart Wallet: sendCalls + paymaster + builder suffix in calldata
const calldata = encodeFunctionData({ abi, functionName, args: args || [] })
const dataWithSuffix = calldata + BUILDER_ADDRESS.slice(2)
const id = await sendCallsAsync({
calls: [{ to: contractAddress, data: dataWithSuffix, value: value || 0n }],
capabilities: { paymasterService: {} },
})
// sendCalls returns an ID (NOT a tx hash) — poll for receipt
const receipt = await pollForConfirmation(id)
return { hash: receipt?.receipts?.[0]?.transactionHash || id }
} else {
// Regular wallet: writeContract + dataSuffix (wagmi v2 supports this natively)
const hash = await writeContractAsync({
address: contractAddress,
abi,
functionName,
args: args || [],
value,
dataSuffix: `0x${BUILDER_ADDRESS.slice(2)}`,
})
return { hash }
}
}, [isConnected, hasPaymaster, sendCallsAsync, writeContractAsync])
return { execute, isPending, hasPaymaster }
}
// Poll getCallsStatus until CONFIRMED or timeout (60s)
async function pollForConfirmation(id) {
const start = Date.now()
while (Date.now() - start < 60_000) {
try {
const status = await getCallsStatus(config, { id })
if (status.status === 'CONFIRMED') return status
} catch {}
await new Promise(r => setTimeout(r, 1500))
}
return null // Timeout — tx may have succeeded, check on chain
}
ВАЖНО — что НЕ делать:
- ❌ require('wagmi') — не работает в Vite/ESM. Только static imports.
- ❌ useCallsStatus хук для polling — создаёт dead code. Используй getCallsStatus из @wagmi/core.
- ❌ Динамический import('@wagmi/core') внутри цикла — импортируй статически вверху.
- ❌ Условные вызовы хуков (нарушает Rules of Hooks). Все хуки вызываются безусловно.
---
ВАРИАНТ B: wagmi v1 (React)
import { useCallback } from 'react'
import { useAccount, useContractWrite, usePrepareContractWrite } from 'wagmi'
import { encodeFunctionData } from 'viem'
const BUILDER_ADDRESS = '0xYOUR_BUILDER_ADDRESS_HERE'
// wagmi v1: нет sendCalls, нет dataSuffix — builder suffix через encodeFunctionData
export function useSmartTransaction() {
const { address, isConnected } = useAccount()
// В v1 нет paymaster support — только builder suffix
const execute = useCallback(async ({ address: contractAddress, abi, functionName, args, value })
=> {
if (!isConnected) throw new Error('Wallet not connected')
const calldata = encodeFunctionData({ abi, functionName, args: args || [] })
const dataWithSuffix = calldata + BUILDER_ADDRESS.slice(2)
// v1: используй sendTransaction напрямую с raw data
const { sendTransactionAsync } = await import('wagmi/actions')
const hash = await sendTransactionAsync({
to: contractAddress,
data: dataWithSuffix,
value,
})
return { hash }
}, [isConnected])
return { execute, isPending: false, hasPaymaster: false }
}
ПРИМЕЧАНИЕ: wagmi v1 не поддерживает sendCalls/useCapabilities.
Рекомендуется обновить до v2 для полной Smart Wallet поддержки.
---
ВАРИАНТ C: ethers.js (v5/v6)
// Утилита (не React хук — для этого фреймворка)
import { ethers } from 'ethers' // v5 или v6
const BUILDER_ADDRESS = '0xYOUR_BUILDER_ADDRESS_HERE'
export async function executeWithBuilder(signer, { address, abi, functionName, args, value }) {
const iface = new ethers.Interface(abi) // v6: Interface, v5: utils.Interface
const calldata = iface.encodeFunctionData(functionName, args || [])
const dataWithSuffix = calldata + BUILDER_ADDRESS.slice(2).toLowerCase()
const tx = await signer.sendTransaction({
to: address,
data: dataWithSuffix,
value: value || 0n,
})
---
ВАРИАНТ D: viem (без wagmi)
import { encodeFunctionData } from 'viem'
const BUILDER_ADDRESS = '0xYOUR_BUILDER_ADDRESS_HERE'
export async function executeWithBuilder(walletClient, { address, abi, functionName, args, value })
{
const hash = await walletClient.writeContract({
address,
abi,
functionName,
args: args || [],
value,
dataSuffix: `0x${BUILDER_ADDRESS.slice(2)}`, // viem поддерживает dataSuffix
})
return { hash }
}
---
ШАГ 4: ЗАМЕНИ ВСЕ ТРАНЗАКЦИИ
Из PROJECT_BRIEF.md секция 5 у тебя есть список ВСЕХ мест с write-транзакциями.
Замени КАЖДЫЙ вызов на execute() из новой обёртки.
Было (wagmi v2):
const { writeContractAsync } = useWriteContract()
await writeContractAsync({
address: GAME_CONTRACT,
abi: gameAbi,
functionName: 'enterGame',
value: parseEther('0.001'),
})
Стало:
const { execute } = useSmartTransaction()
await execute({
address: GAME_CONTRACT,
abi: gameAbi,
functionName: 'enterGame',
value: parseEther('0.001'),
})
Проверь grep'ом что ничего не пропустил:
# Для wagmi: не должно остаться прямых вызовов writeContract (кроме внутри хука)
grep -rn "writeContractAsync\|useContractWrite\|sendTransaction" --include="*.jsx" --include="*.tsx"
--include="*.js" --include="*.ts" src/ --exclude="*useSmartTransaction*"
# Для ethers: не должно остаться прямых contract.functionName() без builder suffix
grep -rn "\.sendTransaction\|contract\." --include="*.jsx" --include="*.tsx" --include="*.js" src/
1. npm run build — билд проходит без ошибок
2. Grep: убедись что writeContractAsync/useContractWrite не используется напрямую в UI хуках (только
внутри useSmartTransaction)
3. Попроси пользователя проверить вручную (это нельзя сделать из CLI):
- "Открой приложение в браузере, подключи кошелёк, отправь тестовую транзакцию"
- "В BaseScan открой транзакцию → Input Data → последние 40 hex символов должны = builder address
(без 0x)"
- "Подтверди что builder suffix виден в транзакции"
НЕ ПИШИ "проверка пройдена" пока пользователь не подтвердит. Это единственный способ убедиться что
ERC-8021 реально работает.
ВЫВОД — СОХРАНИ В PROJECT_BRIEF.md
ОБЯЗАТЕЛЬНО: Допиши в PROJECT_BRIEF.md:
## 14. SMART TRANSACTIONS (Prompt 3)
Дата: [сегодня]
- Builder Address: 0x...
- Стек: [wagmi v2 / wagmi v1 / ethers / viem]
- Обёртка: [путь к файлу]
- Транзакций обновлено: X
- [файл:строка] — functionName
- Smart Wallet (sendCalls + paymaster): [да/нет — только wagmi v2+]
- ERC-8021 builder suffix: добавлен ко всем транзакциям
- Build: ✅
Напиши "Smart Transactions готовы, PROJECT_BRIEF.md обновлён. Готов к Prompt 4."
PROMPT 4 — BASE APP COMPLIANCE
Приведение UI в соответствие с требованиями Base. Это самый объёмный промпт.
Что делаем: Проходим по чеклисту Base Featured Guidelines и приводим UI в порядок — добавляем dark/light mode с переключателем, заменяем 0x адреса на Basename, настраиваем safe area padding, touch targets 44px, viewport-fit=cover, overscroll-behavior, OG meta теги. Если бандл > 500KB — делаем code splitting. Если нет loading индикаторов — добавляем.
Зачем: Base не пустит в Featured без соответствия гайдлайнам. Каждый пункт — реальное требование из docs.base.org/mini-apps/featured-guidelines. Без dark mode — реджект. Без safe area — контент залезает под notch. Без overscroll-behavior — pull-to-refresh ломает геймплей. Без Basename — пользователь видит хеши вместо имён.
Что от тебя нужно: Ничего — агент сам пройдёт по всем блокам (A-F), проверит что есть и чего нет, создаст/поправит файлы. Убедись что после выполнения переключатель темы работает и light mode выглядит адекватно.
ЗАДАЧА: Привести UI в соответствие с требованиями Base Mini App
Прочитай PROJECT_BRIEF.md — секции 6, 7 (адреса в UI, темы и layout).
Base App имеет обязательные требования. Если не соответствуешь — не попадёшь в Featured.
Ниже — полный чеклист. Пройди по КАЖДОМУ пункту, проверь в коде, исправь если нужно.
### БЛОК A: DARK / LIGHT MODE [ОБЯЗАТЕЛЬНО]
Проверь: есть ли в проекте поддержка тёмной и светлой темы.
1. Создай ThemeProvider (или useTheme хук):
```jsx
// src/hooks/useTheme.jsx (или .tsx)
import { createContext, useContext, useState, useEffect } from 'react'
const ThemeContext = createContext()
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() => {
if (typeof window === 'undefined') return 'dark'
const saved = localStorage.getItem('app-theme')
if (saved) return saved
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
})
useEffect(() => {
const root = document.documentElement
root.classList.remove('light', 'dark')
root.classList.add(theme)
localStorage.setItem('app-theme', theme)
// Обнови meta theme-color для mobile browser chrome
const meta = document.querySelector('meta[name="theme-color"]')
if (meta) meta.content = theme === 'dark' ? '#0a0a1a' : '#F5F7FA'
}, [theme])
const toggle = () => setTheme(t => t === 'dark' ? 'light' : 'dark')
return (
<ThemeContext.Provider value={{ theme, setTheme, toggle }}>
{children}
</ThemeContext.Provider>
)
}
export const useTheme = () => useContext(ThemeContext)
2. Оберни приложение в ThemeProvider (в App.jsx или main.jsx)
3. Определи CSS переменные для обеих тем:
/* В глобальном CSS (index.css / globals.css) */
:root, .light {
--color-bg: #F5F7FA;
--color-surface: #FFFFFF;
--color-text: #1A1A2E;
--color-text-secondary: #6B7280;
--color-border: #E5E7EB;
--color-accent: #0052FF; /* Base blue */
}
.dark {
--color-bg: #0a0a1a;
--color-surface: #1a1a2e;
--color-text: #F0F0F0;
--color-text-secondary: #9CA3AF;
--color-border: #2a2a3e;
--color-accent: #0052FF;
}
4. Замени hardcoded цвета на CSS variables во ВСЕХ компонентах.
Если используешь Tailwind — настрой через tailwind.config:
// tailwind.config.js
module.exports = {
darkMode: 'class',
theme: {
extend: {
colors: {
bg: 'var(--color-bg)',
surface: 'var(--color-surface)',
// ...
}
}
}
}
5. Если используешь RainbowKit/ConnectKit — переключай их тему тоже:
import { darkTheme, lightTheme } from '@rainbow-me/rainbowkit'
const { theme } = useTheme()
<RainbowKitProvider theme={theme === 'dark' ? darkTheme() : lightTheme()}>
Если theme УЖЕ ЕСТЬ — проверь:
- Переключатель работает
- Сохраняется в localStorage
- Системная тема детектится (prefers-color-scheme)
- RainbowKit/wallet UI тоже переключается
- Нет мест с хардкод цветами (#ffffff, rgb(0,0,0)) в inline styles
---
БЛОК B: АДРЕСА [ОБЯЗАТЕЛЬНО]
Base App ЗАПРЕЩАЕТ показывать полные 0x адреса. Пользователь должен видеть имя, не хеш.
1. Найди ВСЕ места где показываются адреса (из PROJECT_BRIEF.md секция 6)
2. Замени полные адреса на сокращённые:
// Утилита для сокращения адресов
function shortenAddress(address, chars = 4) {
if (!address) return ''
return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`
}
// Пример: 0xB3b8...595d
3. Ещё лучше — показывай Basename (ENS на Base):
import { useEnsName, useEnsAvatar } from 'wagmi'
import { base } from 'wagmi/chains'
function useBasename(address) {
const { data: name, isLoading } = useEnsName({
address,
chainId: base.id,
universalResolverAddress: '0xC6d566A56A1aFf6508b41f6c90ff131615583BCD',
query: { enabled: Boolean(address), staleTime: 5 * 60 * 1000 },
})
const { data: avatar } = useEnsAvatar({
name,
chainId: base.id,
query: { enabled: Boolean(name), staleTime: 5 * 60 * 1000 },
})
return { name: name || shortenAddress(address), avatar, isLoading }
}
4. Используй useBasename(address) везде где показываешь адрес: лидерборд, профиль, история.
---
БЛОК C: LAYOUT [ОБЯЗАТЕЛЬНО]
Если в приложении есть навигация между экранами — создай bottom nav:
<nav className="fixed bottom-0 left-0 right-0 bg-[var(--color-surface)] border-t
border-[var(--color-border)]"
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}>
<div className="flex justify-around items-center h-14">
<NavItem icon="🎮" label="Game" to="/" />
<NavItem icon="🏆" label="Rewards" to="/rewards" />
<NavItem icon="👤" label="Profile" to="/profile" />
</div>
</nav>
Требования:
- Иконки + текстовые лейблы (НЕ только иконки)
- Safe area padding внизу (для iPhone notch): env(safe-area-inset-bottom)
- Touch targets >= 44px (высота каждого элемента nav)
Если одноэкранное приложение без навигации — bottom nav не нужен (пометь N/A).
- Все экраны оптимизированы под вертикальный экран
- CTA кнопки видимые без скролла (в верхней/средней части)
- Контент не обрезается на узких экранах (320px width тест)
Найди все кнопки, ссылки, интерактивные элементы. Каждый должен быть минимум 44x44px:
button, a, [role="button"] {
min-height: 44px;
min-width: 44px;
}
---
БЛОК D: КЛИЕНТ-АГНОСТИЧНОСТЬ [ОБЯЗАТЕЛЬНО]
Найди в коде:
grep -rni "farcaster\|warpcast\|cast\|fid" --include="*.jsx" --include="*.tsx" --include="*.js"
Замени:
- "Share to Warpcast" → "Share"
- "Post a cast" → "Share to feed"
- Hardcoded Warpcast URLs → generic share
Исключение: farcaster.json manifest и SDK imports — их НЕ трогай.
---
БЛОК E: META TAGS [ОБЯЗАТЕЛЬНО]
<!-- Обязательные -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0,
user-scalable=no, viewport-fit=cover" />
<meta name="theme-color" content="#0a0a1a" />
<!-- Apple PWA -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<!-- OG tags -->
<meta property="og:title" content="App Name" />
<meta property="og:description" content="Description" />
<meta property="og:image" content="https://your-domain.com/og-1200x630.png" />
<!-- Предотвращение pulldown-to-refresh и scroll bounce -->
<style>
body { overscroll-behavior: none; }
</style>
РЕАЛЬНЫЙ СЛУЧАЙ: Без viewport-fit=cover контент залезает под iPhone notch.
Без overscroll-behavior: none — pull-to-refresh мешает геймплею.
Без maximum-scale=1.0 — pinch zoom ломает мобильный layout.
---
БЛОК F: ПРОИЗВОДИТЕЛЬНОСТЬ
- Загрузка < 3 секунд
- Проверь размер бандла: npm run build → посмотри output
- Если > 500KB JS — нужен code splitting / lazy loading
- Loading индикаторы при всех async операциях
- Нет мерцания при загрузке (skeleton/placeholder)
---
ВЫВОД — СОХРАНИ В PROJECT_BRIEF.md
ОБЯЗАТЕЛЬНО: Допиши в PROJECT_BRIEF.md:
## 15. BASE APP COMPLIANCE (Prompt 4)
Дата: [сегодня]
| Требование | Статус | Действие |
|------------|--------|----------|
| Dark mode | ✅/❌ | [что сделано] |
| Light mode | ✅/❌ | ... |
| Theme toggle | ✅/❌ | ... |
| System preference | ✅/❌ | ... |
| No 0x addresses | ✅/❌ | ... |
| Basename support | ✅/❌ | ... |
| Bottom nav | ✅/❌ | ... |
| Nav labels | ✅/❌ | ... |
| Safe area | ✅/❌ | ... |
| Touch 44px | ✅/❌ | ... |
| Portrait layout | ✅/❌ | ... |
| Client-agnostic | ✅/❌ | ... |
| Meta tags | ✅/❌ | ... |
| viewport-fit | ✅/❌ | ... |
| overscroll | ✅/❌ | ... |
| Performance <3s | ✅/❌ | ... |
| Loading states | ✅/❌ | ... |
### Файлы созданные/изменённые:
- [список файлов]
Напиши "Base App Compliance готов, PROJECT_BRIEF.md обновлён. Готов к Prompt 5."
Ссылки, откуда взяты требования:
Featured Checklist: https://docs.base.org/mini-apps/featured-guidelines/overview
Build Checklist: https://docs.base.org/mini-apps/quickstart/build-checklist
Base App Compatibility: https://docs.base.org/mini-apps/troubleshooting/base-app-compatibility
Create a Mini App: https://docs.base.org/mini-apps/quickstart/create-new-miniapp
Embeds and Previews: https://docs.base.org/mini-apps/core-concepts/embeds-and-previews
Base Mini Apps Overview: https://www.base.org/build/mini-apps
PROMPT 5 — FARCASTER MANIFEST + SDK + ASSETS
Что делаем: Создаём "паспорт" приложения для Base App каталога — файл /.well-known/farcaster.json, интегрируем @farcaster/miniapp-sdk и добавляем embed-метатеги в HTML. Также автоматически генерируем все картинки для манифеста (icon, splash, hero, OG) — скрипт generate-assets.sh создаёт их из HTML-шаблонов без единого API-вызова. Если есть Gemini API ключ — hero и OG будут AI-сгенерированы (красивее), если нет — всё создастся процедурно.
Зачем: Без manifest'а Base App и Warpcast не знают что твоё приложение — Mini App. Без sdk.actions.ready() пользователь видит бесконечный загрузочный экран — это самая частая ошибка. Без мета-тегов нет превью при шаринге в ленте. Картинки нужны для каталога — и теперь они генерируются автоматически, а не руками в Figma.
Что от тебя нужно: Ничего — агент сам запустит скрипт генерации, создаст манифест и добавит мета-теги. Проверь что tagline ≤ 30 символов и description ≤ 170 символов — Base реджектит при нарушении лимитов. Если результат генерации не нравится — можно перезапустить скрипт или заменить картинки вручную.
ЗАДАЧА: Настроить Farcaster Mini App manifest, SDK и сгенерировать ассеты
Прочитай PROJECT_BRIEF.md — секция 9 (Farcaster / Base App).
Manifest файл `/.well-known/farcaster.json` — это "паспорт" приложения.
Без него Base App и Warpcast не знают что это Mini App.
Все картинки для манифеста генерируются автоматически скриптом.
Создай файл `scripts/generate-assets.sh` если его ещё нет:
```bash
# Проверь наличие
ls scripts/generate-assets.sh 2>/dev/null && echo "EXISTS" || echo "NEED TO CREATE"
Если скрипта нет — создай его. Скрипт должен:
1. Генерировать 4 картинки из HTML-шаблонов через Python + Playwright (system Chrome):
- icon-1024.png (1024×1024) — название на тёмном фоне, grid pattern, accent glow
- splash-200.png (200×200) — уменьшенная иконка
- hero-1200x630.png (1200×630) — баннер с визуализацией геймплея (SVG чарт/графика)
- og-1200x630.png (1200×630) — социальное превью
2. Если есть GEMINI_API_KEY в ~/.claude/.env — использовать Gemini 2.5 Flash Image
для hero и OG (AI-генерация, красивее):
- API:
https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent
- Auth: header x-goog-api-key
- Config: "responseModalities": ["IMAGE", "TEXT"]
- Gemini генерит 1024×1024 — нужен resize через PIL до 1200×630
- Если Gemini не ответил — fallback на HTML
3. Без GEMINI_API_KEY — ВСЁ генерируется процедурно (HTML шаблоны, 0 API)
4. Параметры:
- --name "App Name" (автодетект из package.json)
- --tagline "Tagline"
- --accent "#00ff88" (accent color)
- --bg "#0a0a1a" (background)
- --no-ai — принудительно процедурный режим
- --skip-existing — не перезаписывать
chmod +x scripts/generate-assets.sh
./scripts/generate-assets.sh --name "APP_NAME" --tagline "TAGLINE"
Подставь реальные name и tagline из PROJECT_BRIEF.md.
for f in public/icon-1024.png public/splash-200.png public/hero-1200x630.png public/og-1200x630.png;
do
if [ -f "$f" ]; then
python3 -c "from PIL import Image; i=Image.open('$f'); print(f'✅ {\"$f\"}:
{i.size[0]}x{i.size[1]}')" 2>/dev/null \
|| echo "✅ $f exists ($(wc -c < "$f" | tr -d ' ') bytes)"
else
echo "❌ $f MISSING"
fi
done
npm install @farcaster/miniapp-sdk
ПРОВЕРЬ ВЕРСИЮ после установки:
ls node_modules/@farcaster/miniapp-sdk/dist/
Убедись что sdk экспортируется.
Добавь в КОРНЕВОЙ компонент приложения (App.jsx / App.tsx / layout.tsx):
import { useEffect } from 'react'
useEffect(() => {
// Динамический импорт — SDK может не быть в обычном браузере
import('@farcaster/miniapp-sdk').then(({ sdk }) => {
sdk.actions.ready()
}).catch(() => {
// Не Mini App контексте (обычный браузер) — ok, игнорируем
})
}, [])
РЕАЛЬНЫЙ СЛУЧАЙ: Без sdk.actions.ready() Base App показывает бесконечный
загрузочный экран. Пользователь видит splash image и ничего не происходит.
Это самая частая ошибка новых Mini App. Вызывай ready() СРАЗУ при монтировании.
ВАЖНО: sdk.actions.ready() нужно вызывать даже если приложение ещё грузит данные.
Оно говорит "фрейм загрузился", не "данные готовы". Вызывай в useEffect([], []).
Создай файл public/.well-known/farcaster.json:
КРИТИЧНО: Ключ ОБЯЗАТЕЛЬНО "miniapp", НЕ "frame"!
Ключ "frame" — это устаревший формат Frame V1. Для Mini App нужен "miniapp".
{
"accountAssociation": {
"header": "PENDING_SIGNATURE",
"payload": "PENDING",
"signature": "PENDING"
},
"miniapp": {
"version": "1",
"name": "APP_NAME",
"subtitle": "Subtitle here",
"tagline": "Max 30 characters tagline",
"description": "Max 170 characters. No emojis or special characters.",
"primaryCategory": "games",
"tags": ["tag1", "tag2", "tag3"],
"homeUrl": "https://YOUR_DOMAIN",
"iconUrl": "https://YOUR_DOMAIN/icon-1024.png",
"splashImageUrl": "https://YOUR_DOMAIN/splash-200.png",
"splashBackgroundColor": "#0a0a1a",
"heroImageUrl": "https://YOUR_DOMAIN/hero-1200x630.png",
"screenshotUrls": [],
"ogTitle": "APP_NAME",
"ogDescription": "Short OG description, max 100 chars",
"ogImageUrl": "https://YOUR_DOMAIN/og-1200x630.png"
},
"baseBuilder": {
"ownerAddress": "0xYOUR_BUILDER_ADDRESS"
}
}
Заполни поля из PROJECT_BRIEF.md:
- name, homeUrl, ownerAddress — из проекта
- iconUrl, splashImageUrl, heroImageUrl, ogImageUrl — указывают на файлы сгенерированные в Шаге 1
- splashBackgroundColor — из --bg цвета (default: #0a0a1a)
┌──────────┬───────────────────────────────────────────────────────────────────────────┬────────┐
│ │ │ Ошибка │
│ Поле │ Лимит │ если │
│ │ │ наруше │
│ │ │ но │
├──────────┼───────────────────────────────────────────────────────────────────────────┼────────┤
│ │ │ Обреже │
│ name │ <= 32 chars │ тся в │
│ │ │ UI │
├──────────┼───────────────────────────────────────────────────────────────────────────┼────────┤
│ │ │ Reject │
│ subtitle │ <= 30 chars, sentence case, без точки │ при │
│ │ │ ревью │
├──────────┼───────────────────────────────────────────────────────────────────────────┼────────┤
│ tagline │ <= 30 chars │ Обреже │
│ │ │ тся │
├──────────┼───────────────────────────────────────────────────────────────────────────┼────────┤
│ descript │ <= 170 chars, без эмодзи │ Reject │
│ ion │ │ │
├──────────┼───────────────────────────────────────────────────────────────────────────┼────────┤
│ │ │ Ошибка │
│ tags │ max 5, каждый <= 20 chars, lowercase, без пробелов │ индек │
│ │ │ сации │
├──────────┼───────────────────────────────────────────────────────────────────────────┼────────┤
│ primaryC │ Одно из: games/social/finance/utility/productivity/entertainment/health-f │ Ошибка │
│ ategory │ itness/news-media/music/shopping/education/developer-tools/art-creativity │ │
├──────────┼───────────────────────────────────────────────────────────────────────────┼────────┤
│ iconUrl │ PNG, 1024×1024, без прозрачности │ Обязат │
│ │ │ ельно │
├──────────┼───────────────────────────────────────────────────────────────────────────┼────────┤
│ splashIm │ │ Рекоме │
│ ageUrl │ PNG, ~200×200 │ ндуетс │
│ │ │ я │
├──────────┼───────────────────────────────────────────────────────────────────────────┼────────┤
│ heroImag │ │ Рекоме │
│ eUrl │ PNG, 1200×630 │ ндуетс │
│ │ │ я │
└──────────┴───────────────────────────────────────────────────────────────────────────┴────────┘
Добавь в <head> файла index.html ОБА meta тега (для совместимости):
<!-- Farcaster Mini App embed — ОБЯЗАТЕЛЬНО ОБА тега -->
<meta name="fc:miniapp"
content='{"version":"1","imageUrl":"https://YOUR_DOMAIN/og-1200x630.png","button":{"title":"Open
App","action":{"type":"launch_miniapp","name":"APP_NAME","url":"https://YOUR_DOMAIN","splashImageUrl
":"https://YOUR_DOMAIN/splash-200.png","splashBackgroundColor":"#0a0a1a"}}}' />
<meta name="fc:frame"
content='{"version":"1","imageUrl":"https://YOUR_DOMAIN/og-1200x630.png","button":{"title":"Open
App","action":{"type":"launch_miniapp","name":"APP_NAME","url":"https://YOUR_DOMAIN","splashImageUrl
":"https://YOUR_DOMAIN/splash-200.png","splashBackgroundColor":"#0a0a1a"}}}' />
КРИТИЧНО: Ставь ОБА тега — fc:miniapp И fc:frame.
fc:miniapp — новый формат (Base App каталог).
fc:frame — обратная совместимость (Warpcast лента, embed preview).
Замени YOUR_DOMAIN и APP_NAME на реальные значения.
ШАГ 6: SECURITY HEADERS (если есть бэкенд)
Если бэкенд отдаёт фронтенд — добавь в CSP:
frame-ancestors 'self' https://*.base.org https://warpcast.com https://*.coinbase.com
https://*.farcaster.xyz
Без этого — Base App не сможет загрузить приложение в iframe.
# 1. Картинки существуют
for f in public/icon-1024.png public/splash-200.png public/hero-1200x630.png public/og-1200x630.png;
do
test -f "$f" && echo "✅ $f" || echo "❌ $f MISSING"
done
# 2. Manifest JSON валиден
cat public/.well-known/farcaster.json | python3 -m json.tool > /dev/null && echo "✅ Valid JSON" ||
echo "❌ Invalid JSON"
# 3. Ключ = miniapp
python3 -c "import json; d=json.load(open('public/.well-known/farcaster.json')); assert 'miniapp' in
d and 'frame' not in d, 'FAIL'; print('✅ miniapp key')"
# 4. Tagline существует и <= 30 chars
python3 -c "
import json; d=json.load(open('public/.well-known/farcaster.json'))['miniapp']
tl = d.get('tagline','')
assert tl, 'MISSING'
assert len(tl) <= 30, f'TOO LONG: {len(tl)}/30'
print(f'✅ tagline: \"{tl}\" ({len(tl)}/30)')
"
# 5. Все поля заполнены
python3 -c "
import json; d=json.load(open('public/.well-known/farcaster.json'))['miniapp']
for k in ['name','tagline','description','homeUrl','iconUrl','primaryCategory']:
v = d.get(k,'')
print(f'{chr(9989) if v else chr(10060)} {k}: {v[:50] if v else \"MISSING\"}')"
# 6. ОБА meta тега в HTML
grep -q 'fc:miniapp' index.html && echo "✅ fc:miniapp meta" || echo "❌ fc:miniapp meta MISSING"
grep -q 'fc:frame' index.html && echo "✅ fc:frame meta" || echo "❌ fc:frame meta MISSING"
# 7. SDK ready
grep -rq 'sdk.actions.ready\|sdk\.actions\.ready' src/ && echo "✅ sdk.actions.ready()" || echo "❌
sdk.actions.ready() NOT FOUND"
ВЫВОД — СОХРАНИ В PROJECT_BRIEF.md
ОБЯЗАТЕЛЬНО: Допиши в PROJECT_BRIEF.md:
## 16. FARCASTER MANIFEST + ASSETS (Prompt 5)
Дата: [сегодня]
- Assets: сгенерированы скриптом ✅
- icon-1024.png: [размер]
- splash-200.png: [размер]
- hero-1200x630.png: [размер] ([AI/процедурный])
- og-1200x630.png: [размер] ([AI/процедурный])
- Manifest: public/.well-known/farcaster.json ✅
- SDK: @farcaster/miniapp-sdk@[версия]
- sdk.actions.ready(): [файл:строка]
- fc:miniapp meta: [файл] ✅
- fc:frame meta: [файл] ✅
- Account Association: ⏳ (после деплоя в Prompt 6)
- CSP frame-ancestors: ✅/N/A
Manifest fields:
- name: "[value]" ([X]/32 chars)
- tagline: "[value]" ([X]/30 chars)
- description: "[value]" ([X]/170 chars)
- primaryCategory: [value]
Напиши "Manifest и ассеты готовы, PROJECT_BRIEF.md обновлён. Готов к Prompt 6."
PROMPT 6 — ДЕПЛОЙ + ACCOUNT ASSOCIATION + SMOKE TEST
Всё автоматизировано: билд, деплой, подпись манифеста, проверки. Единственный блокер — FID (Farcaster ID). Если его нет, агент скажет что делать.
Что делаем: Production-деплой + полная настройка Farcaster для приложения. Агент билдит проект, деплоит на хостинг, проверяет есть ли FID у кошелька — если нет, регистрирует ончейн на Optimism (~0.0002 ETH). Затем подписывает манифест (Account Association), создаёт Farcaster-профиль приложения (имя, аватар, bio), и постит первый каст с URL — это триггерит индексацию. В конце —
smoke test всего деплоя.
Зачем: Без FID и Account Association Base App не принимает приложение. Без первого каста индексация не запустится — приложение не появится в каталоге. Всё это делается автоматически скриптами, руками ничего не нужно.
Что от тебя нужно: Убедись что на кошельке оператора есть ~0.0002 ETH на Optimism (регистрация FID + storage). Всё остальное автоматизировано.
ЗАДАЧА: Финальный деплой, подпись манифеста, Farcaster setup, smoke test
Прочитай PROJECT_BRIEF.md — секции 8 (деплой) и 16 (manifest).
Всё подготовлено в предыдущих промптах. Этот промпт делает:
1. Production билд + деплой
2. Регистрация FID (если нет)
3. Account Association (подпись манифеста)
4. Farcaster full setup (storage, signer, профиль, fname, первый каст)
5. Rebuild + redeploy с подписанным манифестом
6. Smoke test
Проверь:
- Билд без ошибок
- .well-known/farcaster.json есть в build output (dist/ или .next/)
- grep "84532" dist/assets/*.js — НЕ должен находить testnet chain ID
ТИПИЧНАЯ ОШИБКА: VITE_* переменные бэйкаются в бандл при билде.
Если билдишь без VITE_NETWORK=mainnet, в бандле будет testnet.
Определи платформу из PROJECT_BRIEF.md:
unset RAILWAY_TOKEN && railway up
Запомни production URL — нужен для шагов ниже.
FID — числовой Farcaster ID. Без него нельзя подписать манифест и постить.
Создай scripts/register-fid.mjs:
ВАЖНО: Расширение .mjs ОБЯЗАТЕЛЬНО. viem и @farcaster/hub-nodejs работают ТОЛЬКО через ES modules
import. require() = ошибка.
#!/usr/bin/env node
import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { optimism } from 'viem/chains'
import { readFileSync } from 'fs'
const envFile = readFileSync('./contracts/.env', 'utf8')
const keyMatch = envFile.match(/OPERATOR_PRIVATE_KEY=(.+)/)
if (!keyMatch) { console.error('OPERATOR_PRIVATE_KEY not found'); process.exit(1) }
const pk = keyMatch[1].trim().startsWith('0x') ? keyMatch[1].trim() : `0x${keyMatch[1].trim()}`
const account = privateKeyToAccount(pk)
console.log(`Wallet: ${account.address}`)
const RECOVERY_PROXY = '0x00000000FcB080a4D6c39a9354dA9EB9bC104cd7'
const ID_REGISTRY = '0x00000000Fc6c5F01Fc30151999387Bb99A9f489b'
const ID_GATEWAY = '0x00000000Fc25870C6eD6b6c7E41Fb078b7656f69'
const ID_REGISTRY_ABI = [
{ type: 'function', name: 'idOf', stateMutability: 'view',
inputs: [{ name: 'owner', type: 'address' }],
outputs: [{ name: '', type: 'uint256' }] }
]
const ID_GATEWAY_ABI = [
{ type: 'function', name: 'price', stateMutability: 'view',
inputs: [{ name: 'extraStorage', type: 'uint256' }],
outputs: [{ name: '', type: 'uint256' }] },
{ type: 'function', name: 'register', stateMutability: 'payable',
inputs: [{ name: 'recovery', type: 'address' }, { name: 'extraStorage', type: 'uint256' }],
outputs: [{ name: '', type: 'uint256' }] }
]
const publicClient = createPublicClient({ chain: optimism, transport:
http('https://mainnet.optimism.io') })
const walletClient = createWalletClient({ account, chain: optimism, transport:
http('https://mainnet.optimism.io') })
const existingFid = await publicClient.readContract({
address: ID_REGISTRY, abi: ID_REGISTRY_ABI, functionName: 'idOf', args: [account.address]
})
if (existingFid > 0n) {
console.log(`Already registered — FID: ${existingFid}`)
process.exit(0)
}
console.log('Registering...')
const price = await publicClient.readContract({
address: ID_GATEWAY, abi: ID_GATEWAY_ABI, functionName: 'price', args: [0n]
})
console.log(`Price: ${Number(price) / 1e18} ETH`)
const hash = await walletClient.writeContract({
address: ID_GATEWAY, abi: ID_GATEWAY_ABI, functionName: 'register',
args: [RECOVERY_PROXY, 0n], value: price
})
await publicClient.waitForTransactionReceipt({ hash })
const newFid = await publicClient.readContract({
address: ID_REGISTRY, abi: ID_REGISTRY_ABI, functionName: 'idOf', args: [account.address]
})
console.log(`Registered FID: ${newFid}`)
node scripts/register-fid.mjs
# Запомни FID — нужен для всех следующих шагов
Если у кошелька УЖЕ есть FID — скрипт просто напечатает его и выйдет.
Создай scripts/sign-manifest.mjs:
#!/usr/bin/env node
import { privateKeyToAccount } from 'viem/accounts'
import { readFileSync, writeFileSync } from 'fs'
const envFile = readFileSync('./contracts/.env', 'utf8')
const keyMatch = envFile.match(/OPERATOR_PRIVATE_KEY=(.+)/)
if (!keyMatch) { console.error('OPERATOR_PRIVATE_KEY not found'); process.exit(1) }
const pk = keyMatch[1].trim().startsWith('0x') ? keyMatch[1].trim() : `0x${keyMatch[1].trim()}`
const FID = parseInt(process.env.FID)
if (!FID) { console.error('FID env var required'); process.exit(1) }
const MANIFEST_PATH = './public/.well-known/farcaster.json'
const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'))
const DOMAIN = new URL(manifest.miniapp?.homeUrl).hostname
const account = privateKeyToAccount(pk)
console.log(`Signing — FID: ${FID}, Domain: ${DOMAIN}, Wallet: ${account.address}`)
function base64url(obj) {
return Buffer.from(JSON.stringify(obj)).toString('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}
function base64urlBytes(hexStr) {
return Buffer.from(hexStr.slice(2), 'hex').toString('base64')
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}
const header = { fid: FID, type: 'custody', key: account.address }
const payload = { domain: DOMAIN }
const encodedHeader = base64url(header)
const encodedPayload = base64url(payload)
const signature = await account.signMessage({ message: `${encodedHeader}.${encodedPayload}` })
manifest.accountAssociation = {
header: encodedHeader,
payload: encodedPayload,
signature: base64urlBytes(signature)
}
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n')
console.log('Account Association signed')
FID=YOUR_FID node scripts/sign-manifest.mjs
Storage + Ed25519 signer + профиль + fname + первый каст.
npm install @noble/ed25519 @noble/hashes @farcaster/hub-nodejs
ИЗВЕСТНЫЕ ЛОВУШКИ (все найдены на реальных проектах — ОБЯЗАТЕЛЬНО УЧТИ)
Каждый из этих пунктов стоил реальных денег и часов отладки.
НЕ пропускай, НЕ "упрощай". Если проигнорируешь — получишь те же ошибки.
1. ESM-only (.mjs)
viem и @farcaster/hub-nodejs = ES modules. Скрипт ОБЯЗАТЕЛЬНО .mjs. require() = ошибка.
2. ed25519 sha512 инициализация
БЕЗ этих строк NobleEd25519Signer не может подписывать — молча возвращает ошибку:
import * as ed from '@noble/ed25519'
import { sha512 } from '@noble/hashes/sha512'
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m))
3. @noble/ed25519 v3: randomSecretKey(), НЕ randomPrivateKey()
Функция переименована в v3. getPublicKeyAsync() принимает Uint8Array, НЕ hex string.
const privKeyBytes = ed.utils.randomSecretKey() // ← Uint8Array
const pubKeyBytes = await ed.getPublicKeyAsync(privKeyBytes) // ← тоже Uint8Array
// Конвертируй в hex ТОЛЬКО для сохранения в JSON:
const privHex = Buffer.from(privKeyBytes).toString('hex')
4. StorageRegistry.rentedUnits — ГЛОБАЛЬНЫЙ счётчик, НЕ per-FID
Нельзя проверить "арендован ли storage для конкретного FID" on-chain. Без защиты = повторные
платежи.
Решение — двойная проверка:
- Primary: локальный маркер файл .storage-rented (JSON с fid и tx hash)
- Secondary: Hub API storageLimitsByFid?fid=
- Rent ТОЛЬКО если ОБА отрицательные
- Записать маркер СРАЗУ после успешного rent (до любых других операций)
5. KeyGateway.add() metadata — TUPLE encoding, не flat
Solidity abi.decode(data, (SignedKeyRequestMetadata)) = struct = tuple.
Flat encoding (uint256, address, bytes, uint256) → REVERT (другой byte layout).
const metadata = encodeAbiParameters(
[{ type: 'tuple', components: [
{ name: 'requestFid', type: 'uint256' },
{ name: 'requestSigner', type: 'address' },
{ name: 'signature', type: 'bytes' },
{ name: 'deadline', type: 'uint256' },
]}],
[{ requestFid: BigInt(FID), requestSigner: account.address, signature: sig, deadline }]
)
6. Neynar demo key (NEYNAR_API_DOCS) → 403 из Node.js fetch()
Тот же ключ работает из curl. Для ВСЕХ запросов к Hub API используй execSync('curl ...'):
const result = execSync(
`curl -s "${HUB_URL}/endpoint?fid=${FID}" -H "x-api-key: ${NEYNAR_KEY}"`,
{ encoding: 'utf8' }
)
const data = JSON.parse(result)
7. makeUserDataAdd / makeCastAdd — сигнатура (body, dataOptions, signer)
FID передаётся во ВТОРОЙ аргумент, НЕ в body. Если fid в body → ошибка "fid is missing".
// ПРАВИЛЬНО:
makeUserDataAdd(
{ type, value }, // body
{ fid: FID, network: 1 }, // dataOptions — fid ЗДЕСЬ
signer
)
// НЕПРАВИЛЬНО (fid в body):
makeUserDataAdd({ type, value, fid: FID }, signer) // → "fid is missing"
8. signUserNameProofClaim возвращает Uint8Array, не hex
Fnames API (fnames.farcaster.xyz/transfers) ожидает hex string с 0x префиксом:
const sig = await eip712Signer.signUserNameProofClaim({ name, timestamp: BigInt(ts), owner })
if (sig.isOk()) {
const sigHex = '0x' + Buffer.from(sig.value).toString('hex')
// Используй sigHex в JSON body
}
9. submitMessage — ТОЛЬКО curl --data-binary, НЕ fetch
Node.js fetch ломает бинарные protobuf данные при отправке в Hub:
const encoded = Buffer.from(Message.encode(msg).finish())
writeFileSync(tmpFile, encoded)
execSync(`curl -s -X POST "${HUB_URL}/submitMessage" \
-H "x-api-key: ${NEYNAR_KEY}" \
-H "Content-Type: application/octet-stream" \
--data-binary @${tmpFile}`, { encoding: 'utf8' })
10. Hub sync — НЕ exit на timeout
Hub'ы синкают ончейн-события с задержкой (минуты → часы). Если поллинг не нашёл signer за 10 минут —
НЕ exit. Продолжай — signer скорее всего уже работает, просто API отстаёт.
11. Hub sync key comparison — strip 0x, lowercase
Hub возвращает ключ с 0x префиксом. Локальный ключ без 0x. При сравнении:
const hubKey = (e.signerEventBody?.key || '').toLowerCase().replace('0x', '')
const localKey = signerPublicKey.toLowerCase()
const match = hubKey.includes(localKey) || localKey.includes(hubKey)
12. Секреты в .gitignore
.farcaster-app-signer.json содержит приватный ключ signer. .storage-rented — маркер.
Оба ОБЯЗАТЕЛЬНО добавить в .gitignore.
---
Скрипт scripts/setup-farcaster.mjs
#!/usr/bin/env node
/**
* Full Farcaster profile setup: storage, signer, profile, fname, first cast.
* Idempotent — safe to re-run, skips completed steps.
*
* Usage: FID=12345 FNAME=appname node scripts/setup-farcaster.mjs
* Requires: OPERATOR_PRIVATE_KEY in contracts/.env
*/
import { createPublicClient, createWalletClient, http, encodeAbiParameters } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { optimism } from 'viem/chains'
import { readFileSync, writeFileSync, existsSync } from 'fs'
import { execSync } from 'child_process'
import { tmpdir } from 'os'
import { join } from 'path'
import * as ed from '@noble/ed25519'
import { sha512 } from '@noble/hashes/sha512'
import {
NobleEd25519Signer,
ViemLocalEip712Signer,
makeUserDataAdd,
makeCastAdd,
UserDataType,
Message,
} from '@farcaster/hub-nodejs'
// ЛОВУШКА #2: Без этой строки NobleEd25519Signer не работает
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m))
// --- Config (подставь реальные значения) ---
const envFile = readFileSync('./contracts/.env', 'utf8')
const keyMatch = envFile.match(/OPERATOR_PRIVATE_KEY=(.+)/)
if (!keyMatch) { console.error('OPERATOR_PRIVATE_KEY not found'); process.exit(1) }
const pk = keyMatch[1].trim().startsWith('0x') ? keyMatch[1].trim() : `0x${keyMatch[1].trim()}`
const FID = parseInt(process.env.FID)
if (!FID) { console.error('FID env var required'); process.exit(1) }
const FNAME = process.env.FNAME || 'appname' // ← ЗАМЕНИТЬ
const DOMAIN = 'your-domain.vercel.app' // ← ЗАМЕНИТЬ
const APP_NAME = 'App Name' // ← ЗАМЕНИТЬ
const BIO = 'App description for Farcaster profile.' // ← ЗАМЕНИТЬ
const HUB_URL = 'https://snapchain-api.neynar.com/v1'
const NEYNAR_KEY = 'NEYNAR_API_DOCS'
const SIGNER_FILE = '.farcaster-app-signer.json'
const account = privateKeyToAccount(pk)
console.log(`\nSetup Farcaster — FID: ${FID}, Wallet: ${account.address}\n`)
const publicClient = createPublicClient({ chain: optimism, transport:
http('https://mainnet.optimism.io') })
const walletClient = createWalletClient({ account, chain: optimism, transport:
http('https://mainnet.optimism.io') })
// --- Contracts ---
const STORAGE_REGISTRY = '0x00000000fcCe7f938e7aE6D3c335bD6a1a7c593D'
const KEY_GATEWAY = '0x00000000fC56947c7E7183f8Ca4B62398CaAdf0B'
const SIGNED_KEY_REQUEST_VALIDATOR = '0x00000000FC700472606ED4fA22623Acf62c60553'
const STORAGE_ABI = [
{ type: 'function', name: 'price', stateMutability: 'view',
inputs: [{ name: 'units', type: 'uint256' }],
outputs: [{ name: '', type: 'uint256' }] },
{ type: 'function', name: 'rent', stateMutability: 'payable',
inputs: [{ name: 'fid', type: 'uint256' }, { name: 'units', type: 'uint256' }],
outputs: [{ name: 'overpayment', type: 'uint256' }] }
]
const KEY_GATEWAY_ABI = [
{ type: 'function', name: 'add', stateMutability: 'nonpayable',
inputs: [
{ name: 'keyType', type: 'uint32' },
{ name: 'key', type: 'bytes' },
{ name: 'metadataType', type: 'uint8' },
{ name: 'metadata', type: 'bytes' }
],
outputs: [] }
]
// ========== STEP 1: STORAGE ==========
console.log('--- Step 1: Storage ---')
// ЛОВУШКА #4: rentedUnits — глобальный. Маркер файл + Hub API = двойная защита.
const STORAGE_MARKER = '.storage-rented'
let hasStorage = false
// Check 1: маркер файл (primary — переживает API outages)
if (existsSync(STORAGE_MARKER)) {
const marker = JSON.parse(readFileSync(STORAGE_MARKER, 'utf8'))
if (marker.fid === FID) {
hasStorage = true
console.log(`Storage already rented (marker: tx ${marker.tx?.slice(0, 16)}...)`)
}
}
// Check 2: Hub API (secondary) — ЛОВУШКА #6: curl, не fetch
if (!hasStorage) {
try {
const curlResult = execSync(
`curl -s "${HUB_URL}/storageLimitsByFid?fid=${FID}" -H "x-api-key: ${NEYNAR_KEY}"`,
{ encoding: 'utf8' }
)
const hubData = JSON.parse(curlResult)
if (hubData.limits?.length > 0) {
hasStorage = true
console.log(`Storage confirmed by Hub (${hubData.limits.length} limits)`)
writeFileSync(STORAGE_MARKER, JSON.stringify({ fid: FID, tx: 'confirmed-by-hub' }))
}
} catch {}
}
if (!hasStorage) {
const storagePrice = await publicClient.readContract({
address: STORAGE_REGISTRY, abi: STORAGE_ABI, functionName: 'price', args: [1n]
})
console.log(`Renting 1 unit for ${Number(storagePrice) / 1e18} ETH...`)
const hash = await walletClient.writeContract({
address: STORAGE_REGISTRY, abi: STORAGE_ABI, functionName: 'rent',
args: [BigInt(FID), 1n], value: storagePrice
})
await publicClient.waitForTransactionReceipt({ hash })
// Маркер СРАЗУ после rent — до любых других операций
writeFileSync(STORAGE_MARKER, JSON.stringify({ fid: FID, tx: hash, date: new Date().toISOString()
}))
console.log(`Storage rented. Tx: ${hash}`)
}
// ========== STEP 2: ED25519 KEYPAIR ==========
console.log('\n--- Step 2: Ed25519 Keypair ---')
let signerPrivateKey, signerPublicKey
if (existsSync(SIGNER_FILE)) {
const saved = JSON.parse(readFileSync(SIGNER_FILE, 'utf8'))
signerPrivateKey = saved.privateKey
signerPublicKey = saved.publicKey
console.log(`Loaded existing keypair from ${SIGNER_FILE}`)
} else {
// ЛОВУШКА #3: randomSecretKey(), НЕ randomPrivateKey(). Принимает/возвращает Uint8Array.
const privKeyBytes = ed.utils.randomSecretKey()
signerPrivateKey = Buffer.from(privKeyBytes).toString('hex')
const pubKeyBytes = await ed.getPublicKeyAsync(privKeyBytes)
signerPublicKey = Buffer.from(pubKeyBytes).toString('hex')
writeFileSync(SIGNER_FILE, JSON.stringify({ privateKey: signerPrivateKey, publicKey:
signerPublicKey }, null, 2))
console.log(`Generated new keypair, saved to ${SIGNER_FILE}`)
// ЛОВУШКА #12: секреты в .gitignore
const gitignore = existsSync('.gitignore') ? readFileSync('.gitignore', 'utf8') : ''
if (!gitignore.includes(SIGNER_FILE)) {
writeFileSync('.gitignore', gitignore.trimEnd() + `\n${SIGNER_FILE}\n.storage-rented\n`)
}
}
console.log(` Public key: 0x${signerPublicKey.slice(0, 16)}...`)
// ========== STEP 3: REGISTER SIGNER ON-CHAIN ==========
console.log('\n--- Step 3: Register Signer ---')
const publicKeyBytes = `0x${signerPublicKey}`
const deadline = BigInt(Math.floor(Date.now() / 1000) + 86400)
const eip712Signer = new ViemLocalEip712Signer(account)
const signedKeyRequestSig = await account.signTypedData({
domain: {
name: 'Farcaster SignedKeyRequestValidator',
version: '1',
chainId: 10,
verifyingContract: SIGNED_KEY_REQUEST_VALIDATOR,
},
types: {
SignedKeyRequest: [
{ name: 'requestFid', type: 'uint256' },
{ name: 'key', type: 'bytes' },
{ name: 'deadline', type: 'uint256' },
],
},
primaryType: 'SignedKeyRequest',
message: { requestFid: BigInt(FID), key: publicKeyBytes, deadline },
})
// ЛОВУШКА #5: TUPLE encoding, не flat
const metadata = encodeAbiParameters(
[{ type: 'tuple', components: [
{ name: 'requestFid', type: 'uint256' },
{ name: 'requestSigner', type: 'address' },
{ name: 'signature', type: 'bytes' },
{ name: 'deadline', type: 'uint256' },
]}],
[{ requestFid: BigInt(FID), requestSigner: account.address, signature: signedKeyRequestSig,
deadline }]
)
try {
const hash = await walletClient.writeContract({
address: KEY_GATEWAY, abi: KEY_GATEWAY_ABI, functionName: 'add',
args: [1, publicKeyBytes, 1, metadata]
})
await publicClient.waitForTransactionReceipt({ hash })
console.log(`Signer registered. Tx: ${hash}`)
} catch (err) {
if (err.message?.includes('0xbaf3f0f7') || err.message?.includes('already')) {
console.log('Signer already registered (skipping)')
} else {
throw err
}
}
// ========== STEP 4: WAIT FOR HUB SYNC ==========
console.log('\n--- Step 4: Wait for Hub sync ---')
const start = Date.now()
let synced = false
const pubKeyLower = signerPublicKey.toLowerCase()
while (Date.now() - start < 600_000) {
try {
// ЛОВУШКА #6: curl, не fetch
const curlResult = execSync(
`curl -s "${HUB_URL}/onChainSignersByFid?fid=${FID}" -H "x-api-key: ${NEYNAR_KEY}"`,
{ encoding: 'utf8' }
)
const data = JSON.parse(curlResult)
const signers = data.events || []
// ЛОВУШКА #11: strip 0x, lowercase
if (signers.some(e => {
const hubKey = (e.signerEventBody?.key || '').toLowerCase().replace('0x', '')
return hubKey.includes(pubKeyLower) || pubKeyLower.includes(hubKey)
})) {
synced = true
break
}
} catch {}
process.stdout.write('.')
await new Promise(r => setTimeout(r, 15_000))
}
if (synced) {
console.log('\nSigner synced to Hub')
} else {
// ЛОВУШКА #10: НЕ exit — продолжай
console.log('\nSync timeout (10 min). Continuing anyway — signer may work.')
}
// ========== STEP 5: PROFILE ==========
console.log('\n--- Step 5: Set Profile ---')
const signer = new NobleEd25519Signer(new Uint8Array(Buffer.from(signerPrivateKey, 'hex')))
// ЛОВУШКА #9: submitMessage ТОЛЬКО через curl --data-binary
async function submitToHub(msg) {
const encoded = Buffer.from(Message.encode(msg).finish())
const tmpFile = join(tmpdir(), `fc-msg-${Date.now()}.bin`)
writeFileSync(tmpFile, encoded)
const result = execSync(
`curl -s -X POST "${HUB_URL}/submitMessage" -H "x-api-key: ${NEYNAR_KEY}" -H "Content-Type:
application/octet-stream" --data-binary @${tmpFile}`,
{ encoding: 'utf8' }
)
return JSON.parse(result)
}
const profileData = [
{ type: UserDataType.DISPLAY, value: APP_NAME },
{ type: UserDataType.PFP, value: `https://${DOMAIN}/icon-1024.png` },
{ type: UserDataType.BIO, value: BIO },
{ type: UserDataType.URL, value: `https://${DOMAIN}` },
]
for (const { type, value } of profileData) {
const typeName = Object.entries(UserDataType).find(([, v]) => v === type)?.[0] || type
try {
// ЛОВУШКА #7: fid во ВТОРОМ аргументе (dataOptions), НЕ в body
const msgResult = await makeUserDataAdd(
{ type, value },
{ fid: FID, network: 1 },
signer
)
if (msgResult.isErr()) { console.log(` ${typeName}: ${msgResult.error.message}`); continue }
const hubResult = await submitToHub(msgResult.value)
if (hubResult.hash) {
console.log(` ${typeName}: ${value.slice(0, 50)}`)
} else {
console.log(` ${typeName}: ${JSON.stringify(hubResult).slice(0, 100)}`)
}
} catch (err) {
console.log(` ${typeName}: ${err.message?.slice(0, 80)}`)
}
}
// ========== STEP 6: FNAME + FIRST CAST ==========
console.log('\n--- Step 6: Fname ---')
try {
const timestamp = Math.floor(Date.now() / 1000)
const fnameSignature = await eip712Signer.signUserNameProofClaim({
name: FNAME,
timestamp: BigInt(timestamp),
owner: account.address,
})
if (fnameSignature.isOk()) {
// ЛОВУШКА #8: signUserNameProofClaim → Uint8Array → hex для JSON API
const sigHex = '0x' + Buffer.from(fnameSignature.value).toString('hex')
const fnameRes = await fetch('https://fnames.farcaster.xyz/transfers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: FNAME, from: 0, to: FID, fid: FID,
owner: account.address, timestamp, signature: sigHex,
}),
})
const fnameData = await fnameRes.json()
if (fnameRes.ok || fnameData.code === 'ALREADY_REGISTERED') {
console.log(`Fname: @${FNAME}`)
} else {
console.log(`Fname: ${JSON.stringify(fnameData).slice(0, 100)}`)
}
}
// ЛОВУШКА #7: fid в dataOptions
const usernameMsg = await makeUserDataAdd(
{ type: UserDataType.USERNAME, value: FNAME },
{ fid: FID, network: 1 },
signer
)
if (usernameMsg.isOk()) {
await submitToHub(usernameMsg.value)
console.log(` USERNAME set to ${FNAME}`)
}
} catch (err) {
console.log(`Fname error: ${err.message?.slice(0, 80)}`)
}
// First cast
console.log('\n--- First Cast ---')
try {
const castText = `${APP_NAME} is live!\n\nhttps://${DOMAIN}`
// ЛОВУШКА #7: fid в dataOptions
const castMsg = await makeCastAdd(
{
text: castText,
embeds: [{ url: `https://${DOMAIN}` }],
embedsDeprecated: [],
mentions: [],
mentionsPositions: [],
},
{ fid: FID, network: 1 },
signer
)
if (castMsg.isOk()) {
const castResult = await submitToHub(castMsg.value)
if (castResult.hash) {
console.log(`First cast posted: "${castText.slice(0, 50)}..."`)
} else {
console.log(`Cast: ${JSON.stringify(castResult).slice(0, 100)}`)
}
} else {
console.log(`Cast build error: ${castMsg.error.message}`)
}
} catch (err) {
console.log(`Cast error: ${err.message?.slice(0, 80)}`)
}
console.log('\nFarcaster setup complete!')
FID=YOUR_FID FNAME=appname node scripts/setup-farcaster.mjs
Скрипт идемпотентен — безопасно перезапускать. Выполненные шаги будут пропущены.
Если Hub отвечает ошибками — подожди 5-10 минут и перезапусти.
Стоимость всех операций: ~0.0006 ETH на Optimism
Манифест обновлён (Account Association подписана). Нужен передеплой:
npm run build
vercel --prod # или: unset RAILWAY_TOKEN && railway up
DOMAIN="YOUR_PRODUCTION_DOMAIN"
# 1. HTTP status
echo "=== HTTP Status ==="
curl -s -o /dev/null -w "%{http_code}" https://$DOMAIN
# 2. Manifest
echo -e "\n=== Manifest ==="
curl -s https://$DOMAIN/.well-known/farcaster.json | python3 -c "
import json,sys
d = json.load(sys.stdin)
aa = d.get('accountAssociation', {})
if aa.get('header','').startswith('PENDING') or not aa.get('header'):
print('Account Association: NOT signed')
else:
print('Account Association: signed')
key_used = 'miniapp' if 'miniapp' in d else ('frame' if 'frame' in d else 'NONE')
print(f'Manifest key: {key_used}')
mini = d.get('miniapp', d.get('frame', {}))
for f in ['name','homeUrl','iconUrl','tagline']:
v = mini.get(f)
print(f' {f}: {v or \"MISSING\"}')"
# 3. Assets
echo -e "\n=== Assets ==="
curl -s https://$DOMAIN/.well-known/farcaster.json | python3 -c "
import json,sys,subprocess
d = json.load(sys.stdin)
mini = d.get('miniapp', d.get('frame', {}))
for key in ['iconUrl','splashImageUrl','heroImageUrl','ogImageUrl']:
url = mini.get(key)
if url:
code = subprocess.run(['curl','-s','-o','/dev/null','-w','%{http_code}',url],capture_output=
True,text=True).stdout
print(f'{key}: {code}')"
# 4. Meta tags
echo -e "\n=== Meta Tags ==="
curl -s https://$DOMAIN | python3 -c "
import sys; html = sys.stdin.read()
for tag in ['fc:miniapp','fc:frame','og:title','og:image']:
print(f'{tag}: {\"found\" if tag in html else \"MISSING\"}')"
Всё зелёное → ВЫВОД.
Account Association не подписана → вернись к шагу 4.
Assets 404 → нормально если картинки ещё не созданы. Скажи юзеру какие файлы нужны.
Meta tags missing → поправь сам.
ВЫВОД — СОХРАНИ В PROJECT_BRIEF.md
ОБЯЗАТЕЛЬНО: Допиши в PROJECT_BRIEF.md:
## 17. ДЕПЛОЙ + FARCASTER SETUP (Prompt 6)
Дата: [сегодня]
URL: https://...
Platform: [Vercel/Railway]
FID: [номер]
Wallet: [адрес]
Account Association: signed
Farcaster Profile: Display/PFP/BIO/URL set
First Cast: posted
Signer: 0x... (Ed25519)
Fname: [результат]
Smoke Test:
- HTTP 200: Y/N
- Manifest (miniapp key): Y/N
- Account Association: Y/N
- Assets: Y/N (список недостающих)
- Meta tags (fc:miniapp + fc:frame): Y/N
Scripts:
- scripts/register-fid.mjs
- scripts/sign-manifest.mjs
- scripts/setup-farcaster.mjs
Напиши "Production деплой и Farcaster setup завершены. Готов к Prompt 7."
PROMPT 7 — РЕГИСТРАЦИЯ И LAUNCH
> Финальный шаг. Часть автоматизирована (чеклист, meta tags), часть требует действий юзера (base.dev UI, Warpcast). Агент чётко говорит что нужно сделать.
ЗАДАЧА: Зарегистрировать приложение и запустить
Прочитай PROJECT_BRIEF.md — секция 17 (деплой).
Приложение задеплоено и работает. Часть шагов — ручные (base.dev UI).
Агент не может кликать в браузере → даёт ТОЧНЫЕ инструкции и ждёт.
---
ШАГ 0: ПРОВЕРКИ ПЕРЕД ЗАПУСКОМ
Перед регистрацией на base.dev — убедись что всё настроено:
0.1: CORS на .well-known (КРИТИЧНО для base.dev)
base.dev делает server-side fetch /.well-known/farcaster.json — без CORS получишь 502.
Проверь vercel.json — ДОЛЖНО быть отдельное правило для .well-known:
{
"headers": [
{
"source": "/.well-known/(.*)",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "*" },
{ "key": "Access-Control-Allow-Methods", "value": "GET, OPTIONS" },
{ "key": "Access-Control-Allow-Headers", "value": "Content-Type" },
{ "key": "Cache-Control", "value": "public, max-age=300" },
{ "key": "X-Content-Type-Options", "value": "nosniff" }
]
},
{
"source": "/(.*)",
"headers": [ ... ]
}
]
}
Правило для .well-known ОБЯЗАТЕЛЬНО идёт ПЕРЕД /(.*)!
0.2: CSP frame-ancestors — добавить base.dev
В CSP заголовке frame-ancestors ОБЯЗАТЕЛЬНО должны быть:
frame-ancestors 'self' https://*.base.org https://*.base.dev https://base.dev https://warpcast.com
https://*.coinbase.com https://*.farcaster.xyz
0.3: Убрать X-Frame-Options (deprecated)
X-Frame-Options: ALLOW-FROM — DEPRECATED, не работает в Chrome/Firefox.
Удали этот заголовок полностью. Вместо него работает frame-ancestors в CSP.
Если оставить — может конфликтовать с frame-ancestors.
0.4: sdk.actions.ready() — ПОСЛЕ монтирования
SDK ready() ОБЯЗАТЕЛЬНО вызывается в useEffect ПОСЛЕ монтирования React, НЕ на верхнем уровне
модуля:
// ПРАВИЛЬНО:
import { sdk } from '@farcaster/miniapp-sdk'
function App() {
useEffect(() => {
sdk.actions.ready()
}, [])
// ...
}
// НЕПРАВИЛЬНО (может не сработать в iframe):
import('@farcaster/miniapp-sdk').then(({ sdk }) => {
sdk.actions.ready() // ← вызывается ДО React mount
})
base.dev preview показывает "Ready call: Not Ready" → sdk.actions.ready() не дошёл до хоста.
base.dev preview показывает "Ready call: Ready" → всё ок.
Если что-то из 0.1-0.4 пришлось менять:
npm run build && vercel --prod
Проверь CORS:
curl -sI "https://YOUR_DOMAIN/.well-known/farcaster.json" | grep -i "access-control-allow-origin"
# Должно быть: access-control-allow-origin: *
---
ШАГ 1: РУЧНЫЕ ДЕЙСТВИЯ — СРАЗУ ПОСЛЕ ПРОВЕРОК
КРИТИЧНО: Покажи юзеру ВСЁ что нужно сделать руками
СРАЗУ, ДО автоматических проверок чеклиста.
Пока юзер работает в браузере — ты параллельно гоняешь чеклист (шаг 2).
═══════════════════════════════════════════════════════════
НУЖНО ДЕЙСТВИЕ: base.dev регистрация (2 мин)
1. Открой https://base.dev
2. Подключи кошелёк: [ownerAddress из manifest baseBuilder]
3. "Create App" → заполни:
- URL: [production URL из PROJECT_BRIEF.md]
- Name: [name из manifest]
- Category: [primaryCategory из manifest]
4. Скинь мне App ID — он в URL:
base.dev/apps/XXXXXXXXXXXXX
^^^^^^^^^^^^^ вот это
Если интерфейс непонятный — скинь скриншот,
разберёмся вместе.
═══════════════════════════════════════════════════════════
ЖДИ ОТВЕТА. Но пока ждёшь — параллельно запускай автоматические проверки (шаг 2).
ШАГ 2: АВТОМАТИЧЕСКИЕ ПРОВЕРКИ (параллельно с шагом 1)
Прогони полный чеклист. Всё что можно проверить — проверь:
DOMAIN="YOUR_PRODUCTION_DOMAIN" # из PROJECT_BRIEF.md
echo "=== LAUNCH CHECKLIST ==="
# Manifest
echo -e "\n--- Manifest ---"
curl -s https://$DOMAIN/.well-known/farcaster.json | python3 -c "
import json,sys
d = json.load(sys.stdin)
aa = d.get('accountAssociation',{})
mini = d.get('miniapp',{})
checks = [
('miniapp key', 'miniapp' in d),
('Account Association signed', aa.get('header','').startswith('ey')),
('name filled', bool(mini.get('name'))),
('tagline filled', bool(mini.get('tagline'))),
('description filled', bool(mini.get('description'))),
('homeUrl filled', bool(mini.get('homeUrl'))),
('iconUrl filled', bool(mini.get('iconUrl'))),
('splashImageUrl filled', bool(mini.get('splashImageUrl'))),
('heroImageUrl filled', bool(mini.get('heroImageUrl'))),
('primaryCategory filled', bool(mini.get('primaryCategory'))),
('baseBuilder.ownerAddress', bool(d.get('baseBuilder',{}).get('ownerAddress'))),
]
for name, ok in checks:
print(f'{\"✅\" if ok else \"❌\"} {name}')
"
# CORS check
echo -e "\n--- CORS (.well-known) ---"
CORS=$(curl -sI https://$DOMAIN/.well-known/farcaster.json | grep -i "access-control-allow-origin" |
tr -d '\r')
[ -n "$CORS" ] && echo "✅ $CORS" || echo "❌ No CORS headers — base.dev will get 502!"
# Meta tags
echo -e "\n--- Meta Tags ---"
curl -s https://$DOMAIN | python3 -c "
import sys; html = sys.stdin.read()
for tag in ['fc:miniapp','fc:frame','og:title','og:image','theme-color','viewport-fit=cover']:
print(f'{\"✅\" if tag in html else \"❌\"} {tag}')
if 'base:app_id' in html:
print('✅ base:app_id')
else:
print('⏳ base:app_id (after base.dev registration)')
"
# SDK
echo -e "\n--- SDK ---"
grep -rn "sdk.actions.ready\|sdk\.actions\.ready" src/ --include="*.js" --include="*.jsx"
--include="*.ts" --include="*.tsx" 2>/dev/null && echo " ✅ sdk.actions.ready()" || echo " ❌
sdk.actions.ready() NOT FOUND"
# Check it's in useEffect, not top-level
grep -B3 "sdk.actions.ready" src/**/*.{jsx,tsx,js,ts} 2>/dev/null | grep -q
"useEffect\|onMount\|mounted" && echo " ✅ Called after mount" || echo " ⚠️ Check:
sdk.actions.ready() should be in useEffect, not top-level"
# Assets
echo -e "\n--- Assets ---"
curl -s https://$DOMAIN/.well-known/farcaster.json | python3 -c "
import json,sys,subprocess
mini = json.load(sys.stdin).get('miniapp',{})
for key in ['iconUrl','splashImageUrl','heroImageUrl','ogImageUrl']:
url = mini.get(key)
if url:
code = subprocess.run(['curl','-s','-o','/dev/null','-w','%{http_code}',url],capture_output=
True,text=True).stdout
print(f'{\"✅\" if code==\"200\" else \"❌\"} {key}: {code}')
else:
print(f'⏳ {key}: not set')
"
# Farcaster profile + cast (from Prompt 6)
echo -e "\n--- Farcaster Profile ---"
FID_VAR="FID_FROM_PROJECT_BRIEF"
curl -s "https://api.neynar.com/v2/farcaster/user/bulk?fids=$FID_VAR" \
-H "x-api-key: NEYNAR_API_DOCS" | python3 -c "
import json,sys
d = json.load(sys.stdin)
users = d.get('users', [])
if users:
u = users[0]
print(f'✅ FID: {u.get(\"fid\")} Display: {u.get(\"display_name\")} @{u.get(\"username\")}')
else:
print('❌ User not indexed')
"
echo -e "\n--- Casts ---"
curl -s "https://api.neynar.com/v2/farcaster/feed/user/casts?fid=$FID_VAR&limit=3" \
-H "x-api-key: NEYNAR_API_DOCS" | python3 -c "
import json,sys
d = json.load(sys.stdin)
casts = d.get('casts', [])
if casts:
print(f'✅ {len(casts)} casts found. Latest: {casts[0].get(\"text\",\"\")[:60]}')
else:
print('❌ No casts — indexation not triggered')
"
# Security headers
echo -e "\n--- Security Headers ---"
curl -sI https://$DOMAIN | python3 -c "
import sys
headers = sys.stdin.read().lower()
# X-Frame-Options should NOT be present (deprecated)
if 'x-frame-options' in headers:
print('⚠️ X-Frame-Options present (deprecated — remove it, use CSP frame-ancestors)')
else:
print('✅ No X-Frame-Options (correct — using CSP frame-ancestors)')
# frame-ancestors should include base.dev
if 'base.dev' in headers:
print('✅ CSP frame-ancestors includes base.dev')
else:
print('❌ CSP frame-ancestors missing base.dev')
"
Если есть ❌ — поправь автоматически. Если ⏳ — ожидаемо, пометь.
ВАЖНО: Если manifest возвращает HTML вместо JSON — передеплой.
Vercel исключает .well-known/ из rewrites, но только после деплоя с файлом в dist/.
ВАЖНО: Если base.dev preview показывает "Failed to fetch manifest: 502" —
проверь CORS заголовки (шаг 0.1). Без Access-Control-Allow-Origin: * на
.well-known/farcaster.json base.dev не может сделать server-side fetch.
Когда юзер скинет App ID — добавь meta тег в <head> index.html:
<meta name="base:app_id" content="APP_ID_FROM_USER" />
Перебилди + передеплой:
npm run build && vercel --prod
Проверь на base.dev/preview:
- "Ready call: Ready" (зелёный) — sdk.actions.ready() работает
- Манифест загружен без ошибок
- Аппка рендерится в превью
Индексация запускается когда URL шарится в Farcaster ленте.
Если Prompt 6 выполнен — первый каст уже опубликован (проверено в шаге 2).
Если каста НЕТ — перезапусти:
FID=YOUR_FID FNAME=appname node scripts/setup-farcaster.mjs
Скрипт идемпотентен — пропускает выполненные шаги.
ВНИМАНИЕ: Fname cooldown. Fnames API разрешает 1 transfer в 28 дней на FID.
Если FID использовался для другого проекта (другой fname) — переименовать нельзя
~28 дней. Живи с текущим fname или используй другой FID.
Если скрипт не работает (Hub не синкает >30 минут) — fallback:
═══════════════════════════════════════════════════════════
НУЖНО ДЕЙСТВИЕ: Первый пост (1 минута)
1. Открой Warpcast
2. Создай пост: "[App Name] is live! [URL]"
3. Индексация через ~10 минут
Скажи "запостил" — я проверю.
═══════════════════════════════════════════════════════════
ШАГ 5: ПОДАЧА НА FEATURED (опционально)
═══════════════════════════════════════════════════════════
ОПЦИОНАЛЬНО: Подать на Featured Placement
Форма:
https://docs.google.com/forms/d/e/1FAIpQLSeZiB3fmMS7oxBKrWsoaew2LFxGpktnAtPAmJaNZv5TOCXIZg/viewform
Что заполнить:
- URL: [production URL]
- Описание: [из manifest]
- Категория: [primaryCategory]
Скажи "подал" или "пропускаю".
═══════════════════════════════════════════════════════════
### Контракты
- [ ] Deployed to Base Mainnet
- [ ] Verified on BaseScan
- [ ] Test transaction successful
### Frontend
- [ ] Touch targets >= 44px
- [ ] Portrait optimized
- [ ] Loads < 3 seconds
### Transactions
- [ ] Smart Wallet support (sendCalls)
- [ ] Builder Code suffix (ERC-8021)
### Manifest + SDK
- [ ] /.well-known/farcaster.json (miniapp key)
- [ ] Account Association signed
- [ ] sdk.actions.ready() in useEffect (NOT top-level)
- [ ] fc:miniapp + fc:frame meta tags
- [ ] base:app_id meta tag
### CORS + Security Headers
- [ ] .well-known has Access-Control-Allow-Origin: *
- [ ] CSP frame-ancestors includes base.dev
- [ ] No X-Frame-Options header (deprecated)
### Registration
- [ ] base.dev app created + URL verified
- [ ] base.dev preview: Ready (green), manifest loads
- [ ] URL posted in Farcaster feed
- [ ] Featured form submitted (optional)
Пройдись по чеклисту и пометь каждый пункт ✅/❌/⏳.
ВЫВОД — СОХРАНИ В PROJECT_BRIEF.md
ОБЯЗАТЕЛЬНО: Допиши в PROJECT_BRIEF.md:
## 18. РЕГИСТРАЦИЯ И LAUNCH (Prompt 7)
Дата: [сегодня]
- base.dev: зарегистрирован ✅/❌
- base:app_id: [значение или "пропущено"]
- base.dev preview: Ready ✅/❌, Load time: [X]s
- CORS on .well-known: ✅/❌
- Индексация: ✅/❌ (каст с URL в ленте)
- Featured: подано/пропущено
Launch Checklist: [X из Y пунктов ✅]
Оставшиеся блокеры: [список или "нет"]
Напиши "Приложение запущено как Base Mini App. PROJECT_BRIEF.md содержит полную историю запуска."
PROMPT 8 — POST-LAUNCH VERIFICATION
> Финальная проверка живого деплоя. Прогоняет ВСЕ требования из промптов 0-7 за 30 секунд. > Используй после launch или при возвращении к проекту через время.
ЗАДАЧА: Полная верификация запущенного Base Mini App
Прочитай PROJECT_BRIEF.md — секция 17 (деплой), возьми production URL.
Приложение уже запущено. Этот промпт НЕ меняет код — только проверяет что ВСЁ работает.
Результат: таблица ✅/❌ по каждому требованию Base Mini App.
### ШАГ 1: АВТОМАТИЧЕСКИЙ SMOKE TEST
Подставь реальный домен из PROJECT_BRIEF.md:
```bash
DOMAIN="YOUR_PRODUCTION_DOMAIN"
echo "╔══════════════════════════════════════════╗"
echo "║ POST-LAUNCH VERIFICATION ║"
echo "║ $DOMAIN"
echo "╚══════════════════════════════════════════╝"
# ── HTTP ──
echo -e "\n── HTTP ──"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://$DOMAIN)
[ "$HTTP_CODE" = "200" ] && echo "✅ HTTP $HTTP_CODE" || echo "❌ HTTP $HTTP_CODE"
# ── MANIFEST ──
echo -e "\n── Manifest ──"
curl -s https://$DOMAIN/.well-known/farcaster.json | python3 -c "
import json,sys
try:
d = json.load(sys.stdin)
except: print('❌ Invalid JSON'); sys.exit(1)
aa = d.get('accountAssociation',{})
mini = d.get('miniapp', {})
checks = [
('Manifest key = miniapp', 'miniapp' in d and 'frame' not in d),
('Account Association signed', aa.get('header','').startswith('ey')),
('name', bool(mini.get('name'))),
('subtitle', bool(mini.get('subtitle'))),
('tagline', bool(mini.get('tagline'))),
('description', bool(mini.get('description'))),
('homeUrl', bool(mini.get('homeUrl'))),
('iconUrl', bool(mini.get('iconUrl'))),
('splashImageUrl', bool(mini.get('splashImageUrl'))),
('heroImageUrl', bool(mini.get('heroImageUrl'))),
('ogImageUrl', bool(mini.get('ogImageUrl'))),
('primaryCategory', bool(mini.get('primaryCategory'))),
('tags (1-5)', 1 <= len(mini.get('tags',[])) <= 5),
('screenshotUrls', len(mini.get('screenshotUrls',[])) > 0),
('baseBuilder.ownerAddress', bool(d.get('baseBuilder',{}).get('ownerAddress'))),
]
for name, ok in checks:
print(f' {chr(9989) if ok else chr(10060)} {name}')
# Limits
n = mini.get('name',''); st = mini.get('subtitle','')
tl = mini.get('tagline',''); desc = mini.get('description','')
print(f'\n Limits:')
for label, val, mx in [('name',n,32),('subtitle',st,30),('tagline',tl,30),('description',desc,170)]:
ok = len(val) <= mx and len(val) > 0
print(f' {chr(9989) if ok else chr(10060)} {label}: {len(val)}/{mx} chars')
"
# ── ASSETS ──
echo -e "\n── Assets ──"
curl -s https://$DOMAIN/.well-known/farcaster.json | python3 -c "
import json,sys,subprocess
mini = json.load(sys.stdin).get('miniapp',{})
for key in ['iconUrl','splashImageUrl','heroImageUrl','ogImageUrl']:
url = mini.get(key)
if url:
code = subprocess.run(['curl','-s','-o','/dev/null','-w','%{http_code}',url],capture_output=True,text=True).stdout
print(f' {chr(9989) if code==\"200\" else chr(10060)} {key}: HTTP {code}')
else:
print(f' {chr(10060)} {key}: NOT SET')
"
# ── META TAGS ──
echo -e "\n── Meta Tags ──"
curl -s https://$DOMAIN | python3 -c "
import sys; html = sys.stdin.read()
tags = [
('fc:miniapp', 'fc:miniapp' in html),
('fc:frame', 'fc:frame' in html),
('og:title', 'og:title' in html),
('og:image', 'og:image' in html),
('og:description', 'og:description' in html),
('theme-color', 'theme-color' in html),
('viewport-fit=cover', 'viewport-fit=cover' in html),
('base:app_id', 'base:app_id' in html),
('apple-mobile-web-app-capable', 'apple-mobile-web-app-capable' in html),
]
for tag, ok in tags.items() if isinstance(tags, dict) else tags:
print(f' {chr(9989) if ok else chr(10060)} {tag}')
"
# ── HEALTH (если есть бэкенд) ──
echo -e "\n── Backend Health ──"
HEALTH=$(curl -s https://$DOMAIN/health 2>/dev/null)
if echo "$HEALTH" | python3 -c "import json,sys; d=json.load(sys.stdin); print('✅ Backend alive, uptime:', d.get('uptime','?'), 'sec')" 2>/dev/null; then
:
else
echo "⏭️ No /health endpoint (frontend-only deploy)"
fi
```
```bash
echo "── Code Checks ──"
# SDK
echo ""
grep -rn "sdk.actions.ready\|sdk\.actions\.ready" src/ --include="*.js" --include="*.jsx" --include="*.ts" --include="*.tsx" 2>/dev/null && echo " ✅ sdk.actions.ready()" || echo " ❌ sdk.actions.ready() NOT FOUND"
# Builder Code (ERC-8021)
echo ""
BUILDER=$(grep -rn "BUILDER_ADDRESS\|dataSuffix\|ERC-8021" src/ --include="*.js" --include="*.jsx" --include="*.ts" --include="*.tsx" 2>/dev/null | head -1)
[ -n "$BUILDER" ] && echo " ✅ Builder Code (ERC-8021)" || echo " ❌ Builder Code NOT FOUND"
# Smart Transaction wrapper
SMART=$(grep -rn "useSmartTransaction\|executeWithBuilder" src/ --include="*.js" --include="*.jsx" --include="*.ts" --include="*.tsx" 2>/dev/null | head -1)
[ -n "$SMART" ] && echo " ✅ Smart Transaction wrapper" || echo " ⚠️ No Smart Transaction wrapper (direct writeContract)"
# Direct writeContract outside wrapper (should be 0)
DIRECT=$(grep -rn "writeContractAsync\|useWriteContract\|useContractWrite" src/ --include="*.js" --include="*.jsx" --include="*.ts" --include="*.tsx" --exclude="*useSmartTransaction*" --exclude="*useSmart*" 2>/dev/null | grep -v "import" | wc -l | tr -d ' ')
[ "$DIRECT" = "0" ] && echo " ✅ No direct writeContract calls" || echo " ⚠️ $DIRECT direct writeContract calls found (should go through wrapper)"
# Theme
THEME=$(grep -rn "useTheme\|ThemeProvider\|prefers-color-scheme" src/ --include="*.js" --include="*.jsx" --include="*.ts" --include="*.tsx" 2>/dev/null | head -1)
[ -n "$THEME" ] && echo " ✅ Theme support (dark/light)" || echo " ❌ No theme support"
# Address display
ADDR=$(grep -rn "shortenAddress\|useBasename\|useEnsName\|formatAddress" src/ --include="*.js" --include="*.jsx" --include="*.ts" --include="*.tsx" 2>/dev/null | head -1)
[ -n "$ADDR" ] && echo " ✅ Address shortening/Basename" || echo " ⚠️ No address shortening found"
# CSP frame-ancestors (backend only)
CSP=$(grep -rn "frame-ancestors" server/ --include="*.ts" --include="*.js" 2>/dev/null | head -1)
[ -n "$CSP" ] && echo " ✅ CSP frame-ancestors" || echo " ⏭️ No CSP (frontend-only or check manually)"
# overscroll-behavior
OVERSCROLL=$(grep -rn "overscroll-behavior" src/ index.html --include="*.css" --include="*.html" --include="*.js" --include="*.jsx" 2>/dev/null | head -1)
[ -n "$OVERSCROLL" ] && echo " ✅ overscroll-behavior: none" || echo " ⚠️ No overscroll-behavior"
# Security: secrets in gitignore
echo -e "\n── Security ──"
for f in ".env" ".farcaster-signer.json" ".farcaster-app-signer.json"; do
if [ -f "$f" ]; then
if grep -q "$(echo $f | sed 's/\./\\\\./g')" .gitignore 2>/dev/null; then
echo " ✅ $f in .gitignore"
else
echo " ❌ $f EXISTS but NOT in .gitignore!"
fi
fi
done
# Build
echo -e "\n── Build ──"
npm run build 2>&1 | tail -1
```
Посчитай результаты и выведи саммари:
```
═══════════════════════════════════════════════════
POST-LAUNCH VERIFICATION COMPLETE
✅ Passed: X
❌ Failed: Y
⚠️ Warnings: Z
[Если Y > 0: список что починить]
[Если Y = 0: "All checks passed. App is fully compliant."]
═══════════════════════════════════════════════════
```
### ВЫВОД — СОХРАНИ В PROJECT_BRIEF.md (опционально)
Если хочешь зафиксировать результат:
```markdown
## 19. POST-LAUNCH VERIFICATION (Prompt 8)
Дата: [сегодня]
| Ресурс | URL |
|--------|-----|
| Base Dev | https://base.dev |
| Mini Apps Docs | https://docs.base.org/mini-apps/quickstart/migrate-existing-apps |
| Product Guidelines | https://docs.base.org/mini-apps/featured-guidelines/product-guidelines |
| Design Guidelines | https://docs.base.org/mini-apps/featured-guidelines/design-guidelines |
| Technical Guidelines | https://docs.base.org/mini-apps/featured-guidelines/technical-guidelines |
| Builder Codes | https://docs.base.org/base-chain/builder-codes/builder-codes |
| BaseScan | https://basescan.org |
| BaseScan API Key | https://basescan.org/myapikey |
| Basename | https://www.base.org/names |
| Featured Form | https://docs.google.com/forms/d/e/1FAIpQLSeZiB3fmMS7oxBKrWsoaew2LFxGpktnAtPAmJaNZv5TOCXIZg/viewform |
| Grant Form | https://docs.google.com/forms/d/e/1FAIpQLSfXuEzmiAzRhie_z9raFCF1BXweXgVt18o-DvBuRRgyTygL2A/viewform |
| MiniApp SDK | https://www.npmjs.com/package/@farcaster/miniapp-sdk |
| Talent Protocol | https://talent.app/~/earn |
| MiniKit Starter | https://github.com/builders-garden/base-minikit-starter |
О ТАК ОТ! ВСЕМ СПАСИБО! ВОТ МОЙ ПАБЛИК: t.me/psclama, ВОТ МОЯ ЛИЧКА t.me/lamadrops