Studio · Docs

How to pay

A complete recipe for buying a job, for a person with a wallet and for an agent speaking x402. Every address, price and deadline on this page is read from the running configuration.

Every job here is one HTTP POST. Send it without payment and the reply is 402 Payment Required carrying the exact terms; send it again with proof of payment and the reply is your result. There is no account, no API key, no signup and no minimum spend.

Two rails are accepted, and they settle differently. Rail A is a plain USDC transfer from any wallet, proved afterwards with the transaction hash — nothing to sign, nothing to implement. Rail B is x402: a signed EIP-712 authorization in a header, verified offline, no transaction needed up front. If you are a person, use Rail A. If you are an agent, use Rail B.

What is for sale

OfferPriceYou POSTEndpoint
Summarize a document — A faithful summary of your text in at most five sentences.$0.10 (100000 base units)text (required, non-empty, up to 24000 characters). The reply is a summary of at most five sentences.POST https://short.desknav.ai/jobs/summarize
Extract structured data — Pull the fields you name out of unstructured text and return them as JSON.$0.15 (150000 base units)text (required, non-empty, up to 24000 characters) and fields (optional but recommended, up to 32 names). result is a JSON document, as a string, keyed by your field names, with null for anything absent.POST https://short.desknav.ai/jobs/extract
Classify text — Choose one of your labels for a piece of text, with a confidence and a one-line reason.$0.05 (50000 base units)text (required, non-empty, up to 16000 characters) and labels (up to 32). The reply is (up to 32). result is a JSON document, as a string, and the label is always one of yours.POST https://short.desknav.ai/jobs/classify
Short link — Mint a permanent short link to any public http(s) URL, optionally with a custom alias. Redirects and click stats are free and unmetered forever.$0.05 (50000 base units)url (required, must carry an http or https scheme, up to 2048 characters) and alias (optional, 3-32 characters of A-Z a-z 0-9 _ -, never overwrites a taken one). Redirects and click counts are free forever.POST https://short.desknav.ai/jobs/shorten
Gas price oracle — Current gas price and latest base fee for Base or Ethereum L1, read fresh from the chain at call time — never cached. Returns wei and gwei figures plus the chain id, so the answer is unambiguous.$0.02 (20000 base units)See GET https://short.desknav.ai/jobs/gas-price for this offer's fields.POST https://short.desknav.ai/jobs/gas-price
Web page fetch — Fetch a public web page and return its text (markup, scripts and styles stripped) or its raw HTML. Redirects are followed; loopback, private and link-local targets are refused at every hop.$0.10 (100000 base units)See GET https://short.desknav.ai/jobs/fetch-page for this offer's fields.POST https://short.desknav.ai/jobs/fetch-page

Prices are integer US cents converted to USDC base units at 6 decimals. Both numbers are in the agent card, so a machine never has to do the conversion. Your request body is checked BEFORE payment, so a malformed request costs nothing — you get 400 and can fix it and retry with the same funds.

The facts every payment needs

FieldValue
Pay to0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee
TokenUSDC, 6 decimals, 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Chaineip155:8453 (base)
x402 schemeexact
x402 version1
Rail A headerX-Payment-Tx
Rail B headerX-Payment

Send the token at that exact contract address on that exact chain. USDC on any other chain, bridged USDC, or a different token will not be seen, and there is no way to return it.

Rail A — pay from any wallet, then quote the transaction

This is the low-barrier path: any wallet that can send USDC on base can buy. You pay the network fee for your own transfer and nothing else, and the payment settles the moment we see the receipt — there is no authorization to expire.

1. Send the USDC

Send at least the offer's price in USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) on eip155:8453 to 0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee. Any wallet will do. Amounts, if you prefer to paste them:

OfferSendBase unitsWallet link (EIP-681)
summarize$0.10100000 base unitsethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee&uint256=100000
extract$0.15150000 base unitsethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee&uint256=150000
classify$0.0550000 base unitsethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee&uint256=50000
shorten$0.0550000 base unitsethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee&uint256=50000
gas-price$0.0220000 base unitsethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee&uint256=20000
fetch-page$0.10100000 base unitsethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee&uint256=100000

2. Wait for the transaction to confirm

We read the receipt from a base node. A transaction the node does not know yet — still pending, or sent on another chain — comes back as RPC_ERROR, because the node reports an unknown transaction as an error rather than as an empty receipt. Wait a few seconds and try again; a refused attempt consumes nothing.

3. POST the job with the transaction hash

curl -X POST https://short.desknav.ai/jobs/summarize \
  -H 'Content-Type: application/json' \
  -H 'X-Payment-Tx: 0x<transaction-hash>' \
  -d '{"text": "Paste the document to summarize here."}'

A successful reply looks like this:

{
  "invoiceId": "inv_<ulid>",
  "offerId": "summarize",
  "status": "settled",
  "rail": "direct",
  "txHash": "0x<transaction-hash>",
  "output": {
    "offerId": "summarize",
    "result": "The summary text.",
    "model": "<the model that ran it>",
    "inputThreatLevel": "low"
  }
}

When Rail A refuses

The reply is 402 with {"error":"PAYMENT_REJECTED","reason":"<code>"}. The code says exactly what to do:

CodeWhat happenedWhat to do
TX_MALFORMEDThe header is not 0x plus 64 hex characters.Send the transaction hash, not a URL or a block explorer link.
RECEIPT_NOT_FOUNDThe node answered with no receipt at all. Measured against the live deployment, a node reports an unknown transaction as an error instead, so a pending or wrong-chain hash arrives as RPC_ERROR rather than this.Wait for confirmation and retry.
TX_REVERTEDThe transaction failed; no funds moved.Send a new transfer.
NO_MATCHING_TRANSFERNo USDC transfer to the pay-to address in that transaction.Check the token contract and the recipient. Only 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 to 0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee counts.
INSUFFICIENT_AMOUNTThe transfer is below the price.Send another transfer covering the full price, and quote that one.
REPLAYED_TXThat transaction already bought a job.Pay again for a new job.
RPC_ERRORWe could not read the receipt: either the node was unreachable, or it reported the transaction as unknown — which is what a still-pending hash, or one sent on another chain, looks like from here.Confirm the transfer landed on eip155:8453, then retry. Nothing was consumed.

Rail B — x402, for agents and developers

You sign an EIP-712 USDC TransferWithAuthorization and put it in a header. We verify it offline and do the work immediately; we redeem it on chain afterwards and pay that gas ourselves. No transaction, no approval and no gas is needed from you at request time — but the USDC must still be in your wallet when we redeem.

1. Ask, and read the 402

curl -i -X POST https://short.desknav.ai/jobs/summarize \
  -H 'Content-Type: application/json' \
  -d '{"text": "Paste the document to summarize here."}'

The reply is 402. Its body carries the terms, and the same document is repeated as raw JSON in the X-Payment-Required response header:

{
  "error": "PAYMENT_REQUIRED",
  "x402Version": 1,
  "accepts": [
    {
      "scheme": "exact",
      "network": "base",
      "maxAmountRequired": "100000",
      "payToAddress": "0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee",
      "usdcAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "requiredDeadlineSeconds": 1800,
      "resource": "https://short.desknav.ai/jobs/summarize"
    }
  ],
  "offerId": "summarize",
  "priceCents": 10
}

Read every value out of accepts rather than hardcoding it. maxAmountRequired is in USDC base units, not cents and not whole tokens.

2. Sign against this exact domain

The domain is the token's own, not ours. Getting one character of it wrong produces a signature that recovers to a different address, which comes back as BAD_SIGNATURE.

{
  "name": "USD Coin",
  "version": "2",
  "chainId": 8453,
  "verifyingContract": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
}

That domain hashes to 0x02fa7265e7c5d81118673727957699e4d68f74cd74b7db77da710fe8a2c7834f. It is the same value the token returns from DOMAIN_SEPARATOR() on chain — compare yours before you spend a signature on it.

The type, which must be encoded in exactly this field order:

{
  "TransferWithAuthorization": [
    { "name": "from",        "type": "address" },
    { "name": "to",          "type": "address" },
    { "name": "value",       "type": "uint256" },
    { "name": "validAfter",  "type": "uint256" },
    { "name": "validBefore", "type": "uint256" },
    { "name": "nonce",       "type": "bytes32" }
  ]
}
Message fieldWhat it must be
fromYour wallet. The signature must recover to this address, and it is the address the USDC leaves.
toExactly 0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee. Anything else is WRONG_RECIPIENT.
valueAt least maxAmountRequired from the 402, as a decimal string of base units.
validAfterUnix seconds, already in the past by our clock. now - 60 is what this runtime's own client uses; up to 60s of clock disagreement is tolerated.
validBeforeUnix seconds. Set now + 1800 — see the deadline rule below.
nonce32 fresh random bytes as 0x hex. Never reuse one, on any offer.

3. The deadline rule

Settlement is asynchronous: we redeem your authorization in a later sweep, so an authorization that closes before that sweep is worth nothing to us and we will not do work for it. The 402 asks for 1800s (requiredDeadlineSeconds). By the time your request arrives, at least 1680s of that window must still be left, or the reply is DEADLINE_TOO_SOON — the difference is arrival grace for the round trip and clock skew, not spare room to sign a shorter window.

4. Send the envelope

The X-Payment header carries this JSON, either raw or base64-encoded — both are accepted. Base64 is safer in a header: a raw JSON envelope with a non-ASCII character in it cannot be sent.

{
  "x402Version": 1,
  "scheme": "exact",
  "network": "base",
  "payload": {
    "signature": "0x<65-byte-signature>",
    "authorization": {
      "from": "0x<your-wallet>",
      "to": "0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee",
      "value": "100000",
      "validAfter": "<unix-seconds>",
      "validBefore": "<unix-seconds>",
      "nonce": "0x<32-random-bytes>"
    }
  }
}

As one command:

curl -X POST https://short.desknav.ai/jobs/summarize \
  -H 'Content-Type: application/json' \
  -H 'X-Payment: {"x402Version":1,"scheme":"exact","network":"base","payload":{"signature":"0x<65-byte-signature>","authorization":{"from":"0x<your-wallet>","to":"0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee","value":"100000","validAfter":"<unix-seconds>","validBefore":"<unix-seconds>","nonce":"0x<32-random-bytes>"}}}' \
  -d '{"text": "Paste the document to summarize here."}'

A successful reply is 200:

{
  "invoiceId": "inv_<ulid>",
  "offerId": "summarize",
  "status": "delivered",
  "output": {
    "offerId": "summarize",
    "result": "The summary text.",
    "model": "<the model that ran it>",
    "inputThreatLevel": "low"
  }
}

status is delivered, not settled: the work is done and the authorization is ours to redeem. Rail A returns settled instead, because the money had already moved before the request.

5. A working client, in viem

Node 20+ and npm i viem. This is the whole protocol: probe, sign what the 402 asked for, replay.

import { privateKeyToAccount } from "viem/accounts";

const ENDPOINT = "https://short.desknav.ai/jobs/summarize";
const BODY = {"text": "Paste the document to summarize here."};

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const headers = { "Content-Type": "application/json" };
const body = JSON.stringify(BODY);

// 1. Probe. The 402 body carries the terms; never hardcode them.
const probe = await fetch(ENDPOINT, { method: "POST", headers, body });
if (probe.status !== 402) throw new Error(`expected 402, got ${probe.status}`);
const { accepts } = await probe.json();
const terms = accepts.find((a: any) => a.scheme === "exact");
if (!terms) throw new Error("no acceptable payment scheme");

// 2. Sign a TransferWithAuthorization for exactly those terms.
const now = Math.floor(Date.now() / 1000);
const validAfter = now - 60;
const validBefore = now + terms.requiredDeadlineSeconds;
const nonce = ("0x" + Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex")) as `0x${string}`;

const signature = await account.signTypedData({
  domain: {
    name: "USD Coin",
    version: "2",
    chainId: 8453,
    verifyingContract: terms.usdcAddress,
  },
  types: {
    TransferWithAuthorization: [
      { name: "from", type: "address" },
      { name: "to", type: "address" },
      { name: "value", type: "uint256" },
      { name: "validAfter", type: "uint256" },
      { name: "validBefore", type: "uint256" },
      { name: "nonce", type: "bytes32" },
    ],
  },
  primaryType: "TransferWithAuthorization",
  message: {
    from: account.address,
    to: terms.payToAddress,
    value: BigInt(terms.maxAmountRequired),   // BigInt here...
    validAfter: BigInt(validAfter),
    validBefore: BigInt(validBefore),
    nonce,
  },
});

// 3. Replay the identical request with the envelope attached.
const envelope = {
  x402Version: 1,
  scheme: terms.scheme,
  network: terms.network,
  payload: {
    signature,
    authorization: {
      from: account.address,
      to: terms.payToAddress,
      value: terms.maxAmountRequired,          // ...decimal STRINGS on the wire.
      validAfter: String(validAfter),
      validBefore: String(validBefore),
      nonce,
    },
  },
};

const paid = await fetch(ENDPOINT, {
  method: "POST",
  headers: {
    ...headers,
    "X-Payment": Buffer.from(JSON.stringify(envelope)).toString("base64"),
  },
  body,
});
console.log(paid.status, await paid.json());

When Rail B refuses

The reply is 402 with {"error":"PAYMENT_REJECTED","code":"<code>"} and the terms again in accepts, so a conforming client can correct itself and retry. Note the field is code on this rail and reason on Rail A.

CodeWhat happenedWhat to do
MALFORMED_HEADERThe header is not decodable, is over the size cap, or the envelope is missing fields or has them in the wrong shape.Check every numeric field is a decimal string and the nonce is 0x plus 64 hex characters.
UNSUPPORTED_SCHEMEThe scheme is not exact.Send "scheme": "exact".
UNSUPPORTED_NETWORKThe envelope names a chain we are not paid on, or contradicts itself.Use base or eip155:8453, and remove any disagreeing chain field.
WRONG_ASSETThe envelope names a token that is not our USDC.Remove the asset field or set it to 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913.
AMOUNT_UNPARSEABLEvalue is not a plain uint256 decimal string.No 0x, no decimal point, no sign, no whitespace, no exponent.
BAD_SIGNATUREThe signature does not recover to from for our domain.Almost always the EIP-712 domain: name, version, chainId and verifyingContract must match the domain above exactly. Numeric message fields must be hashed as integers, not strings.
WRONG_RECIPIENTThe authorization pays somebody else.Set to to 0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee.
INSUFFICIENT_AMOUNTThe authorized value is below the price.Use maxAmountRequired verbatim.
NOT_YET_VALIDvalidAfter is 60s or more in the future by our clock — 60s is the whole skew tolerance, not a bonus.Use now - 60.
EXPIREDvalidBefore has passed.Sign a fresh authorization; there is no tolerance on this one.
DEADLINE_TOO_SOONStill valid, but under 1680s remain — we could not redeem it.Sign now + 1800 and send promptly.
REPLAYED_NONCEThat nonce was already spent, or the replay store could not confirm it is unused.Generate 32 fresh random bytes per request. Never reuse.

Every other way a request can fail

StatusBodyMeaning
400{"error":"BAD_REQUEST","reason":"..."}Your input, checked before payment. Free to fix and retry — nothing was charged and no nonce was spent.
400{"error":"BAD_JSON"}The request body is not JSON.
402{"error":"PAYMENT_REQUIRED"}No payment header. The terms are in the body.
402{"error":"PAYMENT_REJECTED"}See the rejection tables above.
404{"error":"UNKNOWN_OFFER"}No such offer, or it is not on sale. The live list is at https://short.desknav.ai/offers.
405{"error":"METHOD_NOT_ALLOWED"}Buying is a POST. GET https://short.desknav.ai/jobs/<offer-id> describes the offer instead.
409{"error":"REPLAYED"}Two requests raced for the same nonce or transaction hash and this one lost. No work was done and nothing was charged twice. (A non-racing replay is refused earlier, as a 402.)
413{"error":"BODY_TOO_LARGE"}The body is over 65536 bytes.
429{"error":"RATE_LIMITED"}Per-IP token bucket: 20 requests of burst, refilling at 1 per second. Back off and retry.
500{"error":"WORK_FAILED","invoiceId":"..."}We took payment and the work failed. Keep the invoice id: the failure is recorded against that invoice with its reason.
500{"error":"INTERNAL_ERROR"}Our fault, and nothing about your payment is implied. Retry.

Reading all of this as a machine