Bitcoin Cities is a community-driven platform that tracks every Bitcoin-accepting business across the United States. Cities compete on leaderboards based on the number of Bitcoin-friendly businesses, and users earn badges by checking in at those businesses in person.
A gamified platform that turns local Bitcoin adoption into a competitive, community-driven movement. Cities compete on leaderboards based on the number of Bitcoin-accepting businesses, sourced from OpenStreetMap. Users earn badges by checking in at businesses in person, and everyone can track adoption progress through interactive maps, leaderboards, and challenges.
Built with PHP, MySQL/PostgreSQL, and vanilla HTML/CSS/JS. No frameworks, no build tools, no npm. Just upload the files and run.
Bitcoin Cities makes Bitcoin adoption visible and measurable at the city level. Instead of abstract statistics, you see exactly which cities have the most Bitcoin-accepting businesses, who the most active community members are, and how adoption is growing over time.
Users contribute by checking in at businesses that actually accept Bitcoin. Every check-in unlocks badges and helps their city climb the leaderboard. Businesses are imported directly from OpenStreetMap, so the data starts real and stays community-maintained.
- Full-screen interactive Leaflet.js map as the primary entry point
- Browse all Bitcoin-accepting businesses across the country
- Category filter pills: Food, Cafe, Bar/Brewery, Retail, Services, Other
- Lightning Network and Verified business toggle filters
- "Near Me" geolocation button to find nearby businesses
- Marker clustering for dense areas with custom cluster icons
- Search by business name across the entire map
- Business popups showing name, category, payment methods, open/closed status
- Mobile: collapsible bottom drawer with business listings
- Desktop: sidebar list synced with map view
- Five vote types: Lightning OK, On-chain OK, Issues, Refused, Closed
- Location-verified: users must be within 50 miles of a business to submit a vote
- Optional note (up to 280 characters) for positive votes; required (min 5 chars) for negative votes (issues, refused, closed)
- 24-hour cooldown per business per user to prevent spam
- Time-weighted verification score with 6-month half-life decay
- Check-in summary on every business card: positive/negative counts, last checked date
- Anonymous check-ins supported with IP-based rate limiting (30-min global cooldown, 24-hr per-business cooldown); anonymous check-ins update business scores but do not count toward badges or ambassador progression
Badges are earned purely through check-in counts -- no points system:
| Badge | Check-ins Required |
|---|---|
| Normie | 0 |
| Pleb | 3 |
| Explorer | 5 |
| Contributor | 10 |
| City Ambassador | 20 |
| State Ambassador | 50 |
| Sovereign | 100 |
Badges display with tier-specific Unicode icons for Contributor and above, with CSS glow/border effects for higher tiers. Ambassadors and Sovereigns lose editing powers after 60 days without a check-in; reactivation requires 10 check-ins within 60 days.
Activity-based auto-promotion tied to check-in counts:
- Contributor (10+ check-ins): Recognized on city pages
- City Ambassador (20+ check-ins): Auto-assigned to your top city; can edit city info and pin local tips within 50 miles
- State Ambassador (50+ check-ins): Edit powers across states where you hold ambassador records
- Sovereign (100+ check-ins): Nationwide editing powers
Ambassador assignments are re-evaluated on each check-in. City Ambassadors are limited to one city (the one with the most check-ins). Inactivity decay demotes City Ambassadors to Contributor after 60 days without a check-in; reactivation requires 10 check-ins within 60 days.
- Five tabs: Top Cities, Top Members, Ambassadors, Rising Stars, Monthly Leaders
- Filter all rankings by state
- Top Cities: Ranked by number of Bitcoin-accepting businesses
- Top Members: All-time user rankings by total check-ins
- Ambassadors: Sovereigns, State Ambassadors, and City Ambassadors with assigned cities/states and stats
- Rising Stars: Cities with the biggest rank improvements
- Monthly Leaders: Most active members this month
- Global badge roadmap showing the full tier path: Normie through Sovereign
- Visual progress indicators with highlighted current position
- Per-city ambassador progress with motivational hint text describing what each next tier unlocks
- Active cities, recent check-ins, and top cities at a glance
- City detail pages with business maps, check-in history, comments, and tips
- Prestigious ambassador section with gold-accent styling for City Ambassadors
- Featured ambassador rows showing editing powers and check-in stats
- Separator between City Ambassadors and Contributors
- Motivational CTA when no ambassadors exist yet
- City certification levels based on verified business count
Cities earn certification badges based on their verified business count:
| Level | Businesses Required |
|---|---|
| Bronze | 5+ |
| Silver | 20+ |
| Gold | 50+ |
| Platinum | 100+ |
Progress bars show how close a city is to the next certification level.
Eight all-time challenges spanning three difficulty levels:
Easy:
- First Steps: Complete 3 business check-ins
- Neighborhood Scout: Check in at businesses in 2 different cities
Medium:
- Check-in Sprint: Complete 25 check-ins
- Explorer: Check in at businesses in 5 different cities
- Trailblazer: Be the first person to check in at 10 different businesses
- Dedicated Checker: Check in at businesses on 15 different days
Hard:
- Centurion: Complete 100 check-ins
- Sovereign: Reach 100 total check-ins
Completed challenges are displayed as pill badges on user public profiles.
- Businesses sourced exclusively from OpenStreetMap via the Overpass API
- Categories: Restaurant, Cafe, Bar/Brewery, Retail, Services, Hotel/Lodging, Entertainment, Health/Wellness, Education, Automotive, Technology, Other
- Tracks Lightning Network and on-chain Bitcoin acceptance separately
- Extended metadata from OSM: opening hours, cuisine, operator, social contacts, wheelchair access, outdoor seating, takeaway, payment methods, brand, images, descriptions (stored in
osm_tagsJSONB column) - Open/Closed status calculated from OSM opening hours in real time (see Future Optimizations)
- "Edit on OSM" link on each business detail page, directing users to suggest corrections on OpenStreetMap
- SEO-friendly URLs:
/state/city/business-nameformat with auto-generated slugs - Auto-generated descriptions: Every business page has a unique, factual description built from OSM data (name, category, city, state, payment methods, address, hours) via
generateBusinessDescription()with 80+ category display name mappings for grammatical correctness - AI-enhanced descriptions (optional): Richer 150-250 word descriptions generated via OpenAI in bulk from the admin panel, stored in
ai_descriptioncolumn. AI descriptions display on the page when available; auto-generated template used as fallback. Ambassadors cannot edit descriptions — AI generation is admin-only - Schema.org types: Business pages use specific schema types (CafeOrCoffeeShop, Restaurant, BarOrPub, Store, etc.) via
getBusinessSchemaType()mapping 70+ categories instead of generic LocalBusiness - Disclaimer: Bottom of each business page shows OSM data source notice, accuracy warning, and link to edit on OSM or check in
- Import Bitcoin-accepting businesses directly from OpenStreetMap
- Multi-mirror failover (kumi.systems, mail.ru, overpass-api.de) with exponential backoff
- Queries all nodes/ways tagged with
payment:bitcoin=yes, excluding ATMs and vending machines - Automatic city creation and US state normalization during import
- Reverse geocoding via Nominatim for businesses missing address data
- Safe to re-run: duplicates are skipped by OSM ID
Three-provider system for business descriptions, plus city auto-fill:
- OpenAI (GPT-4o-mini): Fast bulk generation of 150-250 word descriptions. Best for quickly filling in content across all businesses. "Missing only" or "Regenerate all" modes with auto-continue
- Perplexity Sonar: Live-web research phase. Searches the web, reviews, Reddit, news, and more to surface real, current data with inline citations. Research is stored in
perplexity_researchcolumn for the writing phase to consume - Claude (Anthropic): Writing and synthesis phase. Produces polished, publication-ready descriptions that synthesize Perplexity research with structured data. Consistent voice, less revision needed. Three modes: "Has research, missing description" (intended workflow), "Missing description (any)", or "Regenerate all"
- City auto-fill: Uses OpenAI to auto-fill city details (population, main industries, description)
All prompts are editable in the admin panel under AI Settings. API keys for all three providers are stored securely in the database via the same settings panel. The platform works fully without AI — auto-generated templates fill in for all content.
- Badge tier with tier-specific icon prominently displayed
- Badge progression ladder showing path to next tier
- Active cities based on check-in history
- Check-in history with business links and vote details
- Vote type breakdown (Lightning/On-chain/Issues/Refused/Closed)
- Community trust system: vouch (public trust signal) and flag (private, sent to mod queue)
- Completed challenges shown as pill badges
- Tip section with QR codes for Lightning/BTC addresses, copy-to-clipboard buttons, deep-linkable via
#tipanchor - Tip pills displayed next to usernames across the platform
- Leaflet.js maps with OpenStreetMap tiles
- City detail pages: businesses plotted with custom Bitcoin markers
- Lightning (yellow) vs. on-chain (orange) marker differentiation
- Verified businesses highlighted with green borders
- Marker clustering on both explore and city maps
- Users can submit support tickets with subject and message
- Threaded conversation between user and moderators
- Ticket status tracking (open, in progress, resolved, closed)
- Accessible from user navigation when logged in
Three-tier role system:
| Role | Capabilities |
|---|---|
| User | Check in at businesses, comment, submit support tickets |
| Moderator | All user actions + manage users (ban/unblock), delete reviews/comments, view/reply support tickets, post mod notes |
| Admin | All moderator actions + assign roles, trigger OSM imports, manage system settings, delete accounts |
- Contributors (10+ check-ins) do NOT have editing powers
- City Ambassador (20+ check-ins): Edit city info and pin tips for their assigned city (within 50 miles)
- State Ambassador (50+ check-ins): Edit across their ambassador states
- Sovereign (100+ check-ins): Nationwide editing powers
- All editing requires active status (no 60-day inactivity gap)
- Edits logged in the
edit_logtable
- Forgot password flow with token-based reset via email
- Rate-limited (5 requests per 60 minutes) to prevent abuse
- Tokens expire after 1 hour
- Constant-time response prevents email enumeration
- "Forgot your password?" link on login page
- Lightweight PHP
mail()wrapper for notifications, email verification, and password reset - Broadcast capabilities with admin controls
- Support ticket notification emails for moderators/admins
- Comprehensive Schema.org JSON-LD structured data on every page type:
- Landing:
WebSite+Organization+FAQPagewithabout,knowsAbout,speakable - City:
Place+BreadcrumbList+FAQPage+ArticlewithcontainsPlace(top 10 businesses asLocalBusiness),containedInPlace,speakable - Business:
LocalBusiness+BreadcrumbList+AggregateRating(from check-in vote data),about,speakable - State:
AdministrativeArea+BreadcrumbListwithcontainedInPlace,about,mentions,speakable - About:
AboutPage+HowTo(4 steps) +Organizationwithabout,mentions - Explore:
WebPagewithabout,mentions,speakable - Profile:
ProfilePage+PersonwithinteractionStatistic,knowsAbout, conditionaljobTitle
- Landing:
- Entity-based E-E-A-T:
about/mentionsproperties link to Wikipedia entities for Bitcoin and Lightning Network viasameAs SpeakableSpecificationon key content selectors for voice assistant citation- Unique title and meta description per page
- Open Graph and Twitter Card meta tags for social sharing
- Canonical URLs on all pages
- XML sitemap at
/sitemap.xml robots.txtwith proper crawl directivesgeo.regionmeta tag for geographic relevance
- Six dropdown FAQ items covering platform overview, check-ins, badges, ambassadors, data sources, and account requirements
- FAQPage schema.org markup for rich search results
- Link to About page for detailed information
- Bitcoin orange (#F7931A) color scheme with supporting gradients
- Dark and light mode toggle with
localStoragepersistence - Mobile-first responsive design
- Inter (body) and Space Grotesk (headings) fonts via Google Fonts
- No build step required
| Component | Technology |
|---|---|
| Backend | PHP 8.0+ with PDO |
| Database | MySQL 5.7+ / MariaDB 10.3+ / PostgreSQL 14+ |
| Frontend | Server-side rendered HTML, CSS, vanilla JavaScript |
| Maps | Leaflet.js + Leaflet.markercluster + OpenStreetMap tiles |
| AI (optional) | OpenAI API, Perplexity Sonar API, Anthropic Claude API |
| Auth | Email/password with bcrypt + PHP sessions |
| QR Codes | qrcode-generator@1.4.4 (CDN) |
No frameworks. No build tools. No npm. Just PHP files you upload and run.
bitcoin-cities/
├── index.php # Main router (API + page routing)
├── .htaccess # Apache rewrite rules + security headers
├── .env.example # Environment configuration template
├── robots.txt # Search engine crawl directives
├── config/
│ ├── database.php # PDO connection + .env file loader
│ └── app.php # App constants (site name, cert levels, categories, states,
│ # AI prompt templates, GA tracking ID)
├── includes/
│ ├── header.php # HTML head, navbar, dark/light toggle, SEO meta
│ ├── footer.php # Footer with nav links, scripts
│ ├── auth.php # Register, login, logout functions (bcrypt)
│ └── functions.php # Helpers: CSRF, UUID, badges, check-in scoring,
│ # certification levels, opening hours parser, haversine distance,
│ # ambassador promotion, tip pills, badge icons,
│ # business description generator, schema type mapper
├── api/
│ ├── auth.php # Login, register, logout, get current user
│ ├── cities.php # City CRUD, search, top cities, state list
│ ├── businesses.php # Business CRUD, explore endpoint
│ ├── verifications.php # Submit votes, get check-in summary
│ ├── challenges.php # Challenges & achievements, progress tracking
│ ├── stats.php # Platform-wide statistics
│ ├── comments.php # City comment CRUD
│ ├── tips.php # Tip system
│ ├── ambassadors.php # Ambassador auto-promotion
│ ├── btcmap_sync.php # OSM Overpass API import with multi-mirror failover
│ ├── geocode.php # Reverse geocoding helper
│ ├── support.php # Support ticket system
│ ├── mod.php # Moderator actions (ban, delete, notes)
│ ├── business_edits.php # Ambassador business editing (hours, phone, website, cuisine)
│ ├── city_edits.php # Ambassador city editing (description, FAQ, content fields)
│ └── admin.php # Admin actions (roles, imports, AI generation, system management)
├── pages/
│ ├── landing.php # Public landing page (hero, stats, how-it-works, FAQ)
│ ├── home.php # Logged-in dashboard (active cities, top cities)
│ ├── explore.php # Full-screen interactive map with filters
│ ├── cities.php # City directory with search, filter, pagination
│ ├── city_detail.php # City profile: map, businesses, ambassadors, check-ins, stats
│ ├── state_detail.php # State profile: cities, stats
│ ├── business_detail.php # Individual business page with check-in UI
│ ├── leaderboard.php # Rankings: cities, people, ambassadors, rising stars
│ ├── challenges.php # Challenges & achievements with progress bars
│ ├── dashboard.php # User dashboard with badge roadmap and city progress
│ ├── profile.php # Edit own profile
│ ├── user_profile.php # Public user profile (badges, activity, challenges, tips)
│ ├── support.php # Support ticket list
│ ├── support_ticket.php # Individual ticket conversation
│ ├── mod_panel.php # Moderator/admin control panel
│ ├── login.php # Login form
│ ├── forgot_password.php # Forgot password form
│ ├── reset_password.php # Reset password form (token-based)
│ ├── register.php # Registration form
│ ├── about.php # About page (how it works, ambassador system, data sources)
│ ├── terms.php # Terms of service
│ ├── privacy.php # Privacy policy
│ ├── sitemap.php # XML sitemap generator
│ └── 404.php # Not found page
├── assets/
│ ├── css/style.css # Full responsive stylesheet (dark/light mode, badge tiers,
│ │ # ambassador sections, badge roadmap)
│ └── js/
│ ├── app.js # Core JS: fetch wrapper, toast, modal, theme toggle, CSRF
│ ├── explore.js # Explore map: markers, clustering, filters, geolocation, drawer
│ └── map.js # City detail map initialization
├── cron/
│ └── sync_osm.php # Cron-compatible OSM sync script (weekly recommended)
└── sql/
├── schema_postgres.sql # PostgreSQL database schema
├── schema_mysql.sql # MySQL database schema
├── migration_v6_business_ai_description.sql # Adds ai_description column to businesses
├── migration_v7_perplexity_research.sql # Adds perplexity_research column to businesses
├── migration_v8_security_hardening.sql # Slug indexes + unique constraints
└── migration_v9_password_reset.sql # Password reset columns on users table
- PHP 8.0 or higher
- MySQL 5.7+ / MariaDB 10.3+ or PostgreSQL 14+
- Apache with
mod_rewriteenabled (standard on most shared hosting) - PHP extensions:
pdo,pdo_mysqlorpdo_pgsql,curl,mbstring,json(all typically included by default)
- Download or clone this repository
- Upload all files to your web root (usually
public_htmlorhtdocs) - Make sure
index.phpand.htaccessare in the root of your web directory
Using a hosting control panel (cPanel, SiteGround, Plesk, etc.):
- Create a new MySQL database (e.g.,
btccities) - Create a database user with a strong password
- Grant the user All Privileges on the database
Using command line (PostgreSQL):
createdb bitcoin_citiesMySQL via phpMyAdmin:
- Open phpMyAdmin from your hosting panel
- Select your database
- Click the Import tab
- Choose
sql/schema_mysql.sql - Click Go
MySQL via command line:
mysql -u your_user -p your_database < sql/schema_mysql.sqlPostgreSQL via command line:
psql -U your_user -d bitcoin_cities -f sql/schema_postgres.sql- Copy
.env.exampleto.env - Edit
.envwith your database credentials:
MySQL (most common for shared hosting):
DATABASE_URL=mysql://your_db_user:your_db_password@localhost/your_db_name
SESSION_SECRET=replace_with_a_long_random_string_at_least_32_charactersPostgreSQL:
DATABASE_URL=postgresql://your_db_user:your_db_password@localhost:5432/your_db_name
SESSION_SECRET=replace_with_a_long_random_string_at_least_32_charactersYou can generate a session secret with:
openssl rand -hex 32The .env file is protected by .htaccess and cannot be accessed from the web.
- Visit your site and click Sign Up to create your account
- Open phpMyAdmin (or your database tool) and find the
userstable - Edit your user row and change the
rolecolumn fromusertoadmin - Save the change
You now have full admin and moderator access. The Mod Panel link will appear in your navigation.
Once logged in as admin:
- Go to the Mod Panel and click the Admin Tools tab
- Click Import US Bitcoin Businesses to pull all businesses tagged with
payment:bitcoin=yesfrom OpenStreetMap - Cities are created automatically during import -- no need to add them manually
- The import takes a few minutes depending on data volume
- Re-running is safe -- businesses already imported (by OSM ID) are skipped
- ATMs and vending machines are excluded automatically
AI powers city auto-fill and a three-provider business description pipeline. API keys can be entered in the Mod Panel > Settings > AI Settings panel (stored securely in the database), or via .env for OpenAI.
OpenAI (fast bulk descriptions + city auto-fill):
- Get an API key from OpenAI
- Enter it in AI Settings or add to
.env:OPENAI_API_KEY=sk-your-key
Perplexity Sonar (live-web research):
- Get an API key from Perplexity
- Enter it in AI Settings
Claude / Anthropic (polished writing):
- Get an API key from Anthropic
- Enter it in AI Settings
Recommended workflow:
- Run Perplexity Research to gather real, cited web data for each business
- Run Claude Descriptions (mode: "Has research, missing description") to synthesize research into polished descriptions
- Or use OpenAI Business Descriptions for faster, simpler generation without the research step
All prompts are editable in AI Settings. The platform works fully without AI — auto-generated templates provide unique content for every page.
To keep business data fresh automatically:
- In your hosting panel (e.g. SiteGround cPanel > Devs > Cron Jobs), add:
php ~/www/bitcoincities.org/public_html/cron/sync_osm.php
- Set to run weekly (recommended)
- Explore the interactive map to find Bitcoin-accepting businesses near you
- Check in at businesses you visit by submitting a vote (Lightning OK, On-chain OK, Issues, Refused, or Closed)
- Earn badges and climb the tiers from Normie to Sovereign
- Become an Ambassador at 20+ check-ins to earn editing powers for your city
- Complete challenges to earn achievement badges on your profile
- Watch your city climb the rankings as more businesses are checked in
- Manage users: ban/unblock accounts, delete inappropriate reviews or comments
- Handle support tickets: respond to user questions and issues
- Post mod notes for internal team communication
- Everything moderators can do, plus:
- Assign roles: promote users to moderator or admin
- Import data: trigger OpenStreetMap business imports
- AI content generation: bulk-generate city and business descriptions via OpenAI
- Generate SEO slugs: backfill URL slugs for businesses, cities, and users
- Fix city slugs: rename duplicate-suffixed slugs (e.g.,
houston-2tohouston) when safe - Normalize states: fix inconsistent state names across the database
- Merge duplicate cities: consolidate cities with matching names in the same state
- Remove non-US data: clean up accidentally imported non-US cities and businesses
- Scan for removed businesses: compare database against OSM to find businesses no longer tagged as accepting Bitcoin
- System management: delete accounts, manage system-wide settings, send broadcasts, configure AI prompts
| Setting | Default | Description |
|---|---|---|
SITE_NAME |
Bitcoin Cities | Shown in navigation, titles, and footer |
SITE_DESCRIPTION |
(see file) | Used in meta tags and structured data |
GA_MEASUREMENT_ID |
G-ZR851PJ8TH | Google Analytics 4 measurement ID |
BTCMAP_CACHE_HOURS |
72 | Hours between OSM re-syncs per city |
CITY_SEARCH_RADIUS_MILES |
30 | Radius for matching businesses to cities |
CERT_LEVELS |
5/20/50/100 | Business counts for Bronze/Silver/Gold/Platinum |
BUSINESS_CATEGORIES |
12 categories | Dropdown options for business submissions |
DEFAULT_AI_BIZ_SYSTEM_PROMPT |
(see file) | System prompt for AI business description generation |
DEFAULT_AI_BIZ_PROMPT |
(see file) | User prompt template for AI business descriptions |
Point your domain's DNS to your hosting provider. The app auto-detects its URL from the HTTP_HOST header -- no hardcoded URLs to change.
If using Nginx instead of Apache, add this to your server block (the .htaccess file is Apache-only):
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ /\.(env|htaccess) {
deny all;
}
location ~ ^/(config|includes|sql)/ {
deny all;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}This project runs on Replit out of the box with PostgreSQL:
- Fork or import the repository
- Replit automatically provisions a PostgreSQL database
- Set a
SESSION_SECRETin the Secrets tab - Run the PostgreSQL schema: connect to the database and execute
sql/schema_postgres.sql - Start the application -- it runs on PHP's built-in development server
The DATABASE_URL, PGHOST, PGDATABASE, PGUSER, PGPASSWORD, and PGPORT environment variables are set automatically by Replit.
- All database queries use PDO prepared statements (SQL injection protection)
- All HTML output is escaped with
htmlspecialchars()(XSS protection) - CSRF tokens required on all POST/PUT/PATCH/DELETE API requests
- Passwords hashed with
bcrypt .envfile blocked from web access via.htaccessconfig/,includes/, andsql/directories blocked from direct web access- Session-based authentication with secure cookie handling
- Business check-ins require 50-mile proximity (location spoofing deterrent)
- 24-hour cooldown on repeat check-ins per business per user
- Role-based access control for all administrative actions
- Tiered editing powers with ambassador table verification and edit logging
All API endpoints are under /api/ and return JSON.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
No | Create account |
| POST | /api/auth/login |
No | Log in |
| POST | /api/auth/logout |
Yes | Log out |
| POST | /api/auth/forgot-password |
No | Request password reset email |
| POST | /api/auth/reset-password |
No | Reset password with token |
| GET | /api/auth/me |
Yes | Get current user |
| GET | /api/cities |
No | List cities (paginated, filterable) |
| GET | /api/cities/top |
No | Top cities by business count |
| GET | /api/cities/states |
No | List of states with cities |
| GET | /api/cities/{id} |
No | Single city details |
| PATCH | /api/cities/{id} |
Amb | Update city description (ambassador+) |
| GET | /api/businesses/explore |
No | Businesses by map bounds with filters |
| GET | /api/businesses/{id} |
No | Single business details |
| GET | /api/verifications/{bizId} |
No | Check-in summary for a business |
| POST | /api/verifications |
Yes | Submit a check-in vote |
| GET | /api/challenges |
No | List challenges and progress |
| GET | /api/stats |
No | Platform statistics |
| POST | /api/support |
Yes | Create support ticket |
| GET | /api/support |
Yes | List user's tickets |
| PATCH | /api/business-edits?business_id=X |
Amb | Edit business details (hours, phone, website, cuisine) |
| POST | /api/admin/generate-slugs |
Admin | Backfill SEO slugs for all records |
| POST | /api/admin/generate-city-descriptions |
Admin | AI-generate city descriptions in bulk |
| POST | /api/admin/generate-business-descriptions |
Admin | AI-generate business descriptions via OpenAI |
| POST | /api/admin/generate-perplexity-research |
Admin | Research businesses via Perplexity Sonar |
| POST | /api/admin/generate-claude-descriptions |
Admin | Generate descriptions via Claude/Anthropic |
| GET | /api/admin/businesses-without-descriptions |
Admin | Count businesses missing AI descriptions |
| POST | /api/admin/normalize-states |
Admin | Fix inconsistent state names |
| POST | /api/admin/merge-duplicate-cities |
Admin | Merge cities with duplicate names |
| POST | /api/admin/cleanup-non-us |
Admin | Remove non-US cities and businesses |
Auth levels: No = public, Yes = logged in, Amb = ambassador or higher, Mod = moderator or admin, Admin = admin only.
- Password reset flow: Full forgot/reset password system with token-based email links, 1-hour expiry, rate limiting (5/60min), and constant-time responses to prevent email enumeration. New pages:
forgot_password.php,reset_password.php. New API endpoints:POST /api/auth/forgot-password,POST /api/auth/reset-password. "Forgot your password?" link added to login page - User deletion cascade: Admin user deletion now properly cleans up all referencing tables:
ambassadors,user_reputation,challenge_progress,user_achievements,support_messages,support_tickets,edit_log,mod_notes(in addition to existingbusiness_verifications,comments,user_activity,tips) - Rising Stars state filter: Leaderboard Rising Stars tab now respects the state dropdown filter (was previously showing all states regardless of selection)
- Migration:
sql/migration_v9_password_reset.sql— addspassword_reset_tokenandpassword_reset_expirescolumns touserstable
- Security hardening for production launch:
- Session cookies now set with
Secure,HttpOnly, andSameSite=Laxflags - XSS fix:
addslashes()replaced withjson_encode()in all JS contexts (support_ticket.php, login.php) - XSS fix: dashboard
$hintTextnow escaped withe() - CSRF fix: reputation voting now uses
apiFetch()instead of rawfetch()(votes were silently failing) - SQL injection prevention:
$scopeColinensureUniqueSlug()now allow-listed - Host header injection:
SITE_URLfallback sanitizesHTTP_HOSTto alphanumeric characters - Rate limiter now fails closed (blocks requests) on database errors instead of allowing them
- Blocked users are now immediately logged out on next request (session destroyed in
currentUser()) - API route matching tightened to exact segment boundaries (prevents prefix collision)
- Cron API files (
cron_merge_cities.php,cron_fix_city_slugs.php) reject direct access outside front controller apiFetch()now handles non-JSON responses gracefully (502s, HTML error pages)- IP spoofing hardened:
getClientIP()walks X-Forwarded-For right-to-left, skipping all private/reserved IPs
- Session cookies now set with
- Merge script improvements:
- MySQL-only
REGEXPreplaced with portableLIKEclauses - No-op
business_verificationsquery removed - All merge operations wrapped in database transactions with rollback on failure
user_activityrecords now moved during merges (previously orphaned)
- MySQL-only
- Performance & reliability:
- Added indexes on
cities.slug,businesses.slug,users.slugfor faster URL routing - Challenges page: N+1 queries eliminated — progress records batch-loaded, same-type calculations cached, unchanged progress skipped
- OSM cron sync: file lock (
flock) prevents overlapping runs
- Added indexes on
- Bug fixes:
daily_streaknow counts actual consecutive days (was counting total distinct days)- Open Now filter: US timezone estimation replaced with proper zone boundaries (Eastern/Central/Mountain/Pacific/Alaska/Hawaii) with DST support
isMobilenow updates on window resize (was set once on page load).envparser now strips surrounding quotes from values- Navbar z-index raised above map controls to prevent overlap
- Migration:
sql/migration_v8_security_hardening.sql— slug indexes + unique constraints
- Three-provider AI business description system: OpenAI (fast bulk), Perplexity Sonar (live-web research with citations), Claude (polished writing that synthesizes research)
- Perplexity research stored in new
perplexity_researchcolumn; Claude and OpenAI write toai_description - All AI prompts (OpenAI, Perplexity, Claude) editable in admin AI Settings panel with "Reset to Defaults" button
- API keys for Perplexity and Anthropic stored in
site_settingsvia admin panel (no.envneeded) - OpenAI business descriptions now have "Missing only" / "Regenerate all" mode dropdown (matching city descriptions)
- Claude has three modes: "Has research, missing description" (intended Perplexity→Claude workflow), "Missing description (any)", "Regenerate all"
- Migration:
sql/migration_v7_perplexity_research.sqladdsperplexity_research TEXTcolumn to businesses
- Landing page FAQ section with six dropdown questions and FAQPage schema.org markup
- About page: new Ambassador System section explaining City/State/Sovereign tiers and editing powers
- About page: updated leaderboard section to document all five tabs
- Leaderboard "Top Contributors" tab renamed to "Top Members"
- Leaderboard empty state text updated to use "check-in" terminology
- Map popup "# Check-ins" button replaced with "View Details" (both explore and city detail maps)
- Map popup meta line separator changed from middot to comma
- Ambassador hours editing: hours input only appears when OSM has no hours; API enforces the same restriction
- Anonymous check-ins: non-logged-in users can submit check-ins with IP-based rate limiting (30-min global, 24-hr per-business)
- FAQ text corrections: inactivity applies to Ambassadors/Sovereigns editing powers (not Contributors), anonymous check-ins acknowledged
- AI-powered business descriptions: bulk-generate 150-250 word unique descriptions via OpenAI from the admin panel, stored in
ai_descriptioncolumn - Auto-generated business descriptions for all pages via
generateBusinessDescription()with 80+ category display name mappings for grammatical correctness (e.g. "other" → "business", "coffee_shop" → "coffee shop", "automotive" → "automotive shop") - Specific Schema.org types per business category via
getBusinessSchemaType()(CafeOrCoffeeShop, Restaurant, BarOrPub, Store, etc. instead of generic LocalBusiness) - Business page disclaimer with OSM data source notice, accuracy warning, and edit-on-OSM link
- Configurable AI prompts for business descriptions (
DEFAULT_AI_BIZ_SYSTEM_PROMPT,DEFAULT_AI_BIZ_PROMPT) inconfig/app.php - Google Analytics 4 integration via
GA_MEASUREMENT_IDconstant - City slug scoping by state to prevent cross-state slug collisions; admin tool to fix existing
-2/-3suffixed slugs - Ambassador business editing limited to hours, phone, website, and cuisine (descriptions are admin-only)
- Redundant cuisine suppression in auto-generated descriptions (e.g. "coffee shop serving coffee shop" no longer occurs)
- Comprehensive Schema.org JSON-LD overhaul across all page types with entity-based
about/mentionsproperties linking to Wikipedia entities (Bitcoin, Lightning Network) - Added
SpeakableSpecificationto key content selectors on every page type for voice assistant citation - City pages embed top 10 businesses as
LocalBusinessviacontainsPlace, withcontainedInPlacegeographic hierarchy - Business pages include
AggregateRatingcomputed from check-in vote data for rich search results - State pages upgraded from
PlacetoAdministrativeAreaschema type - Profile pages include
interactionStatistic(CheckInAction count) and conditionaljobTitlefor badge tier - About page includes
HowToschema with 4 detailed steps for AI model consumption - Organization schema includes
aboutandknowsAboutfor E-E-A-T topical authority - Merchant category chips converted to clickable filter buttons on city detail pages
- Merchant mix summary wrapped in collapsible element
- Comments capped at 1,500 characters (API + frontend enforcement)
- FAQ editor rebuilt as structured Q&A interface with XSS-safe DOM construction
- Nearby cities section on city detail pages (up to 8 cities within 100 miles)
- Rich city content fields (bitcoin_angle, economy_summary, merchant_mix_summary, faq_json) with ambassador editing
- Nostr share buttons on business and city detail pages
- "I was just here" CTA text in map popups
- Pin color by verification freshness (green/yellow/gray) on map markers
- Added Ambassadors tab to leaderboard with Sovereigns, State Ambassadors, and City Ambassadors sections
- Enhanced dashboard with global badge roadmap (Normie through Sovereign) and per-city ambassador progress with motivational hints
- Redesigned city page ambassador section with gold-accent styling, featured ambassador rows, powers descriptions, and separator between ambassadors and contributors
- Added tier-specific Unicode icons to badges for Contributor+ tiers
- Added CSS glow/border effects for higher-tier badges (City Ambassador gold accent, Sovereign purple glow)
- Motivational CTA on city pages when no ambassadors exist
- Replaced reputation system with trust network: vouch (public trust signal) and flag (private, routed to mod queue as mod notes)
- Names link to user profiles across all pages (dashboard, mod panel, leaderboard, city detail)
- Emails no longer exposed on any public-facing pages
- Removed last_name from registration flow and all display contexts
- Tip pill system for Lightning/BTC tipping across the platform
The isOpenNow() function in includes/functions.php currently parses OSM opening_hours strings via regex in PHP on every explore API request when the "Open Now" filter is active. At current scale (~2,300 businesses) this takes single-digit milliseconds and is not a bottleneck. However, at 10,000+ businesses this will become a measurable cost.
Recommended approach when scaling: Add a cached next_close_at / next_open_at timestamp pair to the businesses table, recalculated by a cron job every 15 minutes. The explore API filter then becomes a simple SQL WHERE next_close_at > NOW() instead of fetching all businesses and filtering in PHP. This avoids the complexity of structured day/time columns (OSM hours include seasonal rules, holidays, 24/7, etc.) while moving the computation off the request path entirely.
- Map data and business import data: OpenStreetMap contributors, licensed under the Open Database License (ODbL)
- Maps rendered with Leaflet.js and Leaflet.markercluster
- OpenStreetMap tiles served by the OpenStreetMap Foundation
Copyright (c) 2026 Bitcoin Cities. All rights reserved.
This software is proprietary. No part of this codebase may be reproduced, distributed, or transmitted in any form without the prior written permission of the copyright holder. For licensing inquiries, contact the project owner.