Charge for your MCP server, and stop each account at what it paid for.
For MCP serversSign-in that a strict MCP client accepts, a token check on each call, and a credit balance that refuses the call when it reaches zero. Your server keeps its own code, and Rekey keeps the accounts and the balances.
Where it usually goes wrong
The situation“The MCP client will not connect and it will not tell me which part it hates.”
Before it shows a consent screen, a strict client fetches authorization server metadata, often at a URL built by inserting the well-known segment after the origin, registers itself, and insists on PKCE. Miss one document and it fails without naming which.
“The agent burned through the plan's credits and nothing stopped it.”
An agent calls tools in a loop. Usage counted now and billed at the end of the month turns into either an invoice your customer argues with or a cost you absorb. The check has to happen before the tool runs.
Three steps, with the real calls
The setupThe samples use Express and @rekey.dev/node. The Rekey side is the same from any framework.
Point clients at the authorization server
Turn on MCP for your Application in the panel. Rekey then serves the OAuth 2.1 authorization server at https://api.rekey.dev/api/v1/mcp/your-app: metadata in both URL forms, dynamic client registration, and PKCE with S256 only. Your server says where it is:
// Your MCP server, e.g. https://tools.example.com/mcp
const RESOURCE_METADATA =
"https://tools.example.com/.well-known/oauth-protected-resource";
app.get("/.well-known/oauth-protected-resource", (_req, res) => {
res.json({
resource: "https://tools.example.com/mcp",
authorization_servers: ["https://api.rekey.dev/api/v1/mcp/your-app"],
scopes_supported: ["mcp:account"],
bearer_methods_supported: ["header"],
});
});
// A call with no valid token gets a 401 that points at the document above.
function unauthorized(res) {
res
.status(401)
.set("WWW-Authenticate", `Bearer resource_metadata="${RESOURCE_METADATA}"`)
.end();
}https://api.rekey.dev/api/v1/mcp/your-app/.well-known/oauth-authorization-server https://api.rekey.dev/.well-known/oauth-authorization-server/api/v1/mcp/your-app
Check the token on each call
rekey.mcp.introspect() asks Rekey whether the token is live (RFC 7662), using your secret key. An expired token, one issued before its user signed out everywhere, or one whose user was erased reads as active: false. sub is the end user's id.
import { Rekey, RekeyError } from "@rekey.dev/node";
const rekey = new Rekey({
apiUrl: process.env.REKEY_URL, // https://api.rekey.dev
secretKey: process.env.REKEY_SECRET,
});
// Returns the end user's id, or null when the token is not good.
async function whoIsCalling(req) {
const header = req.headers.authorization ?? "";
if (!header.startsWith("Bearer ")) return null;
const result = await rekey.mcp.introspect(header.slice(7));
return result.active ? result.sub : null;
}Spend credits, and stop at zero
Sell a credit pack as a plan through your own Stripe, PayPal or Razorpay account, and a successful payment adds its credits to the buyer's balance. Each tool call then draws one down with rekey.credits.consume(), which is POST /api/v1/credits/consume. The debit is atomic, so two calls racing for the last credit cannot both win.
app.post("/mcp", async (req, res) => {
const endUserId = await whoIsCalling(req);
if (!endUserId) return unauthorized(res);
if (req.body.method === "tools/call") {
try {
await rekey.credits.consume({ endUserId, amount: 1 });
} catch (err) {
if (err instanceof RekeyError && err.code === "CREDITS_INSUFFICIENT") {
// Stop here. The tool does not run, and the agent is told why.
return res.json({
jsonrpc: "2.0",
id: req.body.id,
result: {
isError: true,
content: [
{ type: "text", text: "Out of credits. Top up at https://tools.example.com/billing" },
],
},
});
}
throw err;
}
}
// ...handle the request as your server already does
});When the balance is too low, this is what the API sends back, and what err.code and err.fix carry:
HTTP/1.1 402 Payment Required
{
"success": false,
"error": {
"code": "CREDITS_INSUFFICIENT",
"message": "Not enough credits: need 1, balance 0.",
"fix": "Buy a credit pack (CREDIT plan/entitlement), or grant credits from the panel.",
"requestId": "..."
}
}What is not built yet
Before you start- Introspection is a network call on every request. Tokens cannot be checked locally yet, and the endpoint shares the rate limits sized for sign-in, so a busy server should hold an answer for a few seconds instead of asking on every tool call.
- There is no starter template for an MCP server yet. The code above is the whole integration, wired into a server you already have.
- Tokens name Rekey's issuer as their audience, not your server's URL. A
resourceparameter (RFC 8707) is accepted and ignored.
What it costs
TermsThe free plan on Rekey Cloud covers one Application in production, which is one MCP server, plus as many development and staging Applications as you need. Free is subject to fair use. Self-hosting is free under the MIT license with no limit on servers. The full terms are on the pricing page.
The protocol details, scopes and troubleshooting are in the MCP guide.
