🎟 招埅コヌド共有掲瀺板を「珟金報酬なし」で蚭蚈した理由 — リヌダヌ×
🎟

招埅コヌド共有掲瀺板を「珟金報酬なし」で蚭蚈した理由

#nextjs#個人開発#automation2026-06-26 · 箄13分

Claude Codeの蚘憶を4局に分けた話から続く「個人開発を量産するための自動化基盀」シリヌズです。今回は、招埅コヌド共有掲瀺板 InviteLoop本番皌働䞭inviteloop.vercel.appを蚭蚈したずきに最初に決めた方針、「報酬は公匏特兞のみ。珟金のやりずりは䞀切排陀する」 ずいう遞択の背景ず、その刀断をコヌドに萜ずし蟌むたでの話を曞きたす。

InviteLoop ずは

InviteLoop は、各皮サヌビスの招埅コヌドをナヌザヌが投皿・共有できる掲瀺板です。Dropbox や Uber のような「玹介するず○○がもらえる」公匏プログラムを持぀サヌビスを察象に、招埅コヌドを䞀か所に集めお探しやすくするこずを目的にしおいたす。

技術スタックは次のずおりです。

レむダヌ採甚技術
フレヌムワヌクNext.js 16.2.9App Router
デヌタベヌスTursolibsql / SQLite 互換
ORMDrizzle ORM
認蚌NextAuth.js v5Google OAuth
スタむリングTailwind CSS v4
テストVitest + Playwright
デプロむVercel

䞀芋シンプルに芋えたすが、蚭蚈でもっずも時間を䜿ったのはUI や機胜よりも「報酬モデルをどこに匕くか」ずいう䞀点でした。

なぜ「珟金報酬なし」にしたのか

招埅コヌド掲瀺板を䜜ろうずするず、自然に浮かぶアむデアが「コヌドが䜿われたらポむントを付䞎しお換金できるようにしよう」ずいうものです。実際、海倖には同様のモデルを持぀サヌビスが存圚したす。それでも私が珟金報酬を排陀した理由は、倧きく䞉぀です。

理由1各サヌビスの利甚芏玄リスク

招埅プログラムを提䟛しおいるサヌビスは、たいおい芏玄の䞭に「招埅特兞の転売・換金を犁止する」「第䞉者プラットフォヌムを介した特兞の再配垃を犁ずる」ずいった条項を持っおいたす。InviteLoop が「コヌドを䜿ったら○円」ずいう仕組みを持぀ず、それを媒介しおナヌザヌが各サヌビスの芏玄を間接的に砎る可胜性が生たれたす。

プラットフォヌム偎には、ナヌザヌの芏玄違反を助長した責任を問われるリスクがありたす。「知らなかった」では枈たないケヌスになりうるため、蚭蚈段階で珟金の流れを完党に切るこずにしたした。

理由2スパム・氎増し投皿の誘発

珟金報酬があるず、「䜿われた回数に応じお収益が出る」ずいう構造になりたす。するず、同䞀ナヌザヌが耇数アカりントを䜿っお自分のコヌドを䜿甚報告する、あるいはボットで倧量投皿するむンセンティブが生たれたす。

スパム察策は埌付けで远加するより、むンセンティブ蚭蚈の段階で封じる方がはるかに楜です。金銭的な動機を消すこずで、䞍正行為のリタヌンをれロにしたした。

理由3コミュニティの健党性

招埅コヌド掲瀺板に期埅される䜓隓は「䜿えるコヌドを玠早く芋぀けられるこず」です。珟金が絡むず、投皿者が「できるだけ倚く䜿われるコヌド」を優先しお品質より数を远いかけるようになりたす。ここでいう品質ずは「実際に有効なコヌドであるこず」「ノヌトが䞁寧で䜿い方が分かるこず」を指したす。報酬は公匏特兞だけにするこずで、「本圓に䜿えるコヌドを共有したい」ずいうモチベヌション以倖が入りにくい蚭蚈にしたした。

スキヌマで「報酬」を最初から存圚させない

蚭蚈刀断をコヌドに萜ずし蟌む最初の䞀手は、DB スキヌマに報酬の抂念を䞀切曞かないこずでした。

// src/db/schema.ts抜粋
export const services = sqliteTable('service', {
  id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
  name: text('name').notNull(),
  slug: text('slug').notNull().unique(),
  category: text('category').notNull(),
  perkSummary: text('perk_summary').notNull().default(''),
  perkJa: text('perk_ja').notNull().default(''),
  perkEn: text('perk_en').notNull().default(''),
  officialUrl: text('official_url').notNull().default(''),
  createdAt: integer('created_at', { mode: 'timestamp_ms' })
    .notNull().$defaultFn(() => new Date()),
})

perkJa / perkEn は「このサヌビスの公匏特兞はこれです」ずいう説明フィヌルドです。金額を入れるこずはありたすが、それはあくたで公匏が公衚しおいる情報をそのたた衚瀺するためのもの。InviteLoop 独自のポむント残高や換金テヌブルはスキヌマのどこにも存圚したせん。

招埅コヌド本䜓のテヌブルを芋おも、報酬に関わるカラムはれロです。

export const inviteCodes = sqliteTable('invite_code', {
  id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
  serviceId: text('service_id').notNull().references(() => services.id, { onDelete: 'cascade' }),
  userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  code: text('code').notNull(),
  url: text('url').notNull().default(''),
  note: text('note').notNull().default(''),
  status: text('status', { enum: ['active', 'hidden', 'removed'] }).notNull().default('active'),
  clickCount: integer('click_count').notNull().default(0),
  upvotes: integer('upvotes').notNull().default(0),
  usageCount: integer('usage_count').notNull().default(0),
  reportCount: integer('report_count').notNull().default(0),
  createdAt: integer('created_at', { mode: 'timestamp_ms' })
    .notNull().$defaultFn(() => new Date()),
})

clickCount・upvotes・usageCount・reportCount はどれも「掲瀺板ずしおの信頌床・人気床を瀺す指暙」であり、換金の根拠にはなりたせん。スキヌマが真実の単䞀゜ヌスである以䞊、存圚しないカラムからは䜕も払えたせん。これが蚭蚈䞊の最匷の担保です。

投皿制限ず URL ドメむン怜蚌

「報酬がなければ乱甚されないか」ず問われるず、正盎れロではありたせん。自分のコヌドを䞊䜍に衚瀺させたい、あるいは単なるいたずらで倧量投皿するケヌスは考えられたす。それを防ぐのが投皿レヌト制限ずURL怜蚌です。

// src/lib/rate-limit.ts
export const MAX_POSTS_PER_DAY = 5

export function checkRateLimit(i: RateLimitInput): RateLimitResult {
  if (i.alreadyPostedThisService) return { ok: false, reason: 'duplicate_service' }
  if (i.postsToday >= i.maxPerDay) return { ok: false, reason: 'daily_limit' }
  return { ok: true }
}

1ナヌザヌが同じサヌビスに耇数のコヌドを投皿するこずは犁止されおいたすduplicate_service。たた、1日の投皿䞊限は5件MAX_POSTS_PER_DAY = 5です。

投皿フォヌムには「コヌドずセットで招埅URLを貌れる」フィヌルドがありたすが、ここに任意のURLを入れられるず別サヌビスぞの誘導に䜿われたす。そこで、URL は圓該サヌビスの公匏ドメむンのみ蚱可する仕組みにしたした。

// src/lib/allowed-hosts.ts
export function allowedHostsFor(officialUrl: string): string[] {
  if (!officialUrl) return []
  try {
    const h = new URL(officialUrl).host
    return h.startsWith('www.') ? [h, h.slice(4)] : [h, `www.${h}`]
  } catch { return [] }
}

officialUrl から www あり/なし の䞡方を蚱可ホストずしお生成し、validateSubmission の䞭で URL を怜蚌したす。

// src/lib/validation.ts抜粋
if (parsed.protocol !== 'https:' || !allowedHosts.includes(parsed.host)) {
  return { ok: false, error: 'url_not_allowed' }
}

https: 限定か぀公匏ドメむン限定。これだけで倖郚リンクを䜿ったフィッシング的な䜿い方をほが封じられたす。

自動モデレヌション3 件レポヌトでオヌトハむド

ナヌザヌからの通報を受けお、䞀定数に達したコヌドは自動的に非衚瀺にしたす。

// src/lib/scoring.ts
export const REPORT_AUTOHIDE_THRESHOLD = 3
// src/repos/engagement.tsreportCode 関数の末尟
const reportCount = rows[0].reportCount
const hidden = reportCount >= REPORT_AUTOHIDE_THRESHOLD
if (hidden) {
  await db.update(inviteCodes)
    .set({ status: 'hidden' })
    .where(eq(inviteCodes.id, codeId))
}
return { ok: true as const, reportCount, hidden }

3件の通報で status が 'hidden' になりたす。䞀芧画面ではこのステヌタスを持぀コヌドを陀倖するため、問題のある投皿は人手を介さずに非衚瀺になりたす。闇雲にハヌドルを䞊げるず正垞なコヌドたで巻き蟌たれるので、閟倀は珟時点では控えめな3件です。

同䞀ナヌザヌが同じコヌドを耇数回通報できないよう、report テヌブルに (code_id, reporter_user_id) の耇合ナニヌク制玄を蚭けおいたす。

// src/db/schema.ts抜粋
export const reports = sqliteTable('report', {
  id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
  codeId: text('code_id').notNull().references(() => inviteCodes.id, { onDelete: 'cascade' }),
  reporterUserId: text('reporter_user_id'),
  reason: text('reason').notNull().default(''),
  createdAt: integer('created_at', { mode: 'timestamp_ms' })
    .notNull().$defaultFn(() => new Date()),
}, (t) => ({ uniq: uniqueIndex('report_code_user_uq').on(t.codeId, t.reporterUserId) }))

フェアな䞊び順スコア×加重シャッフル

投皿数が増えおくるず「どの順番でコヌドを䞊べるか」が UX の芁になりたす。玔粋にスコア順人気順で䞊べるず、叀くから䞊䜍にいるコヌドが有利になりすぎたす。逆に新着順だず、すぐに埋もれお投祚を受けるチャンスがなくなりたす。

この問題を解決するために「加重確率シャッフル」を採甚したした。

// src/lib/scoring.ts
export const SCORE_CLICK_WEIGHT = 1
export const SCORE_USAGE_WEIGHT = 5
export const SCORE_UPVOTE_WEIGHT = 3

export function computeScore(input: {
  clickCount: number
  usageCount: number
  upvoteCount: number
}): number {
  return (
    input.clickCount * SCORE_CLICK_WEIGHT +
    input.usageCount * SCORE_USAGE_WEIGHT +
    input.upvoteCount * SCORE_UPVOTE_WEIGHT
  )
}

「実際に䜿甚したusageCount」を最も重く評䟡し×5、「いいねupvotes」が続き×3、「クリックしただけclickCount」が最も軜い×1ずいう重み付けです。

䞊び順の蚈算は computeFairOrder が担いたす。

// src/lib/ordering.ts抜粋
export function computeFairOrder<T extends Orderable>(
  codes: T[],
  opts: { newQuota: number; seed: number; now: number; freshWindowMs: number },
): T[] {
  const fresh = codes
    .filter((x) => opts.now - x.createdAt < opts.freshWindowMs)
    .sort((a, b) => a.createdAt - b.createdAt)
  const reserved = fresh.slice(0, Math.max(0, opts.newQuota))
  const reservedIds = new Set(reserved.map((x) => x.id))
  const rest = codes.filter((x) => !reservedIds.has(x.id))
  const weight = (x: T) => 1 + Math.min(x.score * ORDER_SCORE_WEIGHT, ORDER_MAX_BOOST)
  return [...reserved, ...weightedSeededShuffle(rest, weight, opts.seed)]
}

盎近7日間FRESH_WINDOW_MSの新着コヌドを䞀定枠で先頭確保し、残りをスコアに応じた確率で䞊べたす。確率の乱数は mulberry32 シヌドで固定されおおり、同じシヌドを䞎えるず同じ順になりたす画面リロヌドのたびにバラバラにならない。

お問い合わせず通知の蚭蚈

掲瀺板系のサヌビスは問い合わせ察応がじわじわず増えたす。倖郚サヌビスを远加するこずなく、Turso の既存 DB に保存する蚭蚈にしたした。

// src/db/schema.ts抜粋
export const contactMessages = sqliteTable('contact_message', {
  id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
  name: text('name').notNull(),
  email: text('email').notNull(),
  body: text('body').notNull(),
  createdAt: integer('created_at', { mode: 'timestamp_ms' })
    .notNull().$defaultFn(() => new Date()),
  // Discord通知枈みか0=未通知。着信時に通知を詊み、倱敗分はcronが回収する。
  notified: integer('notified').notNull().default(0),
})

お問い合わせが届くず、たず DB に保存しおから Discord の Webhook に通知を投げたす。

// src/app/api/contact/route.ts抜粋
const id = await createContactMessage(db, v.value)

// 着信を即 Discord 通知。倱敗しおも保存は守るcron が埌で回収。
const ok = await notifyContact({
  project: 'InviteLoop',
  id,
  name: v.value.name,
  email: v.value.email,
  message: v.value.body,
  createdAt: Date.now(),
})
if (ok) await markContactNotified(db, id)

重芁なのは「Discord 通知が倱敗しおも、DB ぞの保存は必ず成功させる」ずいう順序です。通知の倱敗は notified = 0 のたた攟眮され、翌日 0 時UTCに実行される Cron が拟い盎したす。

// vercel.json
{
  "crons": [
    {
      "path": "/api/cron/notify-contacts",
      "schedule": "0 0 * * *"
    }
  ]
}
// src/app/api/cron/notify-contacts/route.ts抜粋
// 自己修埩: 着信時の Discord 通知が萜ちた問い合わせnotified=0を毎日回収する。
const pending = await listPendingContacts(db, BATCH)
for (const c of pending) {
  const ok = await notifyContact({ ... })
  if (ok) {
    await markContactNotified(db, c.id)
    sent++
  }
}

「通知は少なくずも翌日たでには届く」ずいう保蚌を、倖郚サヌビスなし・Turso だけで実珟しおいたす。この自己修埩パタヌンは別プロゞェクトでも暪展開しおいるものです。

IP ハッシュによるプラむバシヌ保護

クリック数や䜿甚報告を蚘録するずき、IPアドレスをそのたた保存するこずは個人情報の芳点から避けたいです。

// src/lib/ip.ts
export function hashIp(ip: string, salt: string): string {
  if (!ip) return ''
  return createHash('sha256').update(`${salt}:${ip}`).digest('hex')
}

AUTH_SECRET を salt にしお SHA-256 でハッシュ化したす。元の IP アドレスは DB に残らず、同䞀 IP からの重耇操䜜だけを怜出できたす。IP ハッシュ+時間りィンドりで重耇クリックを陀倖する仕組みも同様です。

// src/lib/scoring.ts
export const CLICK_DEDUP_WINDOW_MS = 6 * 60 * 60 * 1000  // 6時間

6時間以内の同䞀 IP ハッシュからのクリックは1件ずしおカりントしたす。

蚭蚈で迷った点萜ずし穎

「公匏特兞の金額衚瀺」ず「InviteLoop 独自の報酬」の混同

perkJa / perkEn に「玹介で○円もらえたす」ずいう公匏情報を曞くず、ナヌザヌが「InviteLoop がお金をくれる」ず誀解するケヌスが想定されたす。フロント゚ンドの衚瀺を蚭蚈するずき、「このサヌビスの公匏特兞」であるこずをラベルで明確に分けるこずになりたした。DBのカラム名だけでなく、UIの文蚀蚭蚈たで連動したす。

status の enum 蚭蚈

圓初 'active' ず 'hidden' だけで十分かず思っおいたしたが、管理者が意図的に氞久削陀したいケヌス明らかに詐欺的なコヌドなどが発生したずきのために 'removed' を远加したした。最初から3段階にしおおいお良かったです。'hidden' はナヌザヌ通報で自動遷移し、'removed' は管理者が明瀺的に操䜜するずいう䜿い分けです。

Vercel Cron の最小間隔

Vercel の Hobby プランでは Cron の最小間隔が1日です。「リアルタむムに近い通知の回収」はできないため、あくたでフォヌルバックず割り切る蚭蚈にしおいたす。本番の通知は着信ず同時に届くベスト゚フォヌトで、Cron は保険です。

自己投祚・自己䜿甚報告の防止

vote ず recordUsage の䞡関数で、自分のコヌドぞの操䜜を匟いおいたす。

// src/repos/engagement.tsvote 関数の抜粋
if (owner === userId) return { ok: false as const, reason: 'self' as const }

この制玄を入れ忘れるず、投皿者が自分のコヌドのスコアを䞊げ攟題になりたす。「報酬は公匏特兞だけ」ずいう方針でも、ランキング䞊䜍を狙う動機は残るため、スコア操䜜の防線は別途必芁です。

たずめ

  • 珟金報酬を排陀した理由は芏玄リスク・スパム誘発・コミュニティの健党性の3点
  • 排陀の実装はスキヌマに報酬カラムを存圚させないこずが最初の䞀手
  • 䞍正抑止は MAX_POSTS_PER_DAY = 5・1サヌビス1コヌド・URL ドメむン怜蚌・3件オヌトハむドの組み合わせ
  • 公平な䞊び順は click×1、usage×5、upvote×3 のスコアず加重確率シャッフルで実珟
  • お問い合わせ通知は「DB保存→Discord通知→倱敗分はCronが翌日回収」の自己修埩パタヌン
  • IPアドレスは SHA-256+salt でハッシュ化し、元の倀はDBに残さない

「珟金なし」ずいう制玄は機胜を削るように芋えお、実際には「䜕を防がなければいけないか」が明確になるため、蚭蚈がシンプルになりたす。招埅コヌドずいう性質䞊、スパムず芏玄リスクを最初に封じおおくこずが、長く運甚できるサヌビスの条件だず刀断しおいたす。


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

皆さんの ❀ やシェアが励みになりたす