diff --git a/broker_sync/providers/_checkpoint.py b/broker_sync/providers/_checkpoint.py
new file mode 100644
index 0000000..0df7a63
--- /dev/null
+++ b/broker_sync/providers/_checkpoint.py
@@ -0,0 +1,30 @@
+from __future__ import annotations
+
+import json
+from datetime import UTC, datetime
+from pathlib import Path
+
+
+class Checkpoint:
+ """Per-account cursor persistence.
+
+ File shape: `{"cursor": "...", "updated_at": "2026-04-17T12:00:00+00:00"}`
+ One file per (provider, account_id) at `
/-.json`.
+ """
+
+ def __init__(self, directory: Path, *, provider: str, account_id: str) -> None:
+ self._path = directory / f"{provider}-{account_id}.json"
+
+ def load(self) -> str | None:
+ if not self._path.exists():
+ return None
+ raw = json.loads(self._path.read_text())
+ cursor = raw.get("cursor")
+ if not isinstance(cursor, str):
+ return None
+ return cursor
+
+ def save(self, cursor: str) -> None:
+ self._path.parent.mkdir(parents=True, exist_ok=True)
+ payload = {"cursor": cursor, "updated_at": datetime.now(UTC).isoformat()}
+ self._path.write_text(json.dumps(payload))
diff --git a/tests/providers/test_checkpoint.py b/tests/providers/test_checkpoint.py
new file mode 100644
index 0000000..d98f7bf
--- /dev/null
+++ b/tests/providers/test_checkpoint.py
@@ -0,0 +1,61 @@
+from __future__ import annotations
+
+import json
+from datetime import UTC, datetime
+from pathlib import Path
+
+from broker_sync.providers._checkpoint import Checkpoint
+
+
+def test_load_missing_returns_none(tmp_path: Path) -> None:
+ cp = Checkpoint(tmp_path, provider="t212", account_id="isa")
+ assert cp.load() is None
+
+
+def test_save_then_load_roundtrip(tmp_path: Path) -> None:
+ cp = Checkpoint(tmp_path, provider="t212", account_id="isa")
+ cp.save("cursor-xyz")
+ assert cp.load() == "cursor-xyz"
+
+
+def test_save_writes_expected_filename(tmp_path: Path) -> None:
+ cp = Checkpoint(tmp_path, provider="t212", account_id="isa")
+ cp.save("cursor-abc")
+ expected = tmp_path / "t212-isa.json"
+ assert expected.exists()
+ raw = json.loads(expected.read_text())
+ assert raw["cursor"] == "cursor-abc"
+ # updated_at is ISO-8601 with tz — just confirm it parses.
+ parsed = datetime.fromisoformat(raw["updated_at"])
+ assert parsed.tzinfo is not None
+
+
+def test_save_overwrites_previous(tmp_path: Path) -> None:
+ cp = Checkpoint(tmp_path, provider="t212", account_id="isa")
+ cp.save("first")
+ cp.save("second")
+ assert cp.load() == "second"
+
+
+def test_separate_accounts_are_isolated(tmp_path: Path) -> None:
+ a = Checkpoint(tmp_path, provider="t212", account_id="isa")
+ b = Checkpoint(tmp_path, provider="t212", account_id="gia")
+ a.save("isa-cursor")
+ b.save("gia-cursor")
+ assert a.load() == "isa-cursor"
+ assert b.load() == "gia-cursor"
+
+
+def test_save_creates_parent_dir(tmp_path: Path) -> None:
+ nested = tmp_path / "a" / "b" / "c"
+ cp = Checkpoint(nested, provider="t212", account_id="isa")
+ cp.save("cursor")
+ assert (nested / "t212-isa.json").exists()
+
+
+def test_load_rejects_legacy_future_time(tmp_path: Path) -> None:
+ # Malformed file (missing 'cursor' key) → treat as absent rather than crash.
+ p = tmp_path / "t212-isa.json"
+ p.write_text(json.dumps({"updated_at": datetime.now(UTC).isoformat()}))
+ cp = Checkpoint(tmp_path, provider="t212", account_id="isa")
+ assert cp.load() is None