An AI tutor is expensive to run per question, which makes it a natural candidate for a
premium membership tier rather than a blanket feature every member gets. This lesson
puts MemberPress’s own membership check directly inside the tutor ability’s
permission_callback, exactly where an Abilities API permission check belongs, so the
AI feature itself becomes a real, enforced tier boundary.
MemberPress installed and active, with at least two membership products at different
tiers. This lesson builds directly on Lesson 2’s lms-tutor/ask-tutor ability.
Step 1: Confirm MeprUser against your actual installed version first
MemberPress doesn’t publish one single canonical class and method reference the way
LearnDash’s developer site does. The methods below, is_active(),
is_active_on_membership( MeprProduct $prd ), and active_product_subscriptions(), are
real and current as of this writing, but MemberPress ships as a plugin you update
independently, and its internal class shape can change between major versions. Check
wp-content/plugins/memberpress/app/models/MeprUser.php in your actual installed
version before shipping this, rather than trusting this lesson’s signatures blindly on
a version you haven’t looked at.
Step 2: The distinction that actually matters: is_active() vs is_active_on_membership()
// File: lms-tutor/class-tutor-gating.php
$mepr_user = new MeprUser( get_current_user_id() );
// Wrong for tiered gating: true for ANY active membership, regardless of which one.
if ( $mepr_user->is_active() ) { /* ... */ }
// Correct for tiered gating: true only for this specific membership product.
$premium_tutor_product = new MeprProduct( PREMIUM_TUTOR_MEMBERSHIP_ID );
if ( $mepr_user->is_active_on_membership( $premium_tutor_product ) ) { /* ... */ }
is_active() answers “is this account a paying member of anything at all.” A site with
a $9 basic tier and a $49 premium tier that gates the tutor on is_active() alone would
let every paying member in, including the $9 tier the tutor was never meant to reach.
is_active_on_membership() checks against one specific MeprProduct, which is the
actual tier boundary you’re trying to enforce.
Step 3: Wire it into the tutor’s permission_callback
// File: lms-tutor/class-tutor-gating.php
define( 'PREMIUM_TUTOR_MEMBERSHIP_ID', 42 ); // Your actual MemberPress membership product ID.
add_action( 'wp_abilities_api_init', function () {
wp_register_ability(
'lms-tutor/ask-tutor',
array(
'label' => __( 'Ask the course tutor', 'lms-tutor' ),
'input_schema' => array(
'type' => 'object',
'properties' => array(
'lesson_id' => array( 'type' => 'integer' ),
'question' => array( 'type' => 'string' ),
),
'required' => array( 'lesson_id', 'question' ),
),
'permission_callback' => function ( $input ) {
// LearnDash: does this account have access to this course resource at all?
if ( ! sfwd_lms_has_access( $input['lesson_id'], get_current_user_id() ) ) {
return new WP_Error( 'not_enrolled', 'You are not enrolled in this course.', array( 'status' => 403 ) );
}
// MemberPress: is this account on the specific tier that unlocks the AI tutor?
$mepr_user = new MeprUser( get_current_user_id() );
$product = new MeprProduct( PREMIUM_TUTOR_MEMBERSHIP_ID );
if ( ! $mepr_user->is_active_on_membership( $product ) ) {
return new WP_Error( 'not_premium', 'The AI tutor is a premium feature. Upgrade to unlock it.', array( 'status' => 403 ) );
}
return true;
},
'execute_callback' => 'lms_tutor_ask',
)
);
} );
Both checks have to pass. Enrollment answers “does this account belong in this
course.” Membership tier answers “does this account’s plan include the AI tutor
specifically.” Neither one substitutes for the other, and a WP_Error with a specific
reason lets your front end show “upgrade to unlock” instead of a generic denial.
Step 4: A softer tier: limited questions instead of no access
A middle tier might get a capped number of tutor questions per day rather than none at
all, using active_product_subscriptions() to check which tier applies and a transient
as a simple daily counter:
// File: lms-tutor/class-tutor-gating.php
function lms_tutor_check_daily_limit( int $user_id, int $limit ): bool {
$key = "lms_tutor_daily_count_{$user_id}_" . gmdate( 'Y-m-d' );
$count = (int) get_transient( $key );
if ( $count >= $limit ) {
return false;
}
set_transient( $key, $count + 1, DAY_IN_SECONDS );
return true;
}
Called inside the same permission_callback, a mid-tier member gets, say, five
questions a day before hitting a clear limit, while the premium tier’s
is_active_on_membership() check bypasses the counter entirely.
Test it: confirm both denial paths, not just the happy path
wp-env run cli wp eval '
// Test user active on the $9 tier, not the $49 premium tutor tier.
wp_set_current_user( 9 );
print_r( wp_get_ability( "lms-tutor/ask-tutor" )->execute( array( "lesson_id" => 12, "question" => "Explain lesson 3." ) ) );
'
Confirm this returns the not_premium error, not a generated answer, then repeat with a
test user actually active on the premium product ID and confirm the ability executes
normally.
The single most common mistake gating an AI feature by membership is reaching for
is_active() because it reads as “are they a member,” gets the check working against
your own premium test account, and ships it, only to discover every tier, including the
cheapest one, now has the expensive AI feature too. Always gate on
is_active_on_membership() against the specific product ID the feature is meant to
belong to, and test with a low-tier account specifically, not just your own admin or
premium test user.
Recap
permission_callback is exactly where a membership check belongs: it runs on every
call, before execute_callback, with no separate code path to accidentally skip.
is_active_on_membership( MeprProduct $prd ), not is_active(), is the real check for
tiered gating, since is_active() alone treats every paying tier as equivalent.
Combine it with LearnDash’s own sfwd_lms_has_access() enrollment check rather than
replacing it, and confirm MemberPress’s exact class shape against your actual installed
version before relying on it, since no single canonical reference covers every release.
Resources & further reading
- MemberPress, memberpress.com
- Abilities API, developer.wordpress.org
- Building Permission-Gated Tools, WordPress MCP Security & Authentication