๐ค Agent API + MCP Server
Pay-per-minute REST and MCP endpoints that let AI agents submit tennis videos to the AceSense GPU pipeline and retrieve coaching reports.
:::tip TL;DR
- REST base:
https://api.acesense.io/v1 - MCP base:
https://api.acesense.io/mcp - Auth:
Authorization: Bearer phk_<keyId>_<secret>(SHA-256-hashed at rest, constant-time compare) - Pricing: โฌ0.75/min of submitted video (โ $0.80 / ยฃ0.65), prepaid credits, โฌ10 free credit per new key
- Quota: per-key sliding-window (default 10 req/min, 200 jobs/day)
- Discovery:
/.well-known/api-catalog,/.well-known/mcp/server-card.json,/.well-known/x402,/.well-known/mpp.json,/.well-known/acp.json:::
๐ฏ What this is forโ
The AceSense Flutter app is one consumer of the GPU pipeline. The agent API is another. Same five-step pipeline (TrackNet โ court keypoints โ MediaPipe pose โ CatBoost classification โ stroke quality), exposed via two protocols an AI agent can speak fluently:
- REST for general HTTP clients, OpenAPI 3.1 described.
- MCP for native LLM-tool integration (Claude desktop, ChatGPT custom GPTs, agent SDKs).
Customers are tennis-coaching bots, training-platform integrations, club-management software, dataset-enrichment pipelines.
๐๏ธ Where it livesโ
| Surface | Repo path | Function name |
|---|---|---|
| REST API | acesense-auth-function/api/ | apiServer (onRequest, region europe-west1) |
| MCP server | acesense-auth-function/mcp/ | mcpServer (onRequest, region europe-west1) |
| API key callables | acesense-auth-function/api/keys.ts | createApiKey, listApiKeys, revokeApiKey |
| Rate limiter | acesense-auth-function/api/rate-limit.ts | (internal) |
| Billing meter | acesense-auth-function/api/billing-meter.ts | (internal) |
| Marketing landing | acesense-landing/src/content/static/agents.md | /agents route |
| Discovery files | acesense-landing/scripts/prerender.mjs | /.well-known/* |
The custom domain api.acesense.io is configured via Firebase Hosting rewrites (set up separately from the function deploy).
๐ API key architectureโ
Stored at api_keys/{keyId} in Firestore:
interface ApiKey {
keyId: string // 'phk_<base64url-rand-16>'
userId: string // owning Firebase user
hashedSecret: string // sha256(fullKey)
name: string
plan: 'free' | 'pro' | 'team'
createdAt: Timestamp
lastUsedAt: Timestamp | null
revokedAt: Timestamp | null
balanceCents: number // prepaid credit balance
spentCentsTotal: number
rateLimit: { perMinute: number; perDay: number }
allowedOrigins?: string[]
}
- Wire format:
phk_<keyId>_<secret>. The secret is only shown to the user once at creation. - All comparisons via
crypto.timingSafeEqualto avoid timing attacks. lastUsedAtis best-effort updated on every successful call.- Soft-revoke sets
revokedAt; the doc stays for audit.
๐ธ Billing flowโ
estimateJobCostCents = ceil(durationSeconds / 60) * pricePerMinuteCentsโ default 75ยข (โฌ0.75/min), overridable viaAPI_PRICE_PER_MINUTE_CENTS.quoteAndReserveis an atomic Firestore transaction atPOST /v1/jobsโ if balance < cost the job is rejected before any RunPod work.assertJobBudgetruns again insideprocessVideoOnUploadas defence-in-depth (a job that reserved budget but then balance got refunded mid-flight is rejected).creditFailedJobrefunds the reserved cost on pipeline failure.- Balance top-ups are manual for now via
adminAdjustApiKeyBalance; no checkout/webhook endpoint is deployed.
๐ง Rate limitingโ
Per-key sliding window backed by Firestore at api_keys/{keyId}/rate_buckets/{minuteKey}:
- Default: 10 req/min + 200 jobs/day
- Plan-tier keys (set via the user's billing page) can raise these
- Returns HTTP 429 +
Retry-After: <seconds>when exceeded X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Resetheaders on every response
For MCP, the same limiter applies. Exceeded โ JSON-RPC error code -32002 "rate limited".
๐ง Tools exposed by the MCP serverโ
| Tool | Paid? | Description |
|---|---|---|
analyze_tennis_video | โ | Submit a video URL; gates on balanceCents >= cost; refunds on ingest failure |
get_job_status | โ | Poll { status, progress, error? } |
get_analysis_result | โ | Fetch the completed analysis JSON |
get_pricing | โ | Current per-minute pricing across EUR/USD/GBP |
get_my_usage | โ | Balance + spend + jobs today/this month |
list_my_jobs | โ | Paginated list of recent jobs for the calling key |
Resources (acesense://*.md) point at the marketing markdown twins (pricing, accuracy, how-it-works, faq, compare/{competitor}) and are fetched on-demand.
๐ก๏ธ Defence-in-depthโ
The same threat-model controls protect both the in-app upload path and the agent API:
- Per-user quota (
shared/quota.ts) โ atomic Firestore transactions; concurrent slot rolled back on Firestore-write failure. - Magic-byte gate (
shared/video-validation.ts) โ first 64 bytes checked for ISO-BMFF / AVI / MKV signatures before RunPod dispatch. Failed jobs marked'invalid_file'. - 30-min signed-URL TTL (
shared/signed-url.ts) โ v4 signed URLs with explicitexpires; SHA-256-hashed audit row insigned_url_audit/{id}. - ffprobe gate โ deferred. Hook point + tolerance helper are wired (
withinToleranceinshared/video-validation.ts); needs@ffprobe-installer/ffprobe(~30 MB) bundled in the deploy. - Locked Firestore rules โ
jobs/{id}updates whitelistuserTitle,userTags,userNotes,isFavorite,updatedAt; everything else is server-only.
See Malicious-upload threat model for the full P0/P1 list and remaining open items.
๐ Discovery surfaceโ
These files are emitted by the landing-site prerender step and served as static JSON:
| Path | Purpose |
|---|---|
/.well-known/api-catalog | RFC 9727 linkset of public retrievable resources |
/.well-known/mcp/server-card.json | SEP-1649 MCP card (live, version 0.2.0) |
/.well-known/agent-skills/index.json | Cloudflare Agent Skills RFC v0.2.0 catalogue |
/.well-known/openid-configuration | OIDC discovery (Firebase Auth issuer) |
/.well-known/oauth-authorization-server | RFC 8414 |
/.well-known/oauth-protected-resource | RFC 9728 |
/.well-known/x402 | x402 payment-required discovery |
/.well-known/mpp.json | Machine Payment Protocol |
/.well-known/acp.json | Agentic Commerce Protocol (transactional: true) |
/.well-known/ucp | Universal Commerce Protocol |
/openapi.json | OpenAPI 3.1 |
๐ Setup checklist (after deploy)โ
- Deploy Cloud Functions:
cd acesense-auth-function && pnpm deploy(orfirebase deploy --only functions). - Configure Firebase Hosting custom domain
api.acesense.iowith rewrites:/v1/**โ functionapiServer/mcpโ functionmcpServer
- Test the public API key flow: sign in as a user, call
createApiKey({ name: 'test', plan: 'free' }), capture the returnedfullKey. - Credit test balance: use
adminAdjustApiKeyBalancefor any paid-path smoke test. - Smoke-test the REST API:
curl -H "Authorization: Bearer phk_..." https://api.acesense.io/v1/healthz. - Smoke-test the MCP server:
POST /mcpwith{"jsonrpc":"2.0","id":1,"method":"tools/list"}. Confirm the 6 tools are returned.