Per CREATOR direction on PR #226. Tightening the existing endpoint-wide
fixed-window from 5/24h to 3/24h per source IP. The BTC Map import-RPC
lane forwards submissions into the upstream reviewer queue (not an
instant publish), and rate-limit is the primary spam control on the
public endpoint.
Adds a second downstream lane to /apis/btcmaps/v1/submit that forwards
the merchant payload to teambtcmap/btcmap-api's submit_place RPC
(merged 2026-05-24 in teambtcmap/btcmap-api#91).
Request schema:
- New fields Lat, Lon, Category, ExternalId on BtcMapsSubmitRequest,
required iff SubmitToBtcMap=true. Validator enforces lat/lon
ranges, lowercase-identifier category, and 1-200 char external_id.
Plugin side (rollforsats/BTCPayServerPlugins) composes external_id
as hostname:storeId so the namespace stays unique per BTCPay
instance.
- New SubmitToDirectory + SubmitToBtcMap routing flags. The
directory flag defaults true to preserve existing callers; btcmap
defaults false so new callers must opt in.
- New Phone field forwarded as OSM Key:phone in extra_fields.
Service layer:
- BtcMapsService.SubmitToBtcMapAsync POSTs a JSON-RPC 2.0 envelope
({jsonrpc, method, params, id}) to BTCMAPS:BtcMapImportEndpoint
(default https://api.btcmap.org/rpc) with method=submit_place,
origin=btcpayserver, and the merchant payload mapped to the
documented param shape. Bearer auth from BTCMAPS:BtcMapImportToken.
- Optional fields (website, description, twitter, github, onion,
phone, country) ride along in extra_fields using OSM tag keys
(contact:twitter, addr:country, etc.) plus the implicit
payment:bitcoin=yes marker.
- New BtcMapTokenMissingException parallels the existing
DirectoryTokenMissingException so the controller can return 503
with a distinct error code when ops haven't provisioned the
scoped token yet.
Controller:
- /apis/btcmaps/v1/submit branches on SubmitToDirectory +
SubmitToBtcMap. At least one must be true (rejected 400 otherwise).
- Each lane has its own exception ladder symmetric to the existing
directory path: token-missing 503 (directory-not-configured /
btcmap-not-configured), caller-cancel rethrow, upstream-timeout
504, generic-failure 502 - error codes namespaced by lane so ops
can tell them apart.
HttpClient registration:
- New HttpClientNames.BtcMap named client registered with 15s
per-call timeout and JSON Accept header, matching the
BtcMapsDirectory budget for bounded worst-case behavior.
Tests:
- 12 new validation tests in BtcMapsServiceTests covering the
SubmitToBtcMap=true required-field paths (Lat / Lon / Category /
ExternalId; range checks; lowercase-identifier policy; overlong
external_id) plus the default-false directory-only-still-works
baseline. 37/37 BtcMapsServiceTests passing.
Addresses post-#224 review feedback from @rollforsats + CodeRabbit:
- IHttpClientFactory + named HttpClientNames.BtcMapsDirectory client
replaces per-request `new HttpClient()`. 15s per-call timeout caps the
~5-7 GitHub round-trips at a bounded worst case instead of the default
100s x N. Bearer token stays per-request (the BTCMAPS token is distinct
from the global PluginBuilder GitHub token; must not leak into the
singleton handler).
- Markdown injection guard on the PR body. User fields (Name, Type,
SubType, Country, Twitter, GitHub) are wrapped in inline code spans
with backtick-escape so a doctored merchant name can't render as a
clickable link in the maintainer-facing PR description. Description
goes inside a fenced code block. URL is rendered as <bare-url> autolink
so the maintainer always sees the actual destination.
- Idempotent branch name: SHA-1-derived suffix from the normalized URL
replaces the random GUID. Two concurrent same-URL submissions now
collide on `git/refs` create instead of racing through preflight and
opening duplicate PRs. The 422 "Reference already exists" surface is
caught and mapped to the open-PR lookup or `branch-exists-no-open-pr`.
- NormalizeUrl lowercases scheme + host only and preserves path + query
case verbatim. Lowercasing the whole URL falsely de-duplicates
case-sensitive paths.
- Country code validation moves to an actual ISO 3166-1 alpha-2 set
built from CultureInfo at startup. Replaces the
`length==2 && IsUpper` shape that accepted reserved/unassigned codes
like ZZ.
- Missing BTCMAPS:DirectoryGithubToken throws
DirectoryTokenMissingException at the service layer; controller maps
it to 503 with `directory-not-configured`. Previously surfaced as a
200 OK with `Skipped` which a client could misread as "accepted".
5 new tests:
- Validate_RejectsNonAssignedTwoLetterCountry (ZZ)
- NormalizeUrl_PreservesPathCase
- NormalizeUrl_PreservesQueryCase
- BuildBranchName_DeterministicForSameUrl
- BuildBranchName_DiffersForDifferentUrls
25/25 BtcMapsServiceTests pass on Release build.
Supersedes PR #211. Per-store OSM OAuth moves to the BTC Map
plugin side (rollforsats/BTCPayServerPlugins PR #5); the
plugin-builder side keeps only the directory PR submission.
Drops vs PR #211:
- TagOnOsmAsync / UnlistFromOsmAsync service paths and all
OSM XML + changeset infrastructure (~430 lines)
- TagOnOsm / UnlistFromOsm / OsmNodeId / OsmNodeType /
Latitude / Longitude / OsmCategory / AcceptsLightning
request fields
- BtcMapsOsmResult response shape + Address sub-model
- OSM-specific validators
Keeps:
- POST /apis/btcmaps/v1/submit opens a PR against
btcpayserver/directory.btcpayserver.org's merchants.json
- GET /apis/btcmaps/v1/ping
- Rate limit: 5 submissions / 24h per source IP
- Validation for name / url / description / type / subType /
country / twitter / github / onionUrl
Build clean (Release); 20 unit tests cover validation, slug,
URL normalization.
Add fixed-window rate limiter to public plugin endpoints using ASP.NET
Core's built-in RateLimiter middleware, keyed by client IP. Rate limit
settings (permit limit, window seconds) are stored in the database and
cached via AdminSettingsCache, with sensible defaults of 30 req/60s.
Hand-crafted OpenAPI 3.0.3 spec covering all 8 REST API endpoints.
Redocly-powered interactive docs served at /docs.
README simplified to link to the new docs page.