让免费 AI 帮我抓住偷钱的订阅 | I Made Free AI Hunt My Sneaky Subscriptions
用 Codex 的免费额度,配合两套开源工程技能,做一个纯 Python 标准库的本地订阅账单管家,重点是让它先把"月末扣款日"这个坑问出来、再用测试锁死。/ Using Codex's free tier plus two open-source engineering skill sets, build a local subscription-bill tracker in pure standard-library Python — the point being to let it interrogate the month-end billing-date trap out of you first, then lock it down with tests.
完整实操案例
为什么选这个:纯标准库、零 pip 安装、M2 16G 毫无压力、模型全在云端跑不占本地内存,而"1 月 31 日订阅下次几号扣"是个真实且非平凡的正确性陷阱。/ Why this one: pure stdlib, zero pip installs, effortless on an M2 with 16GB, all inference runs in the cloud so local memory is untouched — and "January 31st subscription, what's the next billing date" is a genuine, non-trivial correctness trap.
1. 装 Codex CLI/Install Codex CLI
macOS 用官方一键脚本或 Homebrew,装完 codex login 用免费 ChatGPT 账户登录。/On macOS use the official one-liner or Homebrew, then run codex login with a free ChatGPT account.
不需要信用卡,Free 档就能跑通整个例子。/No credit card needed; the Free tier carries this whole example.
2. 建一个干净仓库/Create a clean repo
mkdir sub-tracker && cd sub-tracker && git init,然后 python3 --version 确认是 3.9 以上。/Run mkdir sub-tracker, cd into it, git init, then check python3 --version is 3.9 or above.
macOS 自带的 Python 3 就够,zoneinfo 和 unittest 都在标准库里。/The Python 3 that ships with macOS is enough; zoneinfo and unittest are both stdlib.
3. 装技能库,只装一套/Install the skills, pick one path only
npx skills@latest add mattpocock/skills,在勾选界面务必勾上 setup-matt-pocock-skills。/Run npx skills@latest add mattpocock/skills, and be sure to tick setup-matt-pocock-skills in the picker.
用 Claude Code 的人改用 claude plugins install mattpocock-skills,但两条路只能选一条,否则每个技能都会有两份。/Claude Code users run claude plugins install mattpocock-skills instead — pick one path only, or you get every skill twice.
4. 跑一次初始化/Run the one-time setup
在代理里执行 /setup-matt-pocock-skills,issue tracker 选"本地文件"。/In the agent run /setup-matt-pocock-skills and choose local files as the issue tracker.
选本地文件是为了完全免费、也不用授权任何仓库权限。/Local files keeps it entirely free and avoids granting any repo permissions.
5. 装 Superpowers,并指定谁当家/Install Superpowers, and name the controller
按仓库 docs/README.codex.md 装 Codex 版;Claude Code 用 /plugin install superpowers@claude-plugins-official。/Install the Codex build per the repo's docs/README.codex.md; Claude Code users run /plugin install superpowers@claude-plugins-official.
在 AGENTS.md 里写明"Superpowers 负责流程,mattpocock 技能按需调用",避免两套指令打架。/Write in AGENTS.md that Superpowers owns the process and mattpocock skills are called on demand, so the two don't fight.
6. 先被追问,别急着写码/Get grilled before any code
运行 /grill-with-docs,只给一句需求:"我想知道我的订阅什么时候扣钱"。/Run /grill-with-docs and give it just one line: I want to know when my subscriptions charge me.
它会逼出你没想过的分支——年付还是月付、试用期算不算、时区按哪个、以及那个致命问题:1 月 31 日订的,2 月几号扣?/It will force out the branches you never considered: annual or monthly, does the trial count, whose timezone, and the killer — subscribed January 31st, which day does February bill?
7. 把术语钉进 CONTEXT.md/Nail the vocabulary into CONTEXT.md
让它把"锚定日""计费周期""宽限期"写成项目共享语言。/Have it write anchor day, billing cycle, and grace period into the project's shared language.
以后你说"锚定日漂移"它就懂,不用每次解释三句话。/From then on you say anchor-day drift and it knows, instead of you explaining it three times over.
8. 出 spec 和 tickets/Produce the spec and tickets
/to-spec 把对话变成规格,/to-tickets 拆成带阻塞关系的小票。/Run /to-spec to turn the conversation into a spec, then /to-tickets to split it into tickets with blocking edges.
你会看到"锚定日计算"自动被拆成独立一票——这就是流程在帮你。/You'll see anchor-day calculation break out as its own ticket — that's the process working for you.
9. 用 worktree 隔离/Isolate with a worktree
git worktree add ../sub-tracker-anchor -b feat/anchor,让代理只在这个副本里干活。/Run git worktree add ../sub-tracker-anchor -b feat/anchor and let the agent work only in that copy.
主分支永远干净,搞砸了删掉目录就行。/Your main branch stays pristine; if it goes wrong, just delete the directory.
10. 先写会红的测试/Write the test that goes red first
/tdd,要求第一个测试就是"1 月 31 日订阅,第 1 期应是 2 月 28 日,第 2 期必须回到 3 月 31 日"。/Run /tdd and demand the first test be: subscribed January 31st, cycle one lands February 28th, and cycle two must return to March 31st.
"必须回到 31 号"这句是整个例子的灵魂,90% 的实现都会在这里错。/That "must return to the 31st" is the soul of this example; nine out of ten implementations get it wrong.
11. 让它实现到测试变绿/Let it turn the test green
盯住一点:必须从原始起始日按期数直接算,不能逐月累加。/Watch for one thing: it must compute the nth cycle from the original start date, never accumulate month by month.
逐月累加会让 31 号在二月被永久截断成 28 号,这就是真实世界的账单 bug。/Accumulating month by month permanently clips the 31st down to the 28th, and that is the real-world billing bug.
12. 故意制造一个 bug 再诊断/Plant a bug, then diagnose it
手动把 min 改成 max 提交,然后运行 /diagnosing-bugs。/Manually change min to max, commit, then run /diagnosing-bugs.
它会先建一个能稳定复现的红灯,再最小化、假设、加探针、修复、补回归测试。/It builds a reliably red repro first, then minimises, hypothesises, instruments, fixes, and adds a regression test.
13. 双轴代码审查/Two-axis code review
/code-review 会分两路子代理:一路查代码规范和坏味道,一路查是否忠实实现了第 8 步的 spec。/Run /code-review and it splits into two sub-agents: one checks standards and smells, the other checks faithfulness to the spec from step 8.
两路分开跑,所以不会互相污染结论。/They run separately, so neither contaminates the other's verdict.
14. 规划装不进一个上下文的部分/Plan what won't fit in one context
/wayfinder,目标写"多币种 + 导出 iCal 提醒 + 汇率历史"。/Run /wayfinder with the goal: multi-currency plus iCal reminder export plus exchange-rate history.
它会拆成一串决策票存进本地文件,进度不再靠聊天记忆撑着。/It breaks that into decision tickets in local files, so progress no longer rides on chat memory.
15. 交接给明天的自己/Hand off to tomorrow's you
/handoff 生成交接文档,然后 codex 退出,额度归零也不怕。/Run /handoff to produce a handoff doc, then quit codex — hitting your limit no longer costs you anything.
明天开新会话读那份文档就能续上,这是免费额度用户最重要的一招。/Tomorrow a fresh session reads that doc and picks up where you left off; for free-tier users this is the single most important move.
16. 每天早上一行命令/One command each morning
python3 -m subtracker upcoming --days 14,把它加进 shell alias。/Run python3 -m subtracker upcoming --days 14 and drop it into a shell alias.
七个房客全部现形,一分钱模型额度都不用花。/All seven tenants show their faces, and it costs zero model credits.
核心 Python 代码(纯标准库,零安装)
subtracker/anchor.py — 整个例子的技术核心:
"""
Billing-date arithmetic. Pure stdlib: no third-party packages, no registry needed.
"""
from __future__ import annotations
import calendar
from dataclasses import dataclass
from datetime import date
def _clamp_to_month(year: int, month: int, anchor_day: int) -> date:
"""Place anchor_day in (year, month), clamping to that month's last day."""
last = calendar.monthrange(year, month)[1]
return date(year, month, min(anchor_day, last))
def nth_monthly_renewal(start: date, n: int) -> date:
"""The nth renewal after `start`, computed from the ORIGINAL anchor day.
This is the whole point. Never accumulate month by month: doing so
permanently clips a 31st down to 28th once it passes February.
>>> nth_monthly_renewal(date(2026, 1, 31), 1)
datetime.date(2026, 2, 28)
>>> nth_monthly_renewal(date(2026, 1, 31), 2) # must RETURN to the 31st
datetime.date(2026, 3, 31)
"""
if n < 1:
raise ValueError("n must be >= 1")
total = start.month - 1 + n
return _clamp_to_month(start.year + total // 12, total % 12 + 1, start.day)
@dataclass(frozen=True)
class Subscription:
name: str
amount_cents: int
start: date
cadence: str = "monthly" # "monthly" | "yearly"
def next_renewal(self, today: date) -> date:
if self.cadence == "yearly":
return _clamp_to_month(
today.year + (1 if (today.month, today.day) > (self.start.month, self.start.day) else 0),
self.start.month,
self.start.day,
)
n = 1
while nth_monthly_renewal(self.start, n) <= today:
n += 1
return nth_monthly_renewal(self.start, n)
tests/test_anchor.py — 第 10 步那个会变红的测试:
import unittest
from datetime import date
from subtracker.anchor import Subscription, nth_monthly_renewal
class TestAnchorDrift(unittest.TestCase):
def test_jan31_clamps_in_february(self):
self.assertEqual(nth_monthly_renewal(date(2026, 1, 31), 1), date(2026, 2, 28))
def test_jan31_returns_to_31_in_march(self):
"""The test that catches the real-world billing bug."""
self.assertEqual(nth_monthly_renewal(date(2026, 1, 31), 2), date(2026, 3, 31))
def test_no_cumulative_drift_over_a_year(self):
self.assertEqual(nth_monthly_renewal(date(2026, 1, 31), 12), date(2027, 1, 31))
def test_leap_year_february_29(self):
self.assertEqual(nth_monthly_renewal(date(2028, 1, 31), 1), date(2028, 2, 29))
def test_next_renewal_skips_past_dates(self):
sub = Subscription("Streaming", 1599, date(2026, 1, 31))
self.assertEqual(sub.next_renewal(date(2026, 3, 1)), date(2026, 3, 31))
if __name__ == "__main__":
unittest.main()
跑测试:python3 -m unittest discover -s tests -v
Related Links(相关链接):
https://github.com/romgX/openrelay
https://openai.com/codex
https://github.com/openai/codex
https://developers.openai.com/codex/cli
https://openai.com/chatgpt/pricing/
https://github.com/mattpocock/skills
https://skills.sh/mattpocock/skills
https://www.aihero.dev/skills
https://github.com/obra/superpowers
https://code.claude.com/docs/en/plugins
https://docs.python.org/3/library/calendar.html
https://docs.python.org/3/library/datetime.html
https://docs.python.org/3/library/unittest.html
https://docs.python.org/3/library/zoneinfo.html
https://git-scm.com/docs/git-worktree
评论
发表评论