OAds v4.0.1
Native ad manager for WordPress. Serve direct ads without sharing revenue with ad networks. Six ad types, full targeting, A/B rotation, impression and click tracking, and revenue analytics.
What OAds does
OAds is a native ad manager for direct ad sales. It manages ads you sell directly to advertisers and keeps 100% of the revenue.
| Capability | Detail |
|---|---|
| Ad types | 6 types: image banner, text card, HTML embed, video, sponsored content card, sticky bar |
| Targeting | Device, user role, category, country (geo-IP), frequency cap, date scheduling |
| Rotation | Ad groups with random (weighted), round-robin, and A/B split-test rotation |
| Analytics | Impressions, clicks, CTR, unique IPs, estimated revenue (CPM + CPC) |
| Zones | Named placement areas with dimensions, fill priority, and AdSense fallback |
| Privacy | GDPR consent gate, frequency capping via localStorage, ad disclosure label |
| Placement | Shortcode, PHP template functions, WP action hook, WP widget, Gutenberg block, Elementor widget, auto-injection |
| Advertiser portal | Self-serve submission form, advertiser dashboard, admin approve/reject queue, WooCommerce wiring |
| Sellable plans | Pre-priced packages with impression caps, allowed sections and ad types, WooCommerce auto-activation |
| Header bidding | Optional Prebid.js integration with self-hosted build, per-bidder config, price floor, timeout |
| Surfaces | REST API (oads/v1), full WP-CLI command surface, multisite network defaults |
| Admin UI | Orravo design system: dark/light, sticky header, matches OMailer/OForum |
Getting installed
oads/ folder to wp-content/plugins/.oads_ad custom post type, adds oads_settings option with defaults, registers the click-tracking rewrite rule (/oads-click/{id}/), and flushes rewrite rules.Admin interface
Follows the exact Orravo design system, with a sticky 2-row header positioned below the WP admin bar at top: 32px. Theme preference stored in localStorage under oads_theme.
| Tab | Description |
|---|---|
| Manage Ads | Create, edit, pause, duplicate, bulk-edit, delete, preview ads |
| Analytics | Impressions/clicks/CTR/revenue charts, video event funnel, per-ad table, section matrix |
| Zones | Define named placement zones with dimensions, fill priority, AdSense fallback, prebid sizes |
| Ad Groups | Set up rotation pools and A/B split tests |
| Portal | Review advertiser submissions, approve to publish, reject with reason |
| Plans | Manage sellable plans (price, impression cap, duration, allowed sections, WooCommerce wiring) |
| Tracking Log | Raw impression and click log with CSV export |
| Settings | GDPR, disclosure label, frequency cap, lazy delivery, AdSense fallback, prebid header bidding, data retention, REST API key |
Six ad types
wp_kses_post. Use for rich media or custom units.Ad meta keys
| Meta key | Used by | Description |
|---|---|---|
_oads_image_url | image | Image URL |
_oads_image_id | image | WP attachment ID |
_oads_text_headline | image, text | Headline / overlay headline |
_oads_text_body | text | Body copy |
_oads_text_cta | text | CTA button label (default: "Learn More") |
_oads_destination_url | image, text | Click destination URL |
_oads_html_embed | html | Raw HTML content |
_oads_video_url | video | MP4 URL or YouTube URL |
_oads_video_type | video | self or youtube |
_oads_sponsored_headline | sponsored | Card headline |
_oads_sponsored_desc | sponsored | Card description |
_oads_sponsored_cta | sponsored | CTA label |
_oads_sponsored_img_url | sponsored | Card image URL |
_oads_sticky_position | sticky | top or bottom |
Targeting system
Targeting is evaluated server-side in OAds_CPT::passes_targeting() before an ad is served. Six independent targeting dimensions, all optional and additive.
Device targeting (_oads_target_device)
| Value | Behaviour |
|---|---|
both | All devices (default) |
mobile | Mobile only (uses wp_is_mobile()) |
desktop | Desktop only |
User role targeting (_oads_target_roles)
Comma-separated role slugs. Special values: logged_in (any authenticated user), logged_out (unauthenticated visitors), or any WP role slug such as subscriber, editor. Example: logged_in, subscriber
Category targeting (_oads_target_categories)
Comma-separated category IDs. Ad shows only when the current page belongs to one of those categories. Works on singular posts and category archive pages. Example: 3, 7, 12
Country targeting (_oads_target_countries)
Comma-separated ISO 3166-1 alpha-2 codes. Requires a geo-IP lookup at the theme level. OAds reads the constant or option OADS_VISITOR_COUNTRY. Leave blank to show to all countries.
Frequency cap (_oads_freq_cap)
Maximum impressions per user per day. Implemented in JavaScript using localStorage (key: oads_fc_{ad_id}). Set to 0 to disable.
Date scheduling
| Meta key | Format | Description |
|---|---|---|
_oads_start_date | YYYY-MM-DD | Ad won't show before this date |
_oads_end_date | YYYY-MM-DD | Ad won't show after this date |
Placement & shortcodes
Shortcodes
OAds registers four shortcodes covering ad delivery, the advertiser self-serve portal, the advertiser dashboard, and the sellable plans grid.
SHORTCODE[oads]
[oads section="blog" count="2"]
[oads type="inline" section="shop"]
[oads zone="sidebar"]
[oads placement="sidebar" section="global" count="1"]
[oads_portal] // advertiser submission form
[oads_portal_dashboard] // logged-in advertiser dashboard (their submissions + status)
[oads_plans] // sellable plans pricing grid
| Shortcode | Use |
|---|---|
[oads] | Render an ad in any post, page, widget, or block. |
[oads_portal] | Drop on a sales page so advertisers can submit creative, sections, budget, and dates. Submissions land in wp_oads_submissions for admin review. |
[oads_portal_dashboard] | Logged-in advertiser dashboard listing their own submissions and statuses. |
[oads_plans] | Renders a pricing grid from wp_oads_plans. Wires WooCommerce product IDs (auto-activates the plan on order completion) or routes to a custom CTA URL. |
[oads] attributes
| Attribute | Default | Options |
|---|---|---|
section | global | Any section slug or zone slug |
count | 1 | Integer (capped at 5) |
type | card | card, inline |
zone | (none) | Named zone slug. When set, uses the zone's AdSense fallback if no direct ads are eligible. |
lazy | setting | '0' / '1'. Inherits the global lazy_load setting. |
PHP template functions
PHP// Show 1 card ad in blog section
oads_show( 'blog', 1 );
// Show 2 inline ads
oads_show_inline( 'global', 2 );
// Get raw HTML
$html = oads_get( 'homepage', 1 );
// Inject an ad every 6 items in a WP_Query loop
foreach ( $items as $i => $item ) {
// … render item …
oads_inject_in_loop( 'blog', 6, $i );
}
// Render a named zone
oads_zone( 'sidebar-top', 'card' );
WP action hook
PHPdo_action( 'oads_zone', 'blog', 'inline' );
Automatic injection
OAds auto-injects ads without any template code, and respects a cap of 3 ads per page. Ads in header and footer elements are automatically hidden via a <style> injection.
| Context | How ads are injected |
|---|---|
| Singular posts | After paragraph 3 and paragraph 7 (if post is long enough) |
| Archive / listing pages | Card ads inserted into the largest CSS Grid on the page, every N items |
| Sticky bar ads | Injected into <body> via wp_footer |
Ad groups & rotation
Pool multiple ads together and serve them according to a rotation policy. Set the Ad Group field on each ad to assign it to a pool.
_oads_weight value (1–10). A weight of 3 is 3× more likely than weight 1.oads_group_rr_pointers option.maybe_pick_winner() then compares CTR and locks in the winner exclusively.Setting up a group
[oads] normally. The group's rotation logic picks which ad to serve.Ad zones
Named, configurable placement areas. Instead of hardcoding section names in templates, zones let you define dimensions, fill priority, and an AdSense fallback per zone.
| Field | Description |
|---|---|
| Name | Human-readable label |
| Slug | Used in shortcode: [oads zone="slug"] |
| Max Width / Height | Informational dimensions constraint |
| Fill Priority | direct (sold ads) → house (promotional) → remnant (fallback) |
| AdSense Fallback Code | Shown when no direct ad fills the zone |
SHORTCODE[oads zone="sidebar-top"]
PHPoads_zone( 'sidebar-top', 'card' );
// or via action hook:
do_action( 'oads_zone', 'sidebar-top', 'inline' );
Analytics & revenue
Dashboard metrics
| Metric | How calculated |
|---|---|
| Impressions | Unique page views where an ad was observed (IntersectionObserver ≥ 50% threshold) |
| Clicks | Tracked via server-side redirect through /oads-click/{id}/ |
| CTR | (clicks / impressions) × 100 |
| Unique IPs | Distinct visitor IPs in the selected period |
| Est. Revenue | CPM + CPC rates set per ad (see formula below) |
Revenue formula
Set CPM and/or CPC rates per ad in the ad editor. Total revenue is the sum across all ads for the selected period.
Time periods
| Key | Range |
|---|---|
7d | Last 7 days |
30d | Last 30 days |
90d | Last 90 days |
all | Since 2020-01-01 |
CSV export
Click Export Impressions CSV or Export Clicks CSV on the Analytics or Log tab. AJAX action: oads_export_csv. Parameters: type (impressions|clicks), period (7d|30d|90d|all).
WP widget
OAds registers a WP Widget, OAds Ad Zone, for placing ads in sidebar widget areas.
| Field | Default | Description |
|---|---|---|
| Title | (blank) | Widget title shown above ads |
| Section | sidebar | Which ad section to pull from |
| Number of ads | 1 | 1–5 |
| Format | card | card or inline |
Privacy & GDPR
Consent gate
Enable Settings → Privacy & GDPR → Require consent before tracking. The frontend JS checks localStorage.getItem('oads_consent') === '1' before firing any impression or click tracking. If consent is absent, the ad still displays but no data is recorded.
Your CMP (cookie banner) should set this when the user consents:
JSlocalStorage.setItem('oads_consent', '1');
Data stored
| Table | Data recorded |
|---|---|
wp_oads_impressions | ad_id, page_url, section, visitor_ip, user_agent, timestamp |
wp_oads_clicks | ad_id, page_url, section, visitor_ip, user_agent, referrer, timestamp |
oads_before_impression action or by forking the tracker class.Ad disclosure label
Every ad carries a disclosure label (default: Sponsored). Override per-ad in the editor. Ensures compliance with FTC guidelines and similar requirements.
Database schema
7 custom tables created on activation, all properly indexed for the common queries (per-ad, per-section, per-day rollups).
wp_oads_impressions
SQLid BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
ad_id BIGINT UNSIGNED NOT NULL
page_url VARCHAR(500)
section VARCHAR(100)
visitor_ip VARCHAR(45)
user_agent VARCHAR(500)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
INDEX idx_ad_id (ad_id)
INDEX idx_created (created_at)
INDEX idx_section (section)
wp_oads_clicks
SQLid BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
ad_id BIGINT UNSIGNED NOT NULL
page_url VARCHAR(500)
section VARCHAR(100)
visitor_ip VARCHAR(45)
user_agent VARCHAR(500)
referrer VARCHAR(500)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
INDEX idx_ad_id (ad_id)
INDEX idx_created (created_at)
wp_oads_zones
SQLid BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
name VARCHAR(100)
slug VARCHAR(100) UNIQUE
max_width INT UNSIGNED
max_height INT UNSIGNED
fill_priority ENUM('direct','house','remnant')
adsense_code TEXT
prebid_enabled TINYINT(1) DEFAULT 0
prebid_sizes VARCHAR(200) DEFAULT '300x250'
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
wp_oads_groups
SQLid BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
name VARCHAR(100)
rotation_type ENUM('random','roundrobin','ab')
ab_winner BIGINT UNSIGNED DEFAULT 0
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
wp_oads_video_events
SQLid BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
ad_id BIGINT UNSIGNED NOT NULL
event_type VARCHAR(20) /* start, q1, mid, q3, end */
visitor_ip VARCHAR(45)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
INDEX idx_ad_id (ad_id)
INDEX idx_event (event_type)
INDEX idx_created (created_at)
wp_oads_submissions
SQLid BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
name VARCHAR(200)
email VARCHAR(200)
company VARCHAR(200)
website VARCHAR(500)
ad_type VARCHAR(50)
section VARCHAR(100)
headline VARCHAR(300)
body TEXT
destination_url VARCHAR(500)
image_url VARCHAR(500)
budget DECIMAL(10,2)
preferred_start DATE
notes TEXT
status VARCHAR(20) DEFAULT 'pending'
post_id BIGINT UNSIGNED DEFAULT 0
reject_reason TEXT
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
INDEX idx_email (email)
INDEX idx_status (status)
INDEX idx_created (created_at)
wp_oads_plans
SQLid BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
name VARCHAR(200)
description TEXT
price DECIMAL(10,2)
impressions BIGINT UNSIGNED
duration_days INT UNSIGNED DEFAULT 30
ad_types VARCHAR(200)
sections VARCHAR(200)
features TEXT
cta_label VARCHAR(100) DEFAULT 'Get Started'
cta_url VARCHAR(500)
wc_product_id BIGINT UNSIGNED DEFAULT 0
is_featured TINYINT(1) DEFAULT 0
active TINYINT(1) DEFAULT 1
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
PHP API reference
OAds_CPT
PHP// Get active ads for a section
OAds_CPT::get_ads( string $section, int $limit, string $placement, array $ctx ): array
// Get all meta for an ad
OAds_CPT::get_ad_data( int $ad_id ): array
// Targeting check: returns false if ad should not be shown
OAds_CPT::passes_targeting( int $ad_id, array $ctx ): bool
OAds_Display
PHPOAds_Display::get_ads_html( string $section, int $count ): string
OAds_Display::get_inline_ads_html( string $section, int $count ): string
OAds_Display::render_ad( WP_Post $ad, string $section, string $format ): string
OAds_Display::detect_section(): string
OAds_Tracker
PHPOAds_Tracker::record_impression( int $ad_id, string $page_url, string $section ): void
OAds_Tracker::record_click( int $ad_id ): void
// Per-ad stats: returns [ impressions, clicks, ctr, revenue ]
OAds_Tracker::get_ad_stats( int $ad_id, string $period ): array
// Overview stats for all ads
OAds_Tracker::get_overview_stats( string $period ): array
// Raw log entries
OAds_Tracker::get_recent_log( int $limit, string $type ): array
// CSV export (exits)
OAds_Tracker::export_csv( string $type, string $period ): never
OAds_Zones
PHPOAds_Zones::get_all(): array
OAds_Zones::get_by_id( int $id ): ?object
OAds_Zones::get_by_slug( string $slug ): ?object
OAds_Zones::save( array $data ): int|false
OAds_Zones::delete( int $id ): bool
OAds_Groups
PHPOAds_Groups::get_all(): array
OAds_Groups::get_by_id( int $id ): ?object
OAds_Groups::get_ads_in_group( int $group_id ): array
OAds_Groups::pick_ad( int $group_id ): ?WP_Post
OAds_Groups::maybe_pick_winner( int $group_id ): void
OAds_Groups::save( array $data ): int|false
OAds_Groups::delete( int $id ): bool
Hooks & filters
Actions
| Hook | Args | Description |
|---|---|---|
oads_zone | string $zone_slug, string $format | Render an ad zone. Call via do_action('oads_zone', 'sidebar', 'card') or use the oads_zone() helper. |
oads_before_impression | int $ad_id, string $page_url, string $section | Fires immediately before an impression row is inserted. Hook to short-circuit, anonymize, or fan out to your own analytics. |
oads_after_click | int $ad_id, string $destination_url | Fires after a click is recorded, immediately before the 302 redirect. |
oads_portal_submission_received | array $row | Fires after a new advertiser submission row is inserted via the portal. |
oads_submission_approved | int $ad_id, array $sub | Fires when an admin approves a submission and a corresponding oads_ad post is created. |
oads_submission_rejected | int $submission_id, array $sub | Fires when an admin rejects a submission. |
oads_data_retention | (none) | Weekly cron event that calls OAds_Tracker::purge_old_data(). Replace with your own callback to customize purge logic. |
Filters
| Filter | Args | Description |
|---|---|---|
oads_candidate_ads | WP_Post[] $ads, string $section, array $context | Modify the candidate pool of ads after targeting and yield ranking, before delivery. |
oads_targeting_result | bool $passes, int $ad_id, array $ctx | Override whether a given ad passes targeting for the current request. |
oads_render_ad | string $html, WP_Post $ad, string $format | Override or extend the rendered ad HTML for any display format. |
oads_setting | mixed $value, string $key, mixed $default | Filter any resolved oads_settings value. Used by the multisite module to apply network-level defaults. |
REST API
All endpoints live under the oads/v1 namespace. Auth accepts a Bearer API key (set api_key in settings) or any logged-in user with manage_options.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /oads/v1/stats?period=30d | Overview metrics: impressions, clicks, CTR, unique visitors, revenue. |
GET | /oads/v1/ads | List all ads with their meta (up to 100). |
GET | /oads/v1/ads/{id}/stats?period=30d | Per-ad stats for a given period. |
GET | /oads/v1/ads/{id}/video-stats?period=30d | Per-ad video event funnel (start, q1, mid, q3, end). |
GET | /oads/v1/analytics/section-matrix?period=30d | Section-by-ad-type performance matrix. |
Period values: 1d, 7d, 30d, 90d, all.
Auth example
SHcurl -H "Authorization: Bearer ${OADS_API_KEY}" \
https://your-site.com/wp-json/oads/v1/stats?period=7d
Settings reference
Settings stored in get_option('oads_settings'). Access via helper: oads_setting( 'key', $default ).
| Key | Type | Default | Description |
|---|---|---|---|
gdpr_consent | '0' / '1' | '0' | Require localStorage consent before tracking |
gdpr_gpc | '0' / '1' | '1' | Honor the browser Global Privacy Control signal |
ccpa_enabled | '0' / '1' | '0' | Surface a "Do Not Sell" link for California visitors |
ip_anonymize | '0' / '1' | '0' | Hash visitor IP before storage (GDPR-friendly) |
data_retention_days | int | 180 | Weekly cron purges impression and click rows older than this |
disclosure_label | string | 'Sponsored' | Default ad label text shown on every ad |
freq_cap_default | int | 0 | Global default frequency cap (0 = off) |
lazy_load | '0' / '1' | '1' | Defer below-the-fold zone rendering until scrolled into view |
adsense_pub_id | string | '' | AdSense publisher ID for zone fallbacks |
adsense_slot_id | string | '' | Default AdSense slot ID |
api_key | string | '' | Bearer token for the REST API. When set, requests must send Authorization: Bearer {key}. |
prebid_enabled | '0' / '1' | '0' | Enable header bidding via Prebid.js |
prebid_url | string | '' | Self-hosted Prebid.js URL. Module refuses to load until set, with a clear admin notice. |
prebid_timeout | int | 1500 | Prebid auction timeout in milliseconds |
prebid_price_floor | float | 0.50 | Minimum CPM floor for accepting prebid bids |
prebid_bidders | JSON string | '[]' | List of bidder codes participating in auctions |
prebid_bidder_config | JSON string | '[]' | Per-bidder parameters keyed by bidder code |
portal_notify_email | string | admin email | Address that receives advertiser portal submission notifications |
Clean uninstall
Deactivating preserves all data. Deleting the plugin via WP admin runs the uninstall hook and removes everything permanently.
wp_oads_impressionswp_oads_clickswp_oads_zoneswp_oads_groupswp_oads_video_eventswp_oads_submissionswp_oads_plansoads_settings and oads_group_rr_pointers optionsoads_ad posts and their post metaHow OAds stacks up
| Feature | OAds | Advanced Ads | AdSanity | Ad Inserter |
|---|---|---|---|---|
| Internal ad CPT | ✓ | ✓ | ✓ | - |
| 6 ad types | ✓ | partial | - | - |
| Impression/click tracking | ✓ | paid add-on | ✓ | - |
| Revenue tracking (CPM/CPC) | ✓ | - | - | - |
| A/B split auto-optimize | ✓ | paid add-on | - | - |
| Ad group rotation | ✓ | ✓ | - | - |
| Category targeting | ✓ | paid add-on | - | ✓ |
| Device targeting | ✓ | paid add-on | - | ✓ |
| Role targeting | ✓ | paid add-on | - | ✓ |
| Geo targeting hook | ✓ | paid add-on | - | ✓ |
| Frequency capping | ✓ | paid add-on | - | paid |
| Named zones | ✓ | ✓ | - | - |
| Analytics chart | ✓ | - | basic | - |
| CSV export | ✓ | - | - | - |
| GDPR consent gate | ✓ | partial | - | - |
| WP Widget | ✓ | ✓ | ✓ | ✓ |
| Modern admin UI | ✓ | - | partial | - |
| Single price, no add-ons | ✓ | - | - | partial |
Got a question about OAds?
Reach out directly. Kenneth replies within 24 hours.

