Quick context: I run GitDealFlow, an MCP server + dataset that tracks GitHub commit-velocity signals across ~100 venture-backed startups. Six free read-only tools, ~700 npm downloads in the first three weeks, listed on Glama and the official MCP registry.
We just shipped a seventh tool: get_deep_signal. It's paid — €0.19 per call, sold in 100-credit packs at €19. The other six tools stay free forever.
This post is about why we chose per-request over a monthly subscription, and how the implementation actually looks. tl;dr: when your customer is an agent making programmatic API calls, the SaaS rulebook stops working.
The framing
Marc Benioff announced "Salesforce Headless 360" on April 17 — entire Salesforce + Agentforce + Slack platforms exposed as APIs, MCP, and CLI. "The API is the UI now."
That made it official: the new customer for SaaS-shaped data products is an agent, not a human clicking through a dashboard. And agents have a different mental model for paying.
A human dashboard buyer wants to lock in a monthly seat — predictable cost, predictable access. An agent's principal wants what every LLM API has trained them to expect: pay per call, top up when low, no commitment. Same as OpenAI, Anthropic, Replicate, you name it.
So when we sized our paid tool, two pricing structures were on the table:
- Subscription: "agents tier" at €29/mo, unlimited calls.
- Per-request: 100 credits for €19. €0.19 per match. Misses are free.
We picked #2. Three reasons:
- The unit of value is countable. "I called the deep-signal tool 14 times this week" maps cleanly to "I owe €2.66 of consumption." The ROI conversation closes in 10 seconds: an analyst hour saved per call at €50/hr means 250× ROI on €0.19. Subscription pricing forces an "is it worth €29/mo?" calculation that resists fast yes.
-
Misses cost nothing. The deep-signal endpoint returns
{ found: false }when the startup isn't in our universe. We charge 0 credits for that. It's the API equivalent of "you only pay when the lease gets signed." - No drift toward SaaS. Once you have a subscription, the next quarterly review pushes "what features can we add to justify the price?" That's how SaaS bloats. Per-call keeps you focused on quality of each call.
What 1 credit returns
The free get_startup_signal returns a shallow summary: name, velocity, contributor count, sector. Useful for "is this startup tracked?" lookups.
The paid get_deep_signal returns memo-grade output:
{
"found": true,
"name": "ExampleCo",
"sector": "Cybersecurity",
"scores": {
"velocity": 84,
"growth": 67,
"novelty": 45,
"composite": 71
},
"rank": {
"inSector": 3,
"sectorTotal": 17,
"sectorPercentile": 88
},
"thesis": "ExampleCo is sustaining acceleration at Series A in Cybersecurity — driven primarily by commit velocity (+142%). Worth diligence this week.",
"comparables": [
{ "name": "PeerA", "commitVelocityChange": "+88%", "signalType": "acceleration" },
{ "name": "PeerB", "commitVelocityChange": "+67%", "signalType": "steady" }
],
"history": [...],
"balance": 99,
"charged": 1
}
Same dataset as the free tool. Different shape: scored, ranked, comparable-aware, with a plain-English thesis line you can drop straight into a Slack DM.
How the auth works
I needed an API key format that:
- Validates without a database lookup (we don't have one — Stripe customer metadata IS the credit ledger).
- Embeds the customer ID so the server knows who to charge.
- Can't be forged.
Solution: gdf_v2.<stripe_customer_id>.<hmac16> where the HMAC is HMAC-SHA256(AUTH_SECRET, "api-key-v2:" + customer_id) truncated to 16 hex chars.
export function generateApiKeyV2(customerId: string): string {
const tag = createHmac("sha256", AUTH_SECRET)
.update(`api-key-v2:${customerId}`)
.digest("hex")
.slice(0, 16);
return `gdf_v2.${customerId}.${tag}`;
}
export function parseApiKeyV2(key: string) {
// ...split, validate format, recompute HMAC, timing-safe-compare
return { customerId };
}
To validate a key on each request: split on ., recompute the expected HMAC for the embedded customer ID, timing-safe-compare. Stateless, zero DB hops.
How the credit ledger lives on Stripe
I didn't want to stand up Postgres or Upstash for this. Stripe's customer metadata is a 500-byte free-form key-value store that's already attached to the entity I was going to charge anyway. So:
// metadata on every credit-pack customer:
// api_credits = current balance, integer string
// api_credits_purchased = lifetime credits bought
// api_credits_consumed = lifetime credits consumed
// api_credits_last_at = ISO timestamp of last decrement
export async function consumeCredit(customerId: string) {
const customer = await stripe.customers.retrieve(customerId);
const balance = parseInt(customer.metadata.api_credits ?? "0", 10);
if (balance < 1) return { ok: false, reason: "insufficient_credits" };
await stripe.customers.update(customerId, {
metadata: {
api_credits: String(balance - 1),
api_credits_consumed: String(/* prev consumed */ + 1),
api_credits_last_at: new Date().toISOString(),
},
});
return { ok: true, balance: balance - 1 };
}
Tradeoffs:
- ~200ms per call for the Stripe round-trip. Fine for v1; would move to Upstash Redis if traffic forces it.
- Race conditions possible if a customer fires two parallel calls at literally the same millisecond. At v1 traffic (single digits per day per customer), the probability of an actual lost decrement is essentially zero.
- The merchant view is the Stripe Dashboard. No custom admin UI. Customer record metadata shows balance.
This took ~30 lines total. If you've got a small product and need usage-based billing, the temptation is to over-engineer. Stripe metadata covers more than people think.
How the buyer experiences it
- Hit
https://signals.gitdealflow.com/agents/credits→ click "Buy 100 credits — €19" → Stripe checkout (real card or Stripe-test). - Webhook fires
checkout.session.completed→ server adds 100 credits to customer metadata + emails the API key. - Buyer pastes the key as
GITDEALFLOW_API_KEYenv var (MCP host) or asAuthorization: Bearer(direct HTTP). - Each
get_deep_signalcall returns balance in the response body andX-Credits-Balanceheader. - Balance check at
/api/account/credits(web UI at/accountfor paste-and-check).
End-to-end test (real Stripe customer, real production endpoints, no money moved):
[✓] create test customer — cus_USO7dd8hbAEPUk
[✓] seed 100 credits
[✓] initial balance = 100
[✓] ds#1 charges 1 credit, balance 100→99
[✓] scored output present (composite=65, rank #1/17, thesis line)
[✓] miss does NOT charge (balance still 99)
[✓] final balance: 99 / 100 / 1
[✓] bad key → 401
[✓] mcp/rpc forwards correctly, 99→98
[✓] cleanup
✓ E2E PASSED: 10/10
What this lens means for other dev tools
If you're shipping an MCP server or any developer-facing API and the audience is agents (or developers piping through agents), think hard before defaulting to subscription. The OpenAI mental model has trained your buyer. Per-call, pay-as-you-go, top-up-when-low — those are the contours buyers expect now.
The 80/20 implementation is achievable in an afternoon: a payment link, a webhook handler, customer metadata as the ledger, an HMAC-based key format, and one paid endpoint that decrements. The hardest part is letting go of the dashboard.
If you want to play with the live tool — six free, one paid — install:
npx -y @gitdealflow/mcp-signal
Or use it without MCP via direct HTTP:
# free
curl https://signals.gitdealflow.com/api/signal?company=airbytehq
# paid
curl -X POST https://signals.gitdealflow.com/api/agent/deep-signal \
-H "Authorization: Bearer gdf_v2.cus_xxx.<your_hmac>" \
-H "Content-Type: application/json" \
-d '{"name":"airbytehq"}'
Buy credits at signals.gitdealflow.com/agents/credits.
Top comments (0)