Skip to main content

brand-evidence

Local-first, tamper-evident record of trademark use, plus daily mention monitoring. Two paths feed one append-only, hash-chained evidence log:

  • push (hook): each published post is captured the moment it goes live: PNG, PDF, raw HTML, SHA-256 of each, and a submission to the Internet Archive for an independent timestamp.
  • pull (crawl): a daily job searches Hacker News, Reddit, Google Alerts feeds and an optional web search API for the brand terms, dedupes, and captures new mentions.

A daily digest reports run status and facts. It never generates commentary.

Setup (under 10 minutes)

git clone <this repo> brand-evidence && cd brand-evidence
uv sync --all-groups                       # Python 3.12 venv + deps
uv run playwright install chromium         # headless browser for capture
cp .env.example .env                       # fill in the required values
uv run brand-evidence init                 # creates the SQLite schema (alembic upgrade head)
uv run brand-evidence digest               # sanity check: an empty digest is a valid output
uv run brand-evidence schedule install     # launchd: crawl 07:00, archive-poll every 2h,
                                           # digest 07:30, timestamp 07:45

Required in .env: BE_DB_PATH, BE_STORE_PATH, BE_DIGEST_PATH, BE_BRAND_TERMS, BE_ARCHIVE_PROVIDER=wayback, BE_WAYBACK_ACCESS_KEY, BE_WAYBACK_SECRET_KEY. The CLI refuses to start when any is missing. The Wayback keys are free: create an archive.org account and copy them from https://archive.org/account/s3.php. Anonymous Save Page Now no longer works. Optional source credentials disable that source with a warning when absent.

Google Alerts: create alerts for each term at https://www.google.com/alerts, choose "RSS feed" as the delivery, and paste the feed URLs comma-separated into BE_GOOGLE_ALERTS_FEEDS.

Several projects on one machine

Each project has its own .env, database and store. Register them once:

brand-evidence projects add northwind --env-file ~/brands/northwind/.env
brand-evidence projects add acme      --env-file ~/brands/acme/.env
brand-evidence --project northwind digest
brand-evidence schedule install          # one set of launchd jobs per registered project
brand-evidence --project northwind report --json   # machine-readable status

The registry lives at ~/.config/brand-evidence/projects.yaml. BE_PROJECT works as well.

Which sources run, the query terms and the hosts never captured come from a sources.yaml next to that project's .env (or from BE_SOURCES_FILE). Without one, the packaged defaults apply: every source on, terms from BE_BRAND_TERMS, nothing skipped.

Posts published from a server

When publishing happens elsewhere (a posting pipeline on a server, say), point BE_OWN_PUBLICATIONS_URL at a JSON feed of publications; crawl records new items as posts and captures them. The feed returns {platform, external_url, external_id, published_at, text}. sync-posts runs the same step on its own.

Proving dates in the past

Evidence for posts published before this tool existed comes from sources that already carry a date: the platform's own data export (LinkedIn: Settings → Data privacy → Get a copy of your data; Threads: Accounts Center → Download your information), notification emails from the platform in your mailbox, and snapshots the Wayback Machine may already hold.

brand-evidence backfill --file posts.csv          # captures now, archives now, records prior snapshots
brand-evidence archive-history                    # look up prior snapshots for every recorded post
brand-evidence attach --file linkedin-export.zip --kind platform_export --note "requested 2026-09-15"
brand-evidence attach --file post-notification.eml --kind email --post-id <id>

Backfilled posts carry two dates: published_at from the platform and captured_at from now. Prior snapshots are stored with status historical and the archive's own timestamp.

Commands

brand-evidence hook --platform threads --url <url> --body-file <path> [--published-at <iso>]
brand-evidence crawl [--since <iso>] [--source <name>] [--no-capture]
brand-evidence archive-poll
brand-evidence digest [--date YYYY-MM-DD] [--email]
brand-evidence verify
brand-evidence timestamp
brand-evidence export --format zip --out <path>
brand-evidence mentions list [--status new]
brand-evidence mentions set-status <id> <status> [--note <text>]
brand-evidence schedule install|uninstall
brand-evidence backfill --file <csv> [--no-capture] [--no-history]
brand-evidence archive-history [--url <url>]
brand-evidence attach --file <path> --kind <kind> [--post-id <id>] [--note <text>]
brand-evidence sync-posts [--no-capture]
brand-evidence report [--json] [--hours 24]
brand-evidence projects list|add|remove
brand-evidence init

Wiring the publish hook

Whatever publishes a post should call, right after the platform returns the URL:

brand-evidence hook --platform linkedin --url "$POST_URL" --body-file post.txt \
  --published-at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --external-id "$POST_ID"

LinkedIn usually shows a login wall to anonymous visitors. The capture records whatever the anonymous visitor sees and sets capture_meta.login_wall_suspected. Authenticated capture is deliberately out of scope for v1.

Trusted timestamps

Two independent time sources back the record without publishing anything private:

  • brand-evidence timestamp (scheduled daily at 07:45) sends the SHA-256 of the chain head (<seq>:<entry_hash>) to an RFC 3161 Time Stamping Authority and stores the token as an artifact. This proves the whole chain up to that point existed by that time.
  • export stamps MANIFEST.txt and ships MANIFEST.tsr. A third party verifies with openssl ts -verify -data MANIFEST.txt -in MANIFEST.tsr -CAfile <tsa-chain.pem>.

BE_TSA_URL defaults to freetsa.org; any RFC 3161 endpoint works (DigiCert, Sectigo). verify also re-checks every stored token against the chain entry it anchors.

The evidence log

Every write to posts, mentions, artifacts and archive_snapshots appends a row to evidence_log with entry_hash = sha256(prev_hash + occurred_at + event_type + canonical_json(payload)). SQLite triggers refuse UPDATE and DELETE on that table. brand-evidence verify recomputes the whole chain and names the first broken seq. export produces a zip a third party can check with shasum -a 256 -c MANIFEST.txt and a ten-line script described in its README.txt.

Scheduling without a Mac

.github/workflows/crawl.yml runs crawl --no-capture and digest daily on GitHub Actions. It covers discovery only; capture and archive submission need the local machine.

Tests

uv run pytest            # passes with networking disabled; HTTP is mocked with respx
uv run ruff check .

The Playwright integration test is skipped when the Chromium binary is not installed.

Not in v1

Legal or tax advice, auto-posting, auto-responses, authenticated capture, non-English sources, a dashboard, multi-user or hosted anything. See the requirements document §2 and §14.

License

AGPL-3.0-or-later. You may run, study, modify and share this; if you offer it to others as a network service, that service's users are entitled to your changes under the same terms. Copyright remains with the author, so a separate commercial licence is possible - open an issue.

Metadata

Release files for brand-evidence 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for brand-evidence 0.1.0
File Size Uploaded
brand_evidence-0.1.0.tar.gz 123.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for brand-evidence 0.1.0
File Interpreter ABI Platform
brand_evidence-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 204.7 kB

Release files / brand_evidence-0.1.0.tar.gz

Download URL brand_evidence-0.1.0.tar.gz
Size 123.7 kB
Tags Source
SHA-256 checksum
How to use checksums
bfb3f442da406e5afa848700ebe0264f8047ba2f74af0d6fa8c33d0f828d3dd6
BLAKE2b-256 checksum
How to use checksums
d0e7a5672c568d8d0705a02f21b40e01a8b639f758760ba76cf2996024debf50
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.

Transparency log

Release files / brand_evidence-0.1.0-py3-none-any.whl

Download URL brand_evidence-0.1.0-py3-none-any.whl
Size 81.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2dab7fd781b085219fd6ad66e77f2f8080ccb28b220747f7557be4bbaea221be
BLAKE2b-256 checksum
How to use checksums
0824bc5b9749c1ecd122a4fca1a2f92bfde0ab1d7e4978ec8c35c5de95b51842
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page