If you run a blog, a content site, or any kind of publication, you know the pain: every post needs tags, categories, and meta keywords. Done manually, it's tedious. Done with an LLM subscription, it's overkill — you're paying a monthly fee for a task that takes milliseconds per page.
This tutorial shows how to build an automatic SEO keyword tagger in Node.js using a pay-per-use keyword extraction API. Total cost: under $0.01 per page. No subscription required.
What we're building
A Node.js script that:
- Reads URLs from a list (or a sitemap)
- Fetches the page text
- Extracts the top 10 keywords via API
- Outputs a JSON file mapping URL → keywords (ready to push to your CMS or meta tags)
The API
We'll use TextAI API — a pay-per-use REST API with USDC micropayments on Solana.
Pricing for keyword extraction: 5 credits per call. 1 USDC = 1,000 credits, so that's $0.005 per call. Under $0.01 per page.
New accounts get 100 free credits instantly — no credit card, just a POST request.
Setup
npm init -y
npm install node-fetch
Get your free API key:
curl -X POST https://textai-api.overtek.deno.net/keys/create
Response:
{
"apiKey": "tai_xxxxxxxxxxxx",
"credits": 100,
"message": "100 free demo credits included"
}
Save the key:
export TEXTAI_API_KEY=tai_xxxxxxxxxxxx
The tagger script
// seo-tagger.js
import fetch from 'node-fetch';
const API_KEY = process.env.TEXTAI_API_KEY;
const API_URL = 'https://textai-api.overtek.deno.net';
// Your pages — swap this for a sitemap parser or CMS API call
const pages = [
{ url: 'https://example.com/blog/how-to-build-rest-apis', text: 'REST APIs are the backbone of modern web apps. In this guide we cover routing, authentication, rate limiting...' },
{ url: 'https://example.com/blog/node-performance-tips', text: 'Node.js performance optimization starts with event loop management. Avoid blocking I/O, use streams...' },
{ url: 'https://example.com/blog/javascript-closures', text: 'A closure is the combination of a function and the lexical environment within which that function was declared...' },
];
async function extractKeywords(text, maxKeywords = 10) {
const response = await fetch(`${API_URL}/keywords`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
},
body: JSON.stringify({ text, max_keywords: maxKeywords }),
});
if (!response.ok) {
const err = await response.json();
throw new Error(`API error: ${err.error || response.status}`);
}
return response.json();
}
async function tagAllPages(pages) {
const results = [];
for (const page of pages) {
try {
const data = await extractKeywords(page.text);
results.push({
url: page.url,
keywords: data.keywords,
credits_used: data.credits_used,
credits_remaining: data.credits_remaining,
});
console.log(`✓ ${page.url} → [${data.keywords.slice(0,5).join(', ')}...]`);
} catch (err) {
console.error(`✗ ${page.url}: ${err.message}`);
results.push({ url: page.url, keywords: [], error: err.message });
}
}
return results;
}
// Run
const tagged = await tagAllPages(pages);
console.log('\n--- Results ---');
console.log(JSON.stringify(tagged, null, 2));
// Total cost estimate
const totalCalls = tagged.filter(r => !r.error).length;
const totalCredits = totalCalls * 5;
console.log(`\nTotal: ${totalCalls} pages, ${totalCredits} credits (~$${(totalCredits / 1000).toFixed(4)} USDC)`);
Run it:
node --experimental-vm-modules seo-tagger.js
Example output:
✓ https://example.com/blog/how-to-build-rest-apis → [REST API, authentication, routing, web apps, rate limiting...]
✓ https://example.com/blog/node-performance-tips → [Node.js, event loop, performance, I/O, streams...]
✓ https://example.com/blog/javascript-closures → [closure, lexical scope, JavaScript, function, environment...]
--- Results ---
[
{
"url": "https://example.com/blog/how-to-build-rest-apis",
"keywords": ["REST API", "authentication", "routing", "web apps", "rate limiting", "HTTP", "JSON", "middleware", "endpoint", "security"],
"credits_used": 5,
"credits_remaining": 95
},
...
]
Total: 3 pages, 15 credits (~$0.0150 USDC)
Push keywords to your CMS
If you're on WordPress, pipe the output to the REST API:
// After tagAllPages(), for each result:
async function updateWordPressTags(postId, keywords, wpApiUrl, wpToken) {
await fetch(`${wpApiUrl}/wp/v2/posts/${postId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${wpToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
meta: { _yoast_wpseo_focuskw: keywords[0], seo_keywords: keywords.join(', ') }
}),
});
}
For Ghost CMS:
async function updateGhostTags(postSlug, keywords, ghostApiUrl, ghostKey) {
const tags = keywords.slice(0, 5).map(k => ({ name: k }));
await fetch(`${ghostApiUrl}/ghost/api/admin/posts/slug/${postSlug}/`, {
method: 'PUT',
headers: { 'Authorization': `Ghost ${ghostKey}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ posts: [{ tags }] }),
});
}
Scale to an entire site
For a 1,000-page blog:
// Add rate limiting to be polite to the API
import { setTimeout as sleep } from 'timers/promises';
async function tagAllPagesThrottled(pages, concurrency = 5, delayMs = 100) {
const results = [];
for (let i = 0; i < pages.length; i += concurrency) {
const batch = pages.slice(i, i + concurrency);
const batchResults = await Promise.all(batch.map(p => extractKeywords(p.text)));
results.push(...batchResults);
await sleep(delayMs); // 100ms between batches
console.log(`Progress: ${Math.min(i + concurrency, pages.length)}/${pages.length}`);
}
return results;
}
Cost for 1,000 pages: 1,000 × 5 credits = 5,000 credits = $5 USDC.
That's less than a single monthly subscription fee, and you only pay when you run it.
Credits and top-up
Check your balance any time:
curl -H "X-API-Key: $TEXTAI_API_KEY" https://textai-api.overtek.deno.net/credits
When you need more credits, send USDC to the wallet address in the response. 1 USDC = 1,000 credits, auto-credited within seconds on Solana devnet.
Why pay-per-use beats subscriptions for this
| Pay-per-use (TextAI) | Subscription LLM | |
|---|---|---|
| 1 page | $0.005 | ~$0.50 (monthly min) |
| 100 pages | $0.50 | ~$0.50 |
| 1,000 pages | $5.00 | ~$0.50–$20 |
| Idle month | $0 | $20–$100 |
The break-even is around 100 pages/month. Below that, pay-per-use wins. Above 10,000 pages/month, a subscription might be cheaper — but you're probably a large publication at that point.
Try it
Free credits, no credit card: https://textai-api.overtek.deno.net
Full API docs at the same URL. The keyword extraction endpoint also returns a relevance score for each keyword, useful for ranking which tags matter most for a given post.
If you build something with this, drop a comment — curious what use cases people find for bulk keyword extraction.
Top comments (0)