Cost

Building a Cost-Monitoring Dashboard

⏱ 17 min

There is no cost dashboard anywhere in the PHP AI Client SDK. There’s no settings page that shows how many tokens a site has used this month, no built-in per-feature cost breakdown, nothing that answers “how much is this feature actually costing us” without you building the answer yourself. generate_text() returns a plain string, not an object carrying token counts or a dollar figure. If you want that number, and at real scale you do, you log every call yourself, to a table you create, and you build the page that reads it back. That’s the entire subject of this lesson: a real, working cost log and a plain wp-admin summary page, built from scratch because nothing ships it.

What you'll learn in this lesson
Why there is no cost data to read off the SDK response
generate_text() returns text, not usage metadata, confirmed by every SDK example so far.
A custom usage-log table via dbDelta
One row per AI call: feature, model, estimated tokens, estimated cost, timestamp.
Estimating tokens and cost without a usage API to read from
A character-based heuristic and a cost-per-model table you maintain.
A summary wp-admin page
Grouped totals by day and by feature, enough to answer "what is this costing us."
Prerequisites

Comfortable calling wp_ai_client_prompt()->generate_text() (Course 3), and basic familiarity with dbDelta() for creating a custom table and $wpdb for querying it.

Step 1: There’s genuinely nothing to read this off of

Every SDK example across this track shows generate_text() returning a plain string. There’s no accompanying usage object, no token count, no cost figure attached to that return value. If you want to know what a call cost, you have to estimate it yourself, at the point where you have both the prompt and the response in hand, since that’s the only place this information can be captured at all.

This is not a shortcoming to work around, it's the honest baseline

Treat this the same way you’d treat the earlier lessons’ “there is no rate limiter” and “there is no observability handler by default”: the SDK’s job is generating text reliably across providers, not billing reconciliation. Cost tracking is legitimately your application’s responsibility to build, the same way it would be if you were calling any model provider’s API directly without a wrapper at all.

Step 2: A custom table to log every call

Create the table once, on plugin activation, with dbDelta():

// File: my-plugin/class-ai-usage-log.php
function my_plugin_create_ai_usage_table(): void {
	global $wpdb;

	$table_name      = $wpdb->prefix . 'ai_usage_log';
	$charset_collate = $wpdb->get_charset_collate();

	$sql = "CREATE TABLE {$table_name} (
		id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
		created_at DATETIME NOT NULL,
		feature VARCHAR(100) NOT NULL,
		model_used VARCHAR(100) NOT NULL,
		estimated_tokens INT UNSIGNED NOT NULL,
		estimated_cost_usd DECIMAL(10,5) NOT NULL,
		PRIMARY KEY  (id),
		KEY feature (feature),
		KEY created_at (created_at)
	) {$charset_collate};";

	require_once ABSPATH . 'wp-admin/includes/upgrade.php';
	dbDelta( $sql );
}

register_activation_hook( __FILE__, 'my_plugin_create_ai_usage_table' );

Step 3: Estimate tokens and cost, and log every call through one wrapper

With no usage figure coming back from the SDK, a character-count heuristic is the practical estimate: roughly four characters per token is a reasonable approximation for English text, close enough for a cost dashboard whose job is showing trends and outliers, not producing an invoice-grade figure.

// File: my-plugin/class-ai-usage-log.php
function my_plugin_model_cost_per_1k_tokens(): array {
	return array(
		'claude-sonnet-4-5'    => 0.006,
		'gemini-3-pro-preview' => 0.005,
		'gpt-5.1'              => 0.007,
		'default'              => 0.006, // fallback if the resolved model isn't in this table
	);
}

function my_plugin_estimate_tokens( string $text ): int {
	return (int) ceil( strlen( $text ) / 4 );
}

function my_plugin_log_ai_usage( string $feature, string $model_used, string $prompt, string $response ): void {
	global $wpdb;

	$tokens   = my_plugin_estimate_tokens( $prompt ) + my_plugin_estimate_tokens( $response );
	$rates    = my_plugin_model_cost_per_1k_tokens();
	$rate     = $rates[ $model_used ] ?? $rates['default'];
	$est_cost = ( $tokens / 1000 ) * $rate;

	$wpdb->insert(
		$wpdb->prefix . 'ai_usage_log',
		array(
			'created_at'          => current_time( 'mysql', true ),
			'feature'              => $feature,
			'model_used'           => $model_used,
			'estimated_tokens'     => $tokens,
			'estimated_cost_usd'   => $est_cost,
		),
		array( '%s', '%s', '%s', '%d', '%f' )
	);
}

Wrap every AI call through a single function that both generates and logs, so no feature can call the model without also being counted:

// File: my-plugin/class-ai-usage-log.php
function my_plugin_generate_text_tracked( string $feature, string $prompt, string $system_instruction, string $model_hint ) {
	$response = wp_ai_client_prompt( $prompt )
		->using_system_instruction( $system_instruction )
		->using_model_preference( $model_hint )
		->generate_text();

	if ( is_wp_error( $response ) ) {
		return $response;
	}

	my_plugin_log_ai_usage( $feature, $model_hint, $prompt, $response );

	return $response;
}
Update the per-model cost table when pricing changes

my_plugin_model_cost_per_1k_tokens() is a static, self-maintained record of what you believe each model costs per 1,000 tokens, it is not read live from any provider. Pricing changes over time and varies by provider; if you don’t keep this table current, every estimate downstream of it drifts quietly out of accuracy. Revisit it on the same cadence you’d review a provider invoice.

Step 4: A summary page in wp-admin

A minimal admin page that groups the log by day and by feature is enough to answer the question this whole lesson exists to answer, add it under Tools:

// File: my-plugin/class-ai-usage-log.php
add_action( 'admin_menu', function () {
	add_management_page(
		'AI Usage & Cost',
		'AI Usage & Cost',
		'manage_options',
		'my-plugin-ai-usage',
		'my_plugin_render_ai_usage_page'
	);
} );

function my_plugin_render_ai_usage_page(): void {
	global $wpdb;
	$table = $wpdb->prefix . 'ai_usage_log';

	$by_feature = $wpdb->get_results(
		"SELECT feature, COUNT(*) AS calls, SUM(estimated_tokens) AS tokens, SUM(estimated_cost_usd) AS cost
		 FROM {$table}
		 WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
		 GROUP BY feature
		 ORDER BY cost DESC"
	);

	echo '<div class="wrap"><h1>AI Usage &amp; Cost (last 30 days)</h1><table class="widefat striped">';
	echo '<thead><tr><th>Feature</th><th>Calls</th><th>Est. Tokens</th><th>Est. Cost (USD)</th></tr></thead><tbody>';

	foreach ( $by_feature as $row ) {
		printf(
			'<tr><td>%s</td><td>%d</td><td>%s</td><td>$%s</td></tr>',
			esc_html( $row->feature ),
			(int) $row->calls,
			esc_html( number_format( (float) $row->tokens ) ),
			esc_html( number_format( (float) $row->cost, 4 ) )
		);
	}

	echo '</tbody></table></div>';
}

This is a plain, direct $wpdb query rendered as an HTML table, deliberately not a charting library or a REST endpoint, since the goal here is a correct, honest number a site owner can actually read, not a polished product.

Test it

Run a few tracked calls through different features, then load the admin page and confirm the totals match:

wp-env run cli wp eval '
my_plugin_generate_text_tracked( "product-copy", "Write a one-line description for a navy wool scarf.", "Be concise.", "claude-sonnet-4-5" );
my_plugin_generate_text_tracked( "post-summary", "Summarize: This is a test post about caching.", "Be concise.", "gemini-3-pro-preview" );
'

Visit Tools > AI Usage & Cost and confirm both features appear with non-zero estimated tokens and cost.

Recap

Nothing in the PHP AI Client SDK exposes token or cost data, generate_text() returns text and nothing else, so a cost dashboard is entirely self-built: a custom table logging feature, model, estimated tokens, and estimated cost per call, a character-count heuristic standing in for a real usage API, and a plain wp-admin page summarizing the last 30 days by feature. Keep the per-model cost table current, since every number downstream depends on it staying accurate.

Resources & further reading

← Cache Invalidation Strategies for AI-Generated Content Model Routing for Cost →