Publish a Paid MCP Server
Charge a one-time fee for access to your MCP server. AI Coach handles checkout, license issuance, and revocation — your server stays on your infrastructure and verifies tokens locally with a per-server HS256 secret. No proxy, no per-request latency tax.
How it works
- You register your MCP server on AI Coach and set a price.
- A buyer pays via Stripe Checkout. AI Coach mints a JWT bound to
(buyerId, serverId, purchaseId). - Buyer pastes your server URL + the JWT into their MCP client.
- Your server verifies the JWT against the HS256 secret AI Coach gave you. No callback per request.
- Refunds or buyer-requested regenerates show up on the revocation feed; you poll every few minutes and refuse those
jtis.
AI Coach is not on the request path. That keeps your latency budget intact and means SSE / streamable-http / stdio all work — the JWT doesn't care about the transport.
Prerequisites
- Publisher mode enabled on your AI Coach account.
- Your MCP server already registered: see Publish an MCP Server for the free-registry flow. Note
$SERVER_ID— you'll need it. - API key with
publisher:writescope. - A way to receive HTTP requests with a Bearer token header (any MCP server you build will already have this).
Step 1 — Set pricing
Patch your server record with pricingModel: 'paid' and a price in cents. Use the existing publisher MCP routes — pricing fields live on mcp_servers.
curl -X PATCH https://aicoach.pw/api/publisher/mcp-servers/$SERVER_ID \
-H "Authorization: Bearer mcp_xxx" \
-d '{ "pricingModel": "paid", "pricingDetails": "$9.99 one-time" }' Step 2 — Fetch your signing secret
GET this endpoint to provision (or retrieve) the active HS256 key for your server. The secret is shown each time you GET — but treat it like any other API secret and store it server-side. Never ship it to clients.
curl https://aicoach.pw/api/publisher/mcp/$SERVER_ID/signing-secret \
-H "Authorization: Bearer mcp_xxx" {
"keyId": "9b7f…",
"keyVersion": 1,
"algorithm": "HS256",
"signingSecret": "Wv3J…", // store as a server-side secret
"kid": "<serverId>:1",
"verifyHint": "JWT kid header is `<serverId>:<keyVersion>`; verify with HS256."
}
The kid field tells your verifier which key version signed the token. After rotation (step 5) you'll verify both old and new kids until existing tokens expire.
Step 3 — Verify tokens in your server
The JWT arrives in the Authorization: Bearer … header. Validate it with any HS256 JWT library. The example uses jsonwebtoken for Node; the field names are identical in any language.
import jwt from 'jsonwebtoken';
const SIGNING_SECRET = process.env.AICOACH_SIGNING_SECRET!; // from /signing-secret
const SERVER_ID = process.env.AICOACH_SERVER_ID!; // your mcp_servers.id
/**
* Express-ish middleware. Drop into any MCP server transport.
* Returns 401 with a typed reason on failure; sets req.aicoach on success.
*/
export function verifyLicense(req, res, next) {
const header = req.get('authorization') ?? '';
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
if (!token) return res.status(401).json({ error: 'missing_token' });
try {
const claims = jwt.verify(token, SIGNING_SECRET, {
algorithms: ['HS256'],
issuer: 'aicoach.pw',
audience: `mcp_server:${SERVER_ID}`,
});
if (claims.serverId !== SERVER_ID) {
return res.status(401).json({ error: 'server_mismatch' });
}
if (revokedJtis.has(claims.jti)) {
return res.status(401).json({ error: 'revoked' });
}
req.aicoach = claims;
next();
} catch (e) {
return res.status(401).json({ error: 'invalid_token', reason: e.message });
}
} See License Tokens reference for the full claim set and error codes.
Alternative: remote verification
If you'd rather not embed the secret in your server, AI Coach can verify on your behalf. One extra network hop per call — fine for low-volume servers.
// If you don't want to embed the secret, let AI Coach verify on your behalf.
async function verifyRemote(token, serverId) {
const r = await fetch('https://aicoach.pw/api/mcp/licenses/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, serverId }),
});
const body = await r.json();
return body.ok ? body.claims : null;
} Step 4 — Poll the revocation feed
Refunds, regenerates, and admin revocations all land here. Poll every 5 minutes (or subscribe to the cursor for low-latency setups). Maintain an in-memory set of jtis and refuse tokens whose JTI is in the set until the JTI's expiresAt passes.
// Poll every 5 min. Cache revoked jtis until each token's exp.
let cursor = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const revokedJtis = new Map(); // jti -> expiresAt
setInterval(async () => {
const url = new URL('https://aicoach.pw/api/mcp/licenses/revoked');
url.searchParams.set('since', cursor);
url.searchParams.set('serverId', process.env.AICOACH_SERVER_ID);
const r = await fetch(url);
const { revocations, nextCursor } = await r.json();
for (const rev of revocations) {
revokedJtis.set(rev.id, rev.expiresAt);
}
// Drop entries past their exp.
const now = new Date().toISOString();
for (const [jti, exp] of revokedJtis) if (exp < now) revokedJtis.delete(jti);
cursor = nextCursor ?? new Date().toISOString();
}, 5 * 60 * 1000); Step 5 — Rotate your signing key (quarterly)
POST the same endpoint to rotate. The old key stays alive (its isActive=0, no retiredAt) so already-issued tokens still verify. Your server should accept both old and new kids during the overlap window — typically up to 1 year (the default token TTL).
curl -X POST https://aicoach.pw/api/publisher/mcp/$SERVER_ID/signing-secret \
-H "Authorization: Bearer mcp_xxx"
Update your config with the new secret before any new tokens get minted. AI Coach starts signing with the new kid immediately on rotate, so don't leave the old config alone for hours.
What you get + what AI Coach keeps
- Revenue split: 60% publisher / 40% platform, cached per purchase at insert time.
- USD only in v1. Stripe handles FX for non-USD cards.
- Payouts in v1 are manual — contact support once your wallet crosses your payout threshold.
- Refunds are buyer-initiated within 7 days (manual after); the revocation feed handles the corresponding token kill.
Caveats + open items
- No public JWKS for license tokens — HS256 secrets are per-server. If you fork your verifier across multiple instances, share the secret over a vault, not a public URL.
- Buyer-side install UI (
/account/purchases) ships in a follow-up. Until then, buyers fetch their license viaPOST /api/account/purchases/:id/regenerate-license. - Sample
@aicoach/publisher-sdkwith a one-lineverifyLicense()helper is on the roadmap — for now hand-roll with your JWT lib of choice.
Next
- License tokens reference — JWT claims, error codes, revocation feed schema.
- Publish your first skill — for the simpler "skill bundle" flow.