Build for Dunea

Priced tools and metered usage

Charge per successful AI tool call, or report metered usage from your service. Both are paid from the owner's Dunea credits, within a monthly cap they set.

A plugin can charge for what it does, not only for being installed. There are two ways, and both are paid from the app owner's Dunea credits:

Priced AI toolMetered usage
What is chargedEach successful call of one of your AI toolsUnits your service reports ("per label printed")
ManifestagentTools[].priceusage
Who starts the chargeDunea, after your tool answersYour service, with POST /usage
Idempotent onThe tool call's idYour idempotencyKey

You can combine either with a one-time or monthly price, or offer the plugin free and charge only for what is used. Listing cards show + usage next to the price of any plugin with paid tools or metered usage.

You earn the usage share of every charge: see Ways to earn for the fee and the 30-day payout hold.

Owner spend caps

An owner who installs a plugin with paid tools or metered usage sets a monthly spend cap in credits (spendCapCredits on the install). Dunea suggests one at install: usage.suggestedMonthlyCapCredits if you declare usage, and the owner can change it any time.

  • Charges stop at the cap. A charge that would take the month over it is refused, and nothing is charged.
  • No cap means no metered charges at all. The plugin keeps working; only the paid parts are refused.
  • Charges are also refused when the owner doesn't have enough credits.
  • Dev installs and your own apps are never charged. Charges there come back as waived, so you can test the whole flow for free.

Owners see every charge on their monthly statement, with the description you give it.

Priced AI tools

Add price to a tool in agentTools. The amount is in credits per successful call: from 0.05 to 50, with at most two decimals.

src/tools.ts
import { defineTool, DuneaClient } from '@dunea/plugin-sdk';

declare function getAccessToken(installId: string): Promise<string>;
declare function buyLabel(orderId: string, idempotencyKey: string): Promise<{ trackingNumber: string; labelUrl: string }>;

export const createShippingLabel = defineTool({
  name: 'create_shipping_label',
  description:
    'Buy and print a shipping label for a paid order, and add the tracking number to the order. ' +
    'Use when the owner asks to ship an order. Costs 2 credits per label.',
  input: {
    type: 'object',
    properties: {
      orderId: { type: 'string', description: 'The order id' },
    },
    required: ['orderId'],
  },
  handler: async (input, request) => {
    // request.id is the charge key: a retried call has the same id and is charged once.
    // Pass it on, so a retry doesn't buy a second label from your carrier either.
    const label = await buyLabel(input.orderId, request.id);
    const dunea = new DuneaClient({ accessToken: await getAccessToken(request.installId) });

    await dunea.orders.fulfil(input.orderId, { trackingNumber: label.trackingNumber });

    return { output: `Label printed. Tracking number ${label.trackingNumber}.`, data: { labelUrl: label.labelUrl } };
  },
});

defineTool builds the manifest entry without a price, so add it when you put the tool in the manifest:

manifest.ts
import { definePlugin } from '@dunea/plugin-sdk';
import { createShippingLabel } from './src/tools';

export const manifest = definePlugin({
  // ...
  permissions: ['orders:read', 'orders:write'],
  webhookUrl: 'https://labels.example.com/dunea',
  agentTools: [{ ...createShippingLabel.manifest, price: { credits: 2 } }],
});
dunea.plugin.json (excerpt)
{
  "agentTools": [
    {
      "name": "create_shipping_label",
      "description": "Buy and print a shipping label for a paid order… Costs 2 credits per label.",
      "input": { "type": "object", "properties": { "orderId": { "type": "string", "description": "The order id" } }, "required": ["orderId"] },
      "price": { "credits": 2 }
    }
  ]
}

How a priced call works:

  • The AI is told the price. Before it calls a priced tool, the Dunea AI knows what the call costs, and it tells the owner.
  • Charged only on success. The owner is charged when your service answers 2xx without error. A response with error, a non-2xx status or a timeout is never charged. Use error for anything the owner shouldn't pay for.
  • Idempotent on the call id. Dunea charges each call under its id (ToolCallPricing.chargeKey is the same value as ToolCallRequest.id). If a call is retried, it is charged once.
  • Within the cap. If the owner's monthly cap or credit balance can't cover the call, nothing is charged (spend_cap_reached or insufficient_credits) and the AI explains why to the owner.

Say what each paid tool costs in your listing's description and in the tool's own description. Reviewers check that prices are disclosed and match the value (DUN-B002), and dunea-plugin check warns when the description doesn't mention credits.

Metered usage

Use metered usage when the thing you charge for happens in your service, not in a tool call: labels printed by a nightly job, messages sent, minutes transcribed.

Declare it

dunea.plugin.json (excerpt)
{
  "usage": {
    "unit": "label",
    "maxCreditsPerUnit": 2,
    "description": "Each shipping label you print costs up to 2 credits, depending on the carrier.",
    "suggestedMonthlyCapCredits": 100
  }
}
FieldConstraints
unit2 to 40 characters, singular ("label", "message", "minute").
maxCreditsPerUnitThe most you may charge for one unit. 0.05 to 50 credits, at most two decimals.
description20 to 300 characters. Shown to the owner at install.
suggestedMonthlyCapCredits1 to 10,000. Defaults to 50. The cap Dunea suggests at install.

Report it: POST /usage

Your service reports usage through the Plugin API with the install's access token. Each report is charged at once.

interface UsageReport {
  /** How many units (manifest usage.unit), > 0. */
  units: number;
  /** Price per unit in credits, ≤ manifest usage.maxCreditsPerUnit. */
  creditsPerUnit: number;
  /** Shown on the owner's statement ("Printed 3 shipping labels"). ≤ 120 chars. */
  description: string;
  /** Your unique key for this charge (≤ 100 chars); a retry with the same key is charged once. */
  idempotencyKey: string;
}

interface UsageReceipt {
  chargeId: string;
  credits: number;
  /** charged | waived (dev installs and your own apps are never charged) */
  status: 'charged' | 'waived';
  /** Credits left under the owner's cap this month. */
  remainingCapCredits: number;
}
src/usage.ts
import { DuneaApiError, DuneaClient, type UsageRefusal } from '@dunea/plugin-sdk';

// After printing three labels for order 1042:
declare const accessToken: string; // the install's access token (from plugin.installed)

const dunea = new DuneaClient({ accessToken });

try {
  const receipt = await dunea.reportUsage({
    units: 3,
    creditsPerUnit: 1.5,
    description: 'Printed 3 shipping labels for order #1042',
    idempotencyKey: 'order-1042-labels', // the same key on a retry is charged once
  });

  console.log(receipt.status, receipt.credits, receipt.remainingCapCredits);
} catch (err) {
  if (err instanceof DuneaApiError && err.status === 402) {
    // The owner's cap or balance can't cover it. Nothing was charged:
    // pause the paid work and tell the owner in your panel.
    const refusal = err.body as UsageRefusal;
    console.warn(refusal.error, refusal.remainingCapCredits);
  } else {
    throw err;
  }
}
  • Report after the work is done, and use an idempotencyKey you can reproduce (an order or job id), so a retry after a network error is never charged twice.
  • Stay under maxCreditsPerUnit. A report above it is rejected. Raising the maximum is a new version, and it is reviewed by a person.
  • Handle 402. It means the owner's cap or balance can't cover the report. Nothing was charged. Stop the paid work and tell the owner; don't retry in a loop.
interface UsageRefusal {
  error: 'spend_cap_reached' | 'insufficient_credits';
  message: string;
  remainingCapCredits: number;
}

Read it back: GET /usage

GET /usage?month=YYYY-MM (dunea.usage('2026-11') in the SDK) returns the install's usage for a month (the current month by default): its cap, what's been used and each charge, including tool calls.

interface UsageSummary {
  month: string;
  capCredits: number | null;
  usedCredits: number;
  charges: { id: string; item: string; units: number; credits: number; description: string | null; status: string; at: string }[];
}

Use it to show the owner what they've spent in your panel, or to stop work before the cap is reached.

Changing prices

Prices are part of the manifest, so a change ships as a new version. Lowering a tool's price or maxCreditsPerUnit can be fast-tracked; any new tool, a changed tool description or input, a higher price or new metered usage is always reviewed by a person.

On this page