Facebook group into donations on Forward.
The server never touches Facebook. The agent does the reading, with its own
browser session and its own membership of the groups; the endpoint only turns
"here is a post" into a record. That keeps scraping out of the application and
leaves the MCP surface small enough to stay stable.
Three properties are enforced structurally rather than by convention:
- Imports are drafts. A new `draft` state is invisible everywhere, because
every listing already goes through `active`, and the direct URL 404s for
anybody but its owner. Watch alerts are held back until `publish!`.
- Imports are credited to the original poster. `user_id` stays the importing
account - a row needs an owner, and that is who to ask about it - while the
donation is attributed to `source_author_name` and links to `source_url`.
Imports are not requestable, because the importer does not have the item;
visitors are sent to the original post instead. `create_donation` requires
both fields, so there is no way to use this endpoint to post something as
your own.
- Re-scanning a group is safe. `source_url` is unique at the database, and is
normalised to drop fbclid, mibextid and friends first, so the second import
of a post is refused rather than duplicated.
Two changes fall out of the ownership split. The donor-currency check is
skipped for imports, since `user` is the importer and their region says nothing
about the market the donation belongs to - somebody in Copenhagen curating a
Stockholm group is importing SEK donations, and that is correct. To keep the
invariant that check was protecting, `User#update_stats!` now counts only
donations the user posted themselves, so a curator is neither credited for
items they never had nor able to mix currencies into their totals. The region
and charity checks are untouched.
`list_charities` takes a region and returns only charities whose currency
matches it. The currency rule is the constraint an agent would otherwise trip
over on most calls, and answering it in the tool signature beats answering it
in an error message.
Image URLs arrive from a page an agent was told to go and read, so they are
attacker-influenced in the ordinary case. `RemoteImageFetcher` is https-only,
resolves the host and checks every address it resolves to, pins the connection
to the address it checked, and re-runs all of that per redirect hop.
Authentication is a bearer token rather than the session cookie, because the
caller is not a browser. Tokens are stored as HMAC digests, shown once, can be
named and renamed, and are rate limited per token.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
⬆️ Upgrade libraries
something the donor picks per item.
- a region owns the currency; a charity accepts exactly one, which keeps its
total raised a single figure
- a donation copies its region's currency and validates it against the region,
the charity and the donor's own region
- the locale decides only how an amount is punctuated, never which currency
- the currency picker is gone from the donation form; `market_controller` keeps
the symbol and charity list in step with the selected region
- `currency_symbol` columns are dropped for a new `Currency` model, following
`Locale` as the single source of truth
- the admin dashboard's cross-market total is grouped by currency, not summed
Also makes `regions.donations_count` a true counter cache. An active-only
counter cannot work, since claiming or deleting a donation is an update and
never fires the create/destroy callbacks that maintain it. The regions index,
which labelled that number "available items", now queries for the active count.