Marketplace Leads
A custom WordPress lead marketplace: provider approval, credit ledger, and a REST API. OOP PHP.
by Afnan Abbasi · github.com/afnanabbasi/marketplace-leads · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/afnanabbasi/marketplace-leads/archive/refs/heads/main.zipReadme
Marketplace Leads
A lead marketplace for WordPress, built as a custom plugin: tradespeople (providers) register, get approved by an admin, and spend credits to unlock client leads — with the whole flow driven by a clean REST API.
About this repository. This is a standalone demonstrator. It re-implements the core architecture I designed and shipped for a production lead-marketplace platform (a live client product) — but contains none of that client's code, branding, or data. I built it from scratch to show how I approach WordPress engineering: object-oriented, secured, and structured like software rather than a pile of snippets. The production system is private and client-owned.
What it does
- Provider registration — a logged-in user opts in to become a provider and is granted starter credits, with status set to pending.
- Admin approval workflow — administrators approve or reject providers from a dedicated screen. Only approved providers can unlock leads.
- Credit ledger — an append-only ledger (custom DB table) tracks every credit granted or spent, so a provider's balance is always auditable.
- Lead unlocking — unlocking a lead's contact details debits credits atomically; insufficient balance returns a clean
402error; repeat unlocks are idempotent and never double-charge. - Curated REST API — a versioned API under
marketplace-leads/v1, with explicit permission callbacks instead of exposing the leads through WordPress core endpoints.
Architecture
marketplace-leads/
├── marketplace-leads.php # Bootstrap: constants, autoloader, lifecycle hooks
├── uninstall.php # Teardown on delete (drops table, removes role/options)
├── composer.json # PSR-4 metadata (plugin also ships its own autoloader)
└── src/
├── Plugin.php # Thin coordinator; wires modules to hooks
├── Activator.php # Roles + custom table + default options on activation
├── Deactivator.php # Non-destructive deactivation
├── Roles/Roles.php # Provider role + custom capabilities
├── PostTypes/Lead.php # Lead CPT, meta, and public/contact serializers
├── Providers/ProviderManager.php # Provider state machine (pending → approved)
├── Credits/CreditLedger.php # Append-only ledger over a custom table
├── Rest/RestController.php # Routes, permission callbacks, validation
└── Admin/
├── AdminMenu.php # Top-level menu wiring
├── Settings.php # Settings API (unlock cost, starter credits)
└── ProvidersPage.php # Approve/reject UI (nonce + capability protected)
The design keeps each concern isolated: RestController never touches the database directly — it goes through CreditLedger and ProviderManager, which own their own data. Authorization is expressed as capabilities (ml_unlock_leads) rather than role-name string checks, so it stays explicit and easy to reason about.
Engineering practices shown here
- Object-oriented design with a dependency-light PSR-4 autoloader (runs without
composer install). - Custom roles and capabilities for authorization.
- A custom database table created with
dbDelta, read/written with$wpdb->prepare()and typed insert formats. - A versioned REST API where every write route has a permission callback returning typed
WP_Errors with correct HTTP status codes (401,402,403,404,409). - Input sanitization (
sanitize_text_field,sanitize_email,absint) and output escaping (esc_html,esc_url) throughout. - Admin actions protected by nonces (
check_admin_referer) and capability checks. - Activation/deactivation/uninstall lifecycle handled cleanly and non-destructively.
- Extensibility via action hooks (
ml_provider_registered,ml_provider_approved,ml_provider_rejected) so notifications can be added without editing core classes.
REST API
Base: /wp-json/marketplace-leads/v1
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /leads |
Public | List available leads (no contact details). |
| POST | /leads |
Logged-in | Submit a new lead. |
| POST | /leads/{id}/unlock |
Approved provider | Spend credits to reveal a lead's contacts. |
| POST | /providers/register |
Logged-in | Become a provider (pending) + starter credits. |
| GET | /me |
Logged-in | Current provider status and credit balance. |
Examples
List leads:
curl https://example.com/wp-json/marketplace-leads/v1/leads
Register as a provider (cookie-authenticated request with a REST nonce):
curl -X POST https://example.com/wp-json/marketplace-leads/v1/providers/register \
-H "X-WP-Nonce: <wp_rest_nonce>" --cookie "<wp_auth_cookies>"
Unlock a lead:
curl -X POST https://example.com/wp-json/marketplace-leads/v1/leads/42/unlock \
-H "X-WP-Nonce: <wp_rest_nonce>" --cookie "<wp_auth_cookies>"
# 200 -> { "lead_id": 42, "contact": { "email": "...", "phone": "..." }, "charged": 5, "balance": 15 }
# 402 -> { "code": "ml_insufficient_credits", "data": { "status": 402, "balance": 2, "cost": 5 } }
Installation
- Copy the
marketplace-leadsfolder intowp-content/plugins/. - Activate Marketplace Leads from the Plugins screen. Activation creates the provider role, the ledger table, and default settings.
- Configure unlock cost and starter credits under Marketplace → Settings.
- Approve providers under Marketplace → Providers.
No build step or Composer install is required — the plugin ships its own autoloader. composer.json is included for PSR-4 metadata and tooling.
Possible extensions
This demonstrator deliberately stops at the core. In production you would add: payment top-ups (e.g. Stripe) that credit the ledger, email/WhatsApp notifications on the provider hooks, lead categories/filtering, and a database transaction around the debit to harden against concurrent unlocks.
Author
Afnan Abbasi — Full-Stack Web Developer afnanabbasi.com · LinkedIn
License
MIT — see LICENSE.