メールからサブスクを自動で見つける「Cycrio」を作った

技術

はじめに

動画配信、音楽、クラウドストレージ、開発ツール、ドメイン、各種会員サービス。気づけば、いろいろなサービスに月額・年額の料金を払っています。

厄介なのは、契約先が増えるほど全体像が見えなくなることです。月払いと年払いが混在し、契約に使ったメールアドレスもバラバラ。管理表を作ったところで、新しいサービスを契約するたびに手入力する必要があります。料金変更や解約のたびに更新するのも面倒です。

でも、ふと気づきました。契約や請求、更新のお知らせは、ほとんどメールに届いている。

だったら、そのメールから契約情報を拾い集めればいいのでは?

そんな発想から作り始めたのが、サブスクリプション管理Webアプリ Cycrio(サイクリオ) です。

この記事では、Cycrioでできることに加えて、開発中に直面した課題や設計上の工夫も紹介します。

メールを接続して、契約候補を見つける

Cycrioの基本的な使い方はシンプルです。

  1. メールアカウントを接続する
  2. 対象フォルダーと取り込み期間を指定する
  3. メールを解析し、サブスクの契約候補を抽出する
  4. 内容を確認して、新規登録・既存契約への紐付け・不要のいずれかを選ぶ
  5. 契約一覧や請求予定、レポートで管理する

接続先は、Gmail / Google Workspace、個人向けのOutlook.com / Hotmail、IMAPに対応しています。FastmailのようなIMAP対応サービスも利用できます。

複数のメールアカウントを登録できるので、仕事用と個人用など、異なるアドレスに届いた契約情報をまとめて確認できます。もちろん、メールを接続せず手動で登録することも可能です。

登録した契約は、料金、請求周期、次回請求日などを一覧で確認できます。月額・年額に換算した目安、サービス別の内訳、今後の請求予定、支払い・返金履歴、CSV出力にも対応しました。

例えば、年額払いのサービスを月額換算で比べたり、契約をいくつか見直すとどの程度支出を減らせそうか確認したりできます。

AIが抽出した情報を、そのまま信用しない

メールの解析にはLLMを使っています。

最初にLiquid AIでサブスク関連のメールかどうかを判定し、詳しい情報の抽出にはOpenRouter / OpenAI / Anthropicのモデルを利用できる構成です。

メールの書式はサービスごとに違います。「ご利用料金のお知らせ」「領収書」「次回更新のお知らせ」など、表現もさまざま。固定のルールだけで対応するより、LLMを使う方が柔軟に情報を取り出せます。

ただし、抽出できた情報が、そのまま正しい契約情報とは限りません。

例えば、メールに「980円」と書かれていたとします。それは毎月の通常料金なのか、今回だけの請求額なのか。割引後の金額かもしれませんし、返金額かもしれません。あるいは、来月から適用される新料金かもしれません。

そこでCycrioでは、AIの解析結果をいきなり契約に反映せず、まず「確認候補」として表示します。利用者が根拠を確認し、登録するかどうかを判断する仕組みです。

既存契約に紐付ける場合も、根拠となる情報を追加するだけの操作と、契約内容を変更する操作を分けました。変更時には現在の値と抽出した値を比較し、承認した項目だけを反映します。

もう一つこだわったのが、料金が分からないものを「0円」にしないこと。

無料だと確認できた契約と、単に金額を読み取れなかった契約は別物です。通貨や周期が不明な場合も、勝手に補完せず「不明」のまま扱います。

同様に、月額・年額に換算した契約料金と、メールから読み取った実際の支払記録も分けています。外貨の円換算は参考レートを使った目安であり、カード会社の請求額そのものではありません。

作ってみると、難しいのは「メールから数字を抜き出すこと」よりも、その数字が何を意味するのかを正しく扱うことでした。

「来月から値上げ」を、今日の料金にしない

料金の意味だけでなく、「いつから有効なのか」も重要です。

例えば「来月から月額料金を値上げします」というメールが届いたとして、現在の契約料金をすぐに書き換えてしまうと、今月の支出が実態と合わなくなります。

そのためCycrioでは、契約変更を将来の日付で予約できるようにしました。

利用者が変更内容と効力日を確認して予約すると、それまでは現在の契約情報を維持し、指定日以降にWorkerが変更を反映します。効力日は日本時間の日付で管理しています。

ただ、予約した後に利用者が契約を手動編集する可能性もあります。そのまま予約内容を上書きしてしまうのは危険です。

そこで、予約時と反映時の状態が食い違った場合は、自動適用せず「要再確認」にします。変更前後の内容と根拠は履歴に残し、予約の取り消しにも対応しています。契約そのものを削除した場合は、関連する予約も取り消します。

無料期間、自動更新の有無、利用終了日も別々に管理します。「自動更新をオフにした」と「今日からサービスが使えなくなった」は、同じ意味ではないからです。

なお、Cycrioが変更するのはあくまでアプリ内の管理情報です。Cycrio上で契約を削除しても、サービス提供元での解約にはなりません。

AIの利用料金を管理する機能にも、AI代がかかる

開発中、予想以上に考えることになったのがLLMのコストです。

Cycrioでは、メール解析に使うモデルを管理画面から選択できるようにしています。そのため、モデルの一覧と価格を管理する機能も必要になりました。

OpenRouterの価格は一覧APIから取得し、それ以外の対象プロバイダーについてはLLMに公式情報を調べさせる仕組みを実装しました。

ところが、モデルの料金を調べるだけの処理が、想定以上にトークンを消費することがありました。サブスクの支出を管理するアプリなのに、その管理処理のコストが見えないのでは本末転倒です。

調査結果の精度にもばらつきがあったため、仕組みを見直しました。現在は、対象モデルの分類、通常のテキスト入出力料金、公式の出典を短いプロンプトで問い合わせます。同じプロバイダーのモデルを最大16件まとめて調べ、返ってきたモデルID、公式URL、分類、価格の形式をアプリ側で検証します。

ここでは、調査したモデル数と実際のLLM呼び出し回数も分けて集計しています。16モデルを1回の呼び出しで調べたなら、使用量と原価を計上するのは1回分だけ。確認できなかったモデルを、有料の個別問い合わせで無条件に再調査することもしません。

原因を後から調べられるよう、料金調査時に送った内容と応答は管理者向けに暗号化し、7日間保存しています。結果だけでは、プロンプト、LLMの回答、アプリ側の検証のどこに問題があったのか追いにくかったからです。

メール解析の原価は利用者と紐付け、月別・ユーザー別・メール接続別に集計します。管理者による接続テストやモデル料金調査など、システム側の処理は別枠です。

ユーザーごとの月額上限も設定できます。ただし、これは新規の呼び出しを始めるかどうかを判定するもので、実行中の処理によって最終的に上限を超える可能性があります。厳密な請求上限を保証するものではありません。

LLMを組み込むなら、出力の品質を見るだけでなく、何を送り、何回呼び、いくらかかったかまで確認できるようにしておくことが大切だと実感しました。

技術構成はSvelteKitとGo、非同期処理にHatchet

現在の主な構成は次のとおりです。

領域技術・サービス
フロントエンドSvelte 5 / SvelteKit / TypeScript
Web配信Cloudflare Workers Static Assets
API・WorkerGo / Coolify(セルフホスト)
データベースPostgreSQL(別ホスト)
ジョブ管理Hatchet Cloud(本番)/ Hatchet Lite(ローカル)
認証Clerk / Googleログイン
メール通知Resend

メールの取得や解析は時間がかかるため、HTTPリクエスト内ですべて終わるのを待つ構成にはしていません。APIで要求を受け付け、後続の処理をWorkerに任せています。利用者は画面から進捗や履歴を確認できます。

Hatchet Cloudはジョブの制御を担当し、実際にメールを取得・解析するGoのWorkerはCoolify上で動かしています。

失敗時の再試行、取り込みの停止、過去メールの再解析にも対応しています。ただし、外部のAIサービスへリクエストを送った後に結果が分からなくなった場合は要注意です。安易に再送すると二重実行や追加費用につながるため、送信前の失敗とは区別しています。

ローカル環境はDocker Composeで構築し、GoやNode.jsをホスト側に個別インストールしなくても起動・検証できるようにしました。

本番デプロイはGitHub Actionsで自動化しています。

mainへpush
  ↓
テスト・型チェック・ブラウザー検証・DB互換性確認
  ↓
DB migration
  ↓
API・Workerの更新
  ↓
検証済みのWeb成果物を公開
  ↓
APIと公開リリースの正常性を確認

本番と同じPostgreSQL 16での検証に加え、バックアップと暗号化鍵からの復元、Workerの再接続なども確認する構成です。

メールを扱うからこそ、保存する情報を絞る

Cycrioはメールを読み取るアプリです。だからこそ、どんな情報を保存し、どこへ送るのかは最初から意識しました。

まず、アプリへのログインとメールの読み取り権限を分離しています。GoogleアカウントでCycrioにログインしただけでは、Gmailを読む権限は付与されません。

また、メールの本文や件名は永続保存しません。保存するのは抽出した契約情報や、受信日時、メールID、処理状態などです。

ただし、解析時には対象メールの本文を設定されたAIプロバイダーへ送信します。この点は曖昧にせず、送信先やデータの取り扱い条件、解析対象の範囲を確認して同意する流れにしています。

AIのAPIキーやメール接続の資格情報はDBに暗号化して保存し、登録済みの秘密値は管理画面に再表示しません。暗号化鍵はDBと分離して保管するため、復元には両方が必要です。

利用者ごとのデータ分離や管理者権限も設けています。退会時はCycrio内の個人データとClerkのユーザー情報を削除し、個人情報を含まない原価・監査集計だけを残す設計です。

個人利用から始めたサービスですが、接続するメールアカウントが増えてからデータの扱いを考えるのでは遅い。ここは早い段階で整理しておきたかった部分です。

実際に使うと、気になるのは細かな使い勝手

開発ではAIにも実装を手伝ってもらっています。それでも、実際にアカウントを接続して触ってみると、気になる点は次々に出てきます。

例えば「最終取り込み日時」と「最終解析日時」の表示です。

新しいメールが届いていなければ、メールの確認は実行されても解析は増えません。二つの日時が違うのは正常ですが、日時だけを並べると処理が止まっているようにも見えます。

そこで、最終確認の結果、追加されたメールの件数、解析結果、次回確認や再試行の状況も見えるようにしました。

サービスのロゴも、使い勝手を左右する要素の一つです。海外の主要サービスだけでなく、国内のサービスや通信キャリア、ドメイン関連サービスなどにも対応しています。

ロゴはアプリ内から配信しています。表示のたびに外部のロゴ検索サービスへ問い合わせる必要がなく、サービス名やメールアドレスなどの情報を外部の検索サービスへ送らずに済みます。

契約をひと目で見分けられること。処理が動いているのか、完了しているのかが分かること。派手ではありませんが、毎日使う道具としてはこういう細部が大切です。

自分が使い続けたい道具にしていく

最初の目的は単純でした。メールに散らばっているサブスクの契約情報を、一か所に集めたい。 それだけです。

ところが作り始めると、料金の意味、変更の適用日、AIの原価、失敗したジョブの扱い、個人データの削除、バックアップと復元など、考えることが次々に増えていきました。

現在は、メールの取り込みから候補の確認・登録、請求予定やレポート、通知、将来の契約変更予約、本番への自動デプロイまで動くようになりました。

とはいえ、機能を実装したことと、日常的に使いやすいことは別です。これからは実際に使いながら、AIが見つけた候補をどれだけ手間なく確認できるか、料金や更新の変化に気づけるか、無理のない運用コストで続けられるかを確かめていきます。

契約情報をただ集めるだけでなく、自分で把握し、必要なときに見直せるようにする。

Cycrioを、そんな道具として育てていくつもりです。

開発中のソースコードは、GitHub(enjoydarts/cycrio) で公開しています。

GitHub – enjoydarts/cycrio: Cycrio — privacy-conscious subscription management from billing emails
Cycrio — privacy-conscious subscription management from billing emails – enjoydarts/cycrio

コメント

タイトルとURLをコピーしました