# Paying Studio How to buy anything Studio sells, at https://short.desknav.ai. _This file is generated by `renderPayingGuideMarkdown` in `src/revenue/site.ts` from the same catalog the server prices from. The live copies are https://short.desknav.ai/docs (HTML) and https://short.desknav.ai/docs.md (Markdown)._ 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 | Offer | Price | You POST | Endpoint | | --- | --- | --- | --- | | **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](https://short.desknav.ai/.well-known/agent-card.json), 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 | Field | Value | | --- | --- | | Pay to | `0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee` | | Token | USDC, 6 decimals, `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | | Chain | `eip155:8453` (base) | | x402 scheme | `exact` | | x402 version | 1 | | Rail A header | `X-Payment-Tx` | | Rail B header | `X-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: | Offer | Send | Base units | Wallet link (EIP-681) | | --- | --- | --- | --- | | `summarize` | $0.10 | 100000 base units | `ethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee&uint256=100000` | | `extract` | $0.15 | 150000 base units | `ethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee&uint256=150000` | | `classify` | $0.05 | 50000 base units | `ethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee&uint256=50000` | | `shorten` | $0.05 | 50000 base units | `ethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee&uint256=50000` | | `gas-price` | $0.02 | 20000 base units | `ethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee&uint256=20000` | | `fetch-page` | $0.10 | 100000 base units | `ethereum: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 ```bash curl -X POST https://short.desknav.ai/jobs/summarize \ -H 'Content-Type: application/json' \ -H 'X-Payment-Tx: 0x' \ -d '{"text": "Paste the document to summarize here."}' ``` A successful reply looks like this: ```json { "invoiceId": "inv_", "offerId": "summarize", "status": "settled", "rail": "direct", "txHash": "0x", "output": { "offerId": "summarize", "result": "The summary text.", "model": "", "inputThreatLevel": "low" } } ``` - **One transaction buys one job.** The hash is the claim key, so a second attempt with the same hash comes back `REPLAYED_TX` (or `409 REPLAYED` if two attempts race for it — the claim is atomic). Claim your transfer promptly. - **Split payments are summed.** Every USDC transfer to the pay-to address inside one transaction counts toward the price. - **Overpayment is not refunded** and does not carry over. Send the price. - **Your transfer never expires.** A confirmed transaction can be claimed whenever you get to it. - If you send both headers, `X-Payment` wins and `X-Payment-Tx` is ignored. ### When Rail A refuses The reply is `402` with `{"error":"PAYMENT_REJECTED","reason":""}`. The code says exactly what to do: | Code | What happened | What to do | | --- | --- | --- | | TX_MALFORMED | The header is not 0x plus 64 hex characters. | Send the transaction hash, not a URL or a block explorer link. | | RECEIPT_NOT_FOUND | The 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_REVERTED | The transaction failed; no funds moved. | Send a new transfer. | | NO_MATCHING_TRANSFER | No USDC transfer to the pay-to address in that transaction. | Check the token contract and the recipient. Only `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` to `0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee` counts. | | INSUFFICIENT_AMOUNT | The transfer is below the price. | Send another transfer covering the full price, and quote that one. | | REPLAYED_TX | That transaction already bought a job. | Pay again for a new job. | | RPC_ERROR | We 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 ```bash 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: ```json { "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`. ```json { "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: ```json { "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 field | What it must be | | --- | --- | | from | Your wallet. The signature must recover to this address, and it is the address the USDC leaves. | | to | Exactly `0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee`. Anything else is `WRONG_RECIPIENT`. | | value | At least `maxAmountRequired` from the 402, as a decimal string of base units. | | validAfter | Unix 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. | | validBefore | Unix seconds. Set `now + 1800` — see the deadline rule below. | | nonce | 32 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. - Sign `validBefore = now + 1800` and you are always inside the rule. - Signing a short window to limit exposure does not work here: below `1680s` remaining it is refused outright, before any work happens. - Keep the balance in your wallet until the transfer lands. If the balance is gone at redemption, the on-chain transfer fails and the work has already been delivered. ### 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. ```json { "x402Version": 1, "scheme": "exact", "network": "base", "payload": { "signature": "0x<65-byte-signature>", "authorization": { "from": "0x", "to": "0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee", "value": "100000", "validAfter": "", "validBefore": "", "nonce": "0x<32-random-bytes>" } } } ``` - Every numeric field is a **decimal string**, not a JSON number. A number would already have lost precision. - Do not add `asset`, `token` or `chainId` keys that disagree with the terms above: a contradicting asset is `WRONG_ASSET` and a contradicting chain is `UNSUPPORTED_NETWORK`, even though neither can change what you signed. - `network` may be `base` or `eip155:8453`; both normalize to the same chain. As one command: ```bash 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","to":"0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee","value":"100000","validAfter":"","validBefore":"","nonce":"0x<32-random-bytes>"}}}' \ -d '{"text": "Paste the document to summarize here."}' ``` A successful reply is `200`: ```json { "invoiceId": "inv_", "offerId": "summarize", "status": "delivered", "output": { "offerId": "summarize", "result": "The summary text.", "model": "", "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. ```ts 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":""}` 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. | Code | What happened | What to do | | --- | --- | --- | | MALFORMED_HEADER | The 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_SCHEME | The scheme is not `exact`. | Send `"scheme": "exact"`. | | UNSUPPORTED_NETWORK | The envelope names a chain we are not paid on, or contradicts itself. | Use `base` or `eip155:8453`, and remove any disagreeing chain field. | | WRONG_ASSET | The envelope names a token that is not our USDC. | Remove the asset field or set it to `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. | | AMOUNT_UNPARSEABLE | `value` is not a plain uint256 decimal string. | No 0x, no decimal point, no sign, no whitespace, no exponent. | | BAD_SIGNATURE | The 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_RECIPIENT | The authorization pays somebody else. | Set `to` to `0x3c5BbDEf29f7B6bc5973c77f90c5A3f02D829Eee`. | | INSUFFICIENT_AMOUNT | The authorized value is below the price. | Use `maxAmountRequired` verbatim. | | NOT_YET_VALID | `validAfter` is 60s or more in the future by our clock — 60s is the whole skew tolerance, not a bonus. | Use `now - 60`. | | EXPIRED | `validBefore` has passed. | Sign a fresh authorization; there is no tolerance on this one. | | DEADLINE_TOO_SOON | Still valid, but under `1680s` remain — we could not redeem it. | Sign `now + 1800` and send promptly. | | REPLAYED_NONCE | That 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 | Status | Body | Meaning | | --- | --- | --- | | 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](https://short.desknav.ai/offers). | | 405 | `{"error":"METHOD_NOT_ALLOWED"}` | Buying is a POST. `GET https://short.desknav.ai/jobs/` 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 - [https://short.desknav.ai/.well-known/agent-card.json](https://short.desknav.ai/.well-known/agent-card.json) — every offer, both prices, both rails, the pay-to address and the chain, in one document. - [https://short.desknav.ai/offers](https://short.desknav.ai/offers) — the live catalog, ids and prices only. - `GET https://short.desknav.ai/jobs/` — one offer's terms, including the `accepts` array, without having to trigger a 402. - [https://short.desknav.ai/docs.md](https://short.desknav.ai/docs.md) — this page as Markdown. - `GET https://short.desknav.ai/health` — liveness. It says nothing about wallets or balances.