DEV Community

dun999
dun999

Posted on

I Built a Paid API That AI Agents Use Autonomously

Last week, Tempo and Stripe launched the Machine Payments Protocol. The idea: HTTP 402 (Payment Required) finally does something useful. An agent requests data, the server says “pay me first,” the agent pays in USDC, and the server delivers.

I wanted to see how fast I could build a real paid service on top of this. Turns out, pretty fast.

What I built

FinSight is a financial portfolio analytics API. AI agents send a portfolio (a list of assets with weights, returns, and volatility numbers), pay a small amount in USDC per call, and get structured analysis back.

Five paid endpoints:

  • Risk profiling scores each asset 0-100 based on volatility and max drawdown. Stablecoins get a flat 5. Everything else runs through min(100, (vol * 80 + drawdown * 60) / 1.4). Portfolio risk is the weighted average.
  • Rebalance computes target allocation based on a risk profile. Conservative profiles push 60% toward stablecoins. Balanced does 30%. Aggressive does 10%. The rest gets distributed by scoring non-stable assets: conservative uses inverse volatility, balanced uses return/risk ratio, aggressive uses raw return.
  • Diversification calculates the Herfindahl-Hirschman Index. sum(weight^2) * 10000. Grades from “excellent” to “critical.” Fires warnings when a single asset exceeds 50%, stable ratio drops below 5%, or HHI goes above 4000.
  • Stress test runs the portfolio through five scenarios: market crash (-40%), crypto winter (-60%), stablecoin depeg, volatility spike, and bull recovery (+80%). Reports portfolio value, percentage change, worst-hit and best-performing asset per scenario.
  • Full report combines everything in one response.

Live at: finsight-mpp.finsight-mpp.workers.dev

The payment flow

Here is what happens when an agent calls a paid endpoint:

Agent --> POST /analyze/risk + portfolio data
Server --> 402 Payment Required + challenge
Agent --> POST /analyze/risk + payment credential
Server --> 200 OK + analysis + receipt
Enter fullscreen mode Exit fullscreen mode

The mppx SDK handles steps 2-3 automatically. From the agent’s side, it looks like a single request. Payment settles in USDC on Tempo’s blockchain in under a second.

In code, adding payment to a route is one middleware call:

app.post('/analyze/risk', charge('0.01'), async (c) => {
  const { error, portfolio } = await parsePortfolio(c)
  if (error) return error
  return c.json(analyzeRisk(portfolio!))
})
Enter fullscreen mode Exit fullscreen mode

The charge function wraps mppx with lazy initialization. Cloudflare Workers does not have process.env, so secrets come through c.env at request time:

function charge(amount: string) {
  return async (c: any, next: any) => {
    if (!cachedMppx) {
      cachedMppx = Mppx.create({
        secretKey: c.env.MPP_SECRET_KEY,
        methods: [
          tempo({
            chainId: 4217,
            currency: '0x20C000000000000000000000b9537d11c60E8b50',
            recipient: '0x45f7d1ef7fc50e054fb89a20e82485861ee91857',
          }),
        ],
      })
    }
    return cachedMppx.charge({ amount })(c, next)
  }
}
Enter fullscreen mode Exit fullscreen mode

That is the entire payment integration. No Stripe dashboard, no webhook endpoints, no subscription management.

The stack

Nothing exotic:

  • Hono as the web framework. Lightweight, edge-native, first-class support in mppx SDK.
  • mppx for the MPP payment layer. Tempo charge method, USDC on Tempo mainnet.
  • Zod for input validation. Portfolio schema validates field types, ranges, and that weights sum to ~1.0.
  • viem for blockchain interaction under the hood.
  • Cloudflare Workers for hosting. Zero cold start, global edge network, generous free tier.

Project structure:

src/
  index.ts              # Routes + mppx setup
  schemas.ts            # Zod validation
  types.ts              # TypeScript interfaces
  llms.txt              # Agent discovery file
  analyze/
    risk-profile.ts     # Risk scoring
    rebalance.ts        # Allocation targets
    diversification.ts  # HHI calculation
    stress-test.ts      # Scenario simulation
    full-report.ts      # Orchestrator
Enter fullscreen mode Exit fullscreen mode

Each analysis module is a pure function. Portfolio in, structured JSON out. No side effects, no state, no database.

Input validation

All endpoints share one Zod schema:

const HoldingSchema = z.object({
  asset: z.string().min(1).max(20),
  weight: z.number().min(0).max(1),
  avgReturn: z.number().min(-1).max(100).default(0),
  volatility: z.number().min(0).max(100).default(0.3),
  maxDrawdown: z.number().min(0).max(1).default(0.3),
  isStable: z.boolean().default(false),
})

const PortfolioSchema = z.object({
  holdings: z.array(HoldingSchema).min(1).max(100),
  profile: z.enum(['conservative', 'balanced', 'aggressive']).default('balanced'),
  benchmarkReturn: z.number().default(0.08),
})
Enter fullscreen mode Exit fullscreen mode

Weights must sum to approximately 1.0 (0.98-1.02 tolerance). If validation fails, the server returns 400 before any payment happens. You only pay for valid requests.

Agent discovery

For agents to find your service, you need an llms.txt file. Think robots.txt but for AI agents. It describes your endpoints, input format, pricing, and how to pay.

I serve mine at GET /llms.txt:

app.get('/llms.txt', (c) => c.text(llmsTxt))
Enter fullscreen mode Exit fullscreen mode

Any coding agent (Claude Code, Codex, Amp) pointed at this URL can read the full service description and start making paid requests without human intervention.

Stress testing the stress test

The stress test module was the most interesting to build. Each scenario applies a different multiplier to asset values based on their properties:

const SCENARIOS: ScenarioSpec[] = [
  {
    name: 'market_crash',
    description: 'Broad market decline of 40%',
    calcAssetChange: (h) => (h.isStable ? 0 : -(0.4 * h.volatility) / 0.5),
  },
  {
    name: 'recovery_bull',
    description: 'Strong bull market recovery of 80%',
    calcAssetChange: (h) => {
      if (h.isStable) return 0
      return Math.min(2, (0.8 * Math.max(h.avgReturn, 0.05)) / 0.1)
    },
  },
  // ... 3 more scenarios
]
Enter fullscreen mode Exit fullscreen mode

Higher volatility assets get hit harder in crashes. Higher return assets gain more in bull runs. Stablecoins are unaffected except in the depeg scenario. The math is simplified (no correlation matrix), but it gives agents a useful signal about portfolio resilience.

Try it

npx mppx account create

npx mppx https://finsight-mpp.finsight-mpp.workers.dev/analyze/report \
  --method POST \
  --body '{
    "holdings": [
      {"asset":"ETH","weight":0.5,"avgReturn":0.15,"volatility":0.65,"maxDrawdown":0.55},
      {"asset":"BTC","weight":0.3,"avgReturn":0.12,"volatility":0.55,"maxDrawdown":0.45},
      {"asset":"USDC","weight":0.2,"isStable":true}
    ],
    "profile":"balanced"
  }'
Enter fullscreen mode Exit fullscreen mode

First command creates a wallet. Second command calls the API and pays automatically.

What I learned

MPP is production-ready. The SDK works. Payment settles fast. The 402 flow is clean. If you have a computation that agents would pay for, you can ship a paid service in a day.

Stateless is the sweet spot. No database means no migrations, no backups, no state bugs. The service is a pure function. Trivially scalable and nearly impossible to break.

Discoverability matters more than code. Building the API was the easy part. Getting listed in the MPP service directory, writing an llms.txt file, and getting the service in front of agent developers is the actual work.

Cloudflare Workers + Hono is a good combo for this. Edge deployment, zero cold start, and the mppx SDK has first-class Hono middleware. The lazy initialization pattern for secrets was the only Workers-specific gotcha.

Source

Open source: github.com/dun999/finsight-mpp

If you want to build your own paid service on MPP, fork the repo, swap the recipient wallet address, deploy, and you are live.


Built by @ichbindun. MPP docs at mpp.dev.

Top comments (0)