Doc API
Back to blog

Autonomous Invoice Generation: An Agent That Bills Itself

·8 min read

AI agents are increasingly running long-horizon tasks: managing projects, handling customer support, writing and deploying code. As they do more, the question of how they pay for the tools and APIs they use becomes real.

This post covers a pattern where an agent generates invoices autonomously — and a related one where an agent manages its own API credits without any human intervention.

The pattern

An autonomous billing agent:

  1. Tracks usage or completed work
  2. Generates a PDF invoice from that data
  3. Emails the invoice to the client
  4. Monitors its own credit balance and tops up if needed

No human clicks "send invoice." The agent runs on a schedule and handles the full workflow.

Self-registering with the PDF API

Most API services require a human to sign up and paste an API key into environment variables. DocAPI supports programmatic registration — an agent can create its own account and receive an API key without any human involvement:

async function registerAgent(agentId: string): Promise<{ apiKey: string; usdcAddress: string }> {
  const response = await fetch('https://www.docapi.co/api/register', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: `${agentId}@agents.yourapp.com`,
      agentId,
    }),
  })
 
  if (!response.ok) {
    throw new Error(`Registration failed: ${response.status}`)
  }
 
  const { api_key, usdc_address } = await response.json()
  return { apiKey: api_key, usdcAddress: usdc_address }
}

On registration, the agent gets:

  • An API key (pk_...) for generating PDFs
  • A USDC wallet address on Base mainnet for topping up credits
  • 10 free credits to start

Store these in your agent's configuration or database. The agent uses the API key for all subsequent PDF requests.

Checking the agent's own credit balance

Before generating a batch of documents, the agent should verify it has enough credits:

async function checkCredits(apiKey: string): Promise<{ credits: number; usdcAddress: string }> {
  const response = await fetch('https://www.docapi.co/api/topup', {
    headers: { 'x-api-key': apiKey },
  })
 
  const { credits, usdc_address } = await response.json()
  return { credits, usdcAddress: usdc_address }
}
 
async function ensureCredits(apiKey: string, needed: number): Promise<void> {
  const { credits, usdcAddress } = await checkCredits(apiKey)
 
  if (credits < needed) {
    // The agent can't top up itself without an external operator sending USDC.
    // It should notify the operator or log this for monitoring.
    throw new Error(
      `Insufficient credits: ${credits} available, ${needed} needed. ` +
      `Top up by sending USDC to ${usdcAddress} on Base mainnet.`
    )
  }
}

Invoice template

A clean invoice template that an agent can fill with its own billing data:

interface InvoiceData {
  invoiceNumber: string
  issueDate: string
  dueDate: string
 
  from: {
    name: string
    address: string
    email: string
  }
 
  to: {
    name: string
    address: string
    email: string
  }
 
  lineItems: {
    description: string
    quantity: number
    unitPrice: number
    total: number
  }[]
 
  subtotal: number
  tax: number
  taxRate: number
  total: number
 
  notes?: string
}
 
function renderInvoice(data: InvoiceData): string {
  const rows = data.lineItems.map(item => `
    <tr>
      <td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;">${item.description}</td>
      <td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;text-align:right;">${item.quantity}</td>
      <td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;text-align:right;">$${item.unitPrice.toFixed(2)}</td>
      <td style="padding:10px 12px;border-bottom:1px solid #e5e7eb;text-align:right;font-weight:500;">$${item.total.toFixed(2)}</td>
    </tr>
  `).join('')
 
  return `<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #111; margin: 0; padding: 40px; font-size: 14px; }
    .header { display: flex; justify-content: space-between; margin-bottom: 40px; }
    h1 { font-size: 28px; font-weight: 800; color: #111; margin: 0 0 4px; }
    .invoice-meta { color: #6b7280; font-size: 13px; }
    .badge { display: inline-block; background: #f0fdf4; color: #15803d; border: 1px solid #bbf7d0; border-radius: 4px; padding: 2px 10px; font-size: 12px; font-weight: 600; }
    .addresses { display: flex; gap: 60px; margin-bottom: 40px; }
    .address-block h3 { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: #9ca3af; margin: 0 0 6px; }
    .address-block p { margin: 0; line-height: 1.6; color: #374151; }
    table { width: 100%; border-collapse: collapse; margin-bottom: 24px; }
    thead tr { background: #f9fafb; }
    th { padding: 10px 12px; text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: #6b7280; font-weight: 600; }
    th:not(:first-child) { text-align: right; }
    .totals { margin-left: auto; width: 280px; }
    .totals table { margin: 0; }
    .totals td { padding: 6px 8px; color: #374151; }
    .totals td:last-child { text-align: right; }
    .total-row td { font-size: 16px; font-weight: 700; padding-top: 10px; border-top: 2px solid #111; }
    .notes { margin-top: 32px; padding-top: 24px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 13px; }
  </style>
</head>
<body>
  <div class="header">
    <div>
      <h1>Invoice</h1>
      <div class="invoice-meta">
        #${data.invoiceNumber}<br>
        Issued: ${data.issueDate}<br>
        Due: ${data.dueDate}
      </div>
    </div>
    <div>
      <span class="badge">Due ${data.dueDate}</span>
      <div style="font-size:28px;font-weight:800;margin-top:8px;text-align:right;">$${data.total.toFixed(2)}</div>
    </div>
  </div>
 
  <div class="addresses">
    <div class="address-block">
      <h3>From</h3>
      <p>
        <strong>${data.from.name}</strong><br>
        ${data.from.address}<br>
        ${data.from.email}
      </p>
    </div>
    <div class="address-block">
      <h3>Bill To</h3>
      <p>
        <strong>${data.to.name}</strong><br>
        ${data.to.address}<br>
        ${data.to.email}
      </p>
    </div>
  </div>
 
  <table>
    <thead>
      <tr>
        <th>Description</th>
        <th style="text-align:right;">Qty</th>
        <th style="text-align:right;">Unit Price</th>
        <th style="text-align:right;">Amount</th>
      </tr>
    </thead>
    <tbody>${rows}</tbody>
  </table>
 
  <div class="totals">
    <table>
      <tr><td>Subtotal</td><td>$${data.subtotal.toFixed(2)}</td></tr>
      <tr><td>Tax (${data.taxRate}%)</td><td>$${data.tax.toFixed(2)}</td></tr>
      <tr class="total-row"><td>Total Due</td><td>$${data.total.toFixed(2)}</td></tr>
    </table>
  </div>
 
  ${data.notes ? `<div class="notes"><strong>Notes:</strong> ${data.notes}</div>` : ''}
</body>
</html>`
}

Full autonomous billing flow

Combine everything into an agent that runs on a schedule:

import { Anthropic } from '@anthropic-ai/sdk'
import { Resend } from 'resend'
 
const claude = new Anthropic()
const resend = new Resend(process.env.RESEND_API_KEY!)
 
async function generatePdf(html: string, apiKey: string): Promise<Buffer> {
  const response = await fetch('https://api.docapi.co/v1/pdf', {
    method: 'POST',
    headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
    body: JSON.stringify({ html, options: { format: 'A4', printBackground: true } }),
  })
  if (!response.ok) throw new Error(`PDF generation failed: ${response.status}`)
  return Buffer.from(await response.arrayBuffer())
}
 
export async function runBillingAgent(agentConfig: {
  apiKey: string
  fromEmail: string
  fromName: string
}) {
  // 1. Get unbilled work from your database
  const unbilledItems = await db.workItems.findUnbilled()
  if (unbilledItems.length === 0) return { invoicesSent: 0 }
 
  // 2. Use Claude to draft the invoice line items intelligently
  const draftResponse = await claude.messages.create({
    model: 'claude-opus-4-6',
    max_tokens: 1024,
    messages: [{
      role: 'user',
      content: `Given these completed work items, generate clear invoice line items with appropriate descriptions and pricing. Return as JSON array with fields: description, quantity, unitPrice, total.\n\nWork items:\n${JSON.stringify(unbilledItems, null, 2)}`,
    }],
  })
 
  const lineItems = JSON.parse(
    (draftResponse.content[0] as { text: string }).text
  )
 
  const subtotal = lineItems.reduce((sum: number, item: any) => sum + item.total, 0)
  const taxRate = 0.0875  // San Francisco 8.75%
  const tax = subtotal * taxRate
  const total = subtotal + tax
 
  // 3. Build invoice data
  const invoiceData: InvoiceData = {
    invoiceNumber: `INV-${Date.now()}`,
    issueDate: new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }),
    dueDate: new Date(Date.now() + 30 * 24 * 3600 * 1000).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }),
    from: {
      name: agentConfig.fromName,
      address: '123 Main St, San Francisco, CA 94105',
      email: agentConfig.fromEmail,
    },
    to: {
      name: 'Client Name',
      address: '456 Oak Ave, New York, NY 10001',
      email: '[email protected]',
    },
    lineItems,
    subtotal,
    tax,
    taxRate: taxRate * 100,
    total,
    notes: 'Payment due within 30 days. Wire transfer or ACH preferred.',
  }
 
  // 4. Generate the PDF
  const html = renderInvoice(invoiceData)
  const pdfBuffer = await generatePdf(html, agentConfig.apiKey)
 
  // 5. Send via email with attachment
  await resend.emails.send({
    from: `${agentConfig.fromName} <${agentConfig.fromEmail}>`,
    to: invoiceData.to.email,
    subject: `Invoice #${invoiceData.invoiceNumber} — $${total.toFixed(2)} due ${invoiceData.dueDate}`,
    html: `<p>Hi,</p><p>Please find your invoice attached for work completed through ${invoiceData.issueDate}.</p><p>Total due: <strong>$${total.toFixed(2)}</strong> by ${invoiceData.dueDate}.</p>`,
    attachments: [{
      filename: `invoice-${invoiceData.invoiceNumber}.pdf`,
      content: pdfBuffer,
    }],
  })
 
  // 6. Mark items as billed
  await db.workItems.markBilled(unbilledItems.map(i => i.id), invoiceData.invoiceNumber)
 
  return { invoicesSent: 1, invoiceNumber: invoiceData.invoiceNumber, total }
}

Running on a schedule

Deploy this as a Vercel Cron job that runs on the 1st of every month:

// vercel.json
{
  "crons": [
    {
      "path": "/api/cron/billing",
      "schedule": "0 9 1 * *"
    }
  ]
}
// app/api/cron/billing/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { runBillingAgent } from '@/lib/agents/billing'
 
export async function GET(req: NextRequest) {
  if (req.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }
 
  const result = await runBillingAgent({
    apiKey: process.env.DOCAPI_KEY!,
    fromEmail: '[email protected]',
    fromName: 'Your Company',
  })
 
  return NextResponse.json(result)
}

Credit monitoring in the agent loop

For agents that run frequently, check credits before each billing run:

async function guardedBillingRun(agentConfig: AgentConfig) {
  const { credits } = await checkCredits(agentConfig.apiKey)
 
  if (credits < 5) {
    // Alert the operator
    await sendSlackMessage(
      `⚠️ Billing agent low on credits: ${credits} remaining. ` +
      `Top up at ${agentConfig.usdcAddress} on Base mainnet.`
    )
    return { skipped: true, reason: 'insufficient_credits' }
  }
 
  return runBillingAgent(agentConfig)
}

The agent surfaces its own operational status to the humans who need to know about it — without blocking unless it truly can't proceed.

What this enables

This pattern — an agent that self-registers, generates documents, manages its own credits, and alerts humans only when necessary — represents a new class of software. The agent handles routine billing completely autonomously. Humans stay in the loop for exceptions: credit top-ups, client disputes, invoice corrections.

As agent frameworks mature, autonomous document generation will be a standard capability alongside web search and code execution. The infrastructure is ready today.

Get started

npm install @docapi/sdk @anthropic-ai/sdk
DOCAPI_KEY=pk_your_key_here

Get a key at docapi.co/signup — 10 free PDFs, no credit card required. Agents can self-register at /api/register.

Autonomous Invoice Generation: An Agent That Bills Itself | Doc API Blog | Doc API