https://api.decisionnode.com /v1/creditCredit normally comes from the console: a person signs in and pays a top-up. An agent cannot do that, so an agent that runs out of credit stops, and one with no account cannot start. Machine payments remove both stops. The route speaks two protocols: Stripe's Machine Payments Protocol (MPP), with a card through a Shared Payment Token (Stripe's Link agent wallet) or USDC on Tempo, and x402 with USDC on Base. Every payment ends as a Stripe payment, and the credit it buys is the same prepaid credit a console top-up buys: on the key's workspace, drawn down by every call, with a receipt in Billing. A standard MPP client such as Stripe's Link CLI or Tempo's CLI handles the exchange on its own; the sections below are for a client you write yourself.
# Without a key: the challenges for $20.00 of credit
curl -i -X POST https://api.decisionnode.com/v1/credit \
-H "Content-Type: application/json" \
-d '{"amount":"20.00"}'
# With a key: the same, credited to the key's workspace
curl -i -X POST https://api.decisionnode.com/v1/credit \
-H "Authorization: Bearer $DECISIONNODE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount":"20.00"}'How a purchase goes#
- 1.Ask.
POST /v1/creditwith{"amount": "20.00"}, with your API key or without one. The answer is402with oneWWW-Authenticate: Paymentchallenge per method on offer,stripeandtempo, and aPAYMENT-REQUIREDheader for x402. - 2.Pay. Hand one challenge to your payment method: Stripe issues a Shared Payment Token for the amount, a Tempo wallet broadcasts the USDC transfer to the address in the challenge, an x402 wallet signs the USDC authorization.
- 3.Retry with the credential. The same call again, with the challenge echoed and the method's payload in
Payment-Authorization(MPP) or the signed payload inPAYMENT-SIGNATURE(x402). The body is ignored on this call: the amount is the challenge's. - 4.Read the answer.
200with the workspace, the credit now on it, the receipt and, for a keyless purchase, the key. ThePayment-ReceiptorPAYMENT-RESPONSEheader is the protocol's own receipt.
The challenge#
Without a credential the route answers 402 Payment Required with Cache-Control: no-store, a body of type application/problem+json (RFC 9457) whose detail names the amount and the methods, and one WWW-Authenticate line per method, in the order stripe, tempo. Each line is the Payment scheme of the Payment HTTP authentication draft with these parameters, every value a quoted string:
WWW-Authenticate: Payment
idstring- An HMAC-SHA256 over the other parameters (
descriptionexcepted), base64url. The server verifies the challenge you echo against it, so a changed amount, address or expiry is refused. Opaque to you. realmstringapi.decisionnode.com.methodstringstripe(a card through a Shared Payment Token) ortempo(USDC on Tempo).intentstringcharge, always: one payment of the amount.requeststring- The method's request object, JSON canonicalized (RFC 8785) and base64url without padding; decoded below. Hand it to your payment method as it is.
descriptionstring- "DecisionNode prepaid credit, $20.00. Paying accepts https://decisionnode.com/legal/terms": the amount, and that paying accepts the Terms of Service.
expiresstring- RFC 3339, 10 minutes after the challenge was made. Past it, the credential is
402payment_expired: ask for a fresh challenge. headerstringPayment-Authorization: the request header the credential goes in. The API key keepsAuthorization; a credential sent there satisfies nothing.opaquestring- A nonce and, for a key holder, the workspace the credit will land on (
{"n", "ws"}), base64url. Echo it as it is: a challenge is good only for the caller it was issued to.
{
"amount": "2000",
"currency": "usd",
"description": "DecisionNode prepaid credit",
"methodDetails": {
"networkId": "profile_...",
"paymentMethodTypes": ["card", "link"]
}
}| Method | Amount in request | Where the money goes |
|---|---|---|
stripe | amount in cents as a string ("2000"), currency usd | methodDetails.networkId is DecisionNode's Stripe profile; paymentMethodTypes are card and link. Stripe issues the token against that profile. |
tempo | amount in USDC base units, six decimals ("20000000"); currency is the USDC.e token on Tempo | recipient is DecisionNode's deposit address on Tempo mainnet (methodDetails.chainId 4217). Push mode only (supportedModes): your wallet broadcasts the transfer and pays its own network fee. |
| x402 | accepts[0].amount in USDC base units; asset is USDC on Base (network eip155:8453) | payTo is DecisionNode's deposit address on Base; scheme exact, the EIP-3009 transfer method; maxTimeoutSeconds 600. |
Read the addresses from the challenge every time, never from a note of your own: they are part of what the id binds, and a payment to any other address buys nothing. The PAYMENT-REQUIRED header is standard base64 of the x402 v2 document shown above; it carries no workspace, since x402 identifies the workspace by the API key alone.
The credential#
For MPP, send Payment-Authorization: Payment <base64url>, the base64url without padding of a JSON object with challenge (the challenge's parameters echoed as they came, request and opaque still their wire strings) and payload (the method's), at most 16 KiB. For x402, send PAYMENT-SIGNATURE: <base64> of the x402 payment payload. Send one or the other: both at once is 400 api_usage_error, "Send one payment credential."
{
"challenge": {
"id": "X6v1eo7fJ76gAxqY0xN9Jd__4lUyDDYmriryOM-5FO4",
"realm": "api.decisionnode.com",
"method": "stripe",
"intent": "charge",
"request": "eyJhbW91bnQiOiIyMDAwIiwiY3VycmVuY3kiOiJ1c2QiLCJkZXNjcmlwdGlvbiI6...",
"expires": "2026-10-26T12:10:00Z",
"header": "Payment-Authorization",
"opaque": "eyJuIjoiUjNmNGtMbTJwUTl6WHZCbk43U3cifQ"
},
"payload": { "spt": "spt_1N4Zv32eZvKYlo2CPhVPkJlW" }
}| Method | Payload | What the server does with it |
|---|---|---|
stripe | {"spt": "spt_..."}: a Shared Payment Token for the challenge's amount, issued by Stripe to your agent (the Link wallet, or Stripe's agent toolkit); externalId optional | Confirms a Stripe payment with the token for the challenge's amount. succeeded books the credit. A token that needs an authentication (3-D Secure) is 402 payment_action_required: a machine cannot do one. |
tempo | {"type": "hash", "hash": "0x<64 hex>"}: the hash of your transfer of the amount to the challenge's recipient, after you broadcast it | Asks Stripe to verify that the transaction paid the deposit address at least the amount. While the transaction is still being verified the answer is 529 with Retry-After: 5: send the same credential again, and it resumes where it was. A pull-mode transaction payload or a proof is 402 invalid_payload. |
| x402 | The x402 v2 payment payload: accepted (the accepts entry, amount set to what you signed) and payload with the EIP-3009 signature and authorization (from, to, value, validAfter, validBefore, nonce) | Checks that accepted is ours (network, asset, payTo), that accepted.amount equals authorization.value and lies within $5.00 to $5,000.00, then verifies and settles the transfer through a facilitator and records it at Stripe. The amount you signed is the amount credited; it need not equal an earlier challenge's. Permit2 and ERC-7710 payloads are refused. |
Checked on every credential: the id binds the echoed parameters (a tampered challenge fails); expires has not passed; realm and header are ours; the challenge was issued to this caller (a keyless challenge presented with a key, a key holder's without one, or another workspace's, is refused); and the payment reference (the token, the transaction hash, the x402 nonce) has not been presented before. Only Stripe's confirmation books credit: a settled amount or currency other than the challenge's, a replayed payment, or a facilitator's word alone book nothing.
The answer#
200 OK with Cache-Control: private, no-store, and the protocol's receipt in a header: Payment-Receipt for MPP (base64url JSON: method, reference, status, timestamp; the reference is the Stripe payment's id for stripe and the transaction hash for tempo), PAYMENT-RESPONSE for x402 (standard base64 JSON: success, transaction, network, payer).
Response body
workspace_idstring- The workspace the credit landed on: your key's, or the new agent workspace of a keyless purchase.
creditobject- What was booked.
added_microsinteger- The amount, in micro-dollars (
20000000is $20.00). balance_microsinteger- The workspace's balance after the booking, live.
expires_atstring- When this credit expires: 12 months after purchase. Credit is used oldest first.
receiptobject- The receipt, as the console lists it under Billing.
numberstring- The receipt number,
DN-and digits; the PDF is in the console. payment_intentstring- The Stripe payment's id,
pi_.... methodstring- How it was paid, as the receipt says it: the card's brand and last four (
Visa 4242),Link card,USDC on TempoorUSDC on Base.
keyobject- Only on a keyless purchase, and only once: the new workspace's key.
idstringkey_....secretstring- The key,
dn_live_.... It is shown here and nowhere else; store it at once. scopestringfull: a Full access key.
HTTP/1.1 402 Payment RequiredCache-Control: no-storeContent-Type: application/problem+jsonWWW-Authenticate: Payment id="X6v1eo7fJ76gAxqY0xN9Jd__4lUyDDYmriryOM-5FO4", realm="api.decisionnode.com", method="stripe", intent="charge", request="eyJhbW91bnQiOiIyMDAwIiwiY3VycmVuY3kiOiJ1c2QiLCJkZXNjcmlwdGlvbiI6...", description="DecisionNode prepaid credit, $20.00. Paying accepts https://decisionnode.com/legal/terms", expires="2026-10-26T12:10:00Z", header="Payment-Authorization", opaque="eyJuIjoiUjNmNGtMbTJwUTl6WHZCbk43U3cifQ"WWW-Authenticate: Payment id="S91xi-OFGZPMs-j7GsX0FDpIkmCcZT1P9XyV58WNy_U", realm="api.decisionnode.com", method="tempo", intent="charge", request="eyJhbW91bnQiOiIyMDAwMDAwMCIsImN1cnJlbmN5IjoiMHgyMGMwMDAw...", description="DecisionNode prepaid credit, $20.00. Paying accepts https://decisionnode.com/legal/terms", expires="2026-10-26T12:10:00Z", header="Payment-Authorization", opaque="eyJuIjoiOUhxMlRwV2M3WGtMbTRSalZuMmEifQ"PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6MiwiZXJyb3IiOiJQQVlNRU5ULVNJR05BVFVSRSBoZWFkZXIgaXMgcmVxdWlyZWQiLCJyZXNvdXJjZSI6...{"type":"https://paymentauth.org/problems/payment-required","title":"Payment Required","status":402,"detail":"$20.00 of DecisionNode credit. Pay with a Stripe Shared Payment Token, USDC on Tempo, USDC on Base (x402).","error_type":"payment_required"}{
"method": "stripe",
"reference": "pi_3SLx8k2eZvKYlo2C1aBcDeFg",
"status": "success",
"timestamp": "2026-10-26T12:04:32Z"
}# Without a key: the challenges for $20.00 of creditcurl -i -X POST https://api.decisionnode.com/v1/credit \ -H "Content-Type: application/json" \ -d '{"amount":"20.00"}'# With a key: the same, credited to the key's workspacecurl -i -X POST https://api.decisionnode.com/v1/credit \ -H "Authorization: Bearer $DECISIONNODE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"amount":"20.00"}'Amounts#
- $5.00 to $5,000.00 a purchase, in whole cents: the body's
amountis a decimal string in USD with at most two decimals,{"amount": "20.00"};"7"is $7.00. Anything else,"4.99"or"5000.01"included, is400api_usage_error, "amount must be 5.00 to 5000.00 USD in whole cents." The body is optional and at most 4 KiB. - $5.00 without a body, and on the challenges a
402of/v1/decidecarries. Buy more in one go for a long job: a purchase is one Stripe payment, with Stripe's own minimum per charge, so a decision is never paid one at a time. - The amount is the credit. Nothing is added on top: no fee, and no tax is collected on a machine payment. The receipt shows the method and tax 0.
- On the retry the body is ignored: what is charged is the challenge's amount, bound by its
id. To buy another amount, ask for a fresh challenge. - Expiry. Credit bought this way expires 12 months after purchase and is used oldest first, as every top-up (Credit, refunds and disputes). It is prepaid credit under the Terms of Service, which the challenge's
descriptionnames and the payment accepts. - No referral discount, no auto reload. There is no Checkout, so a referral code's first-top-up discount does not apply and no card is saved: an agent that runs low pays again. The console's own top-ups and auto reload work on the same workspace as before.
Without a key#
A call without Authorization is a keyless agent. Its challenges carry no workspace (opaque is {"n"} alone), and a successful payment makes an agent workspace: prepaid, without a free tier, without members, named "Agent" and eight hex digits, with one Full access key, which the answer's key field shows once. The money is the only gate: no email, no sign-up, no console, since nobody can sign in to it. Keep the key: an agent that loses it cannot recover it, and buys again.
{
"workspace_id": "ws_5b8d2f1e-3c7a-4e9b-8d6c-1a4f7e2b9c03",
"credit": {
"added_micros": 5000000,
"balance_micros": 5000000,
"expires_at": "2027-10-26T12:04:32Z"
},
"receipt": {
"number": "DN-100124",
"payment_intent": "pi_3SLx9q2eZvKYlo2C0ZyXwVuT",
"method": "Link card 4242"
},
"key": {
"id": "key_9f2b7c1d4e6a4b8c",
"secret": "dn_live_...",
"scope": "full"
}
}- Use the key like every Full access key:
POST /v1/decide,GET /v1/models, uploads, sessions and collections, under a prepaid workspace's default limits and the default safety policy. Top it up with the same key: a key holder's challenges carry the workspace, and the credit lands on it. - Caps per client address: 20 challenges a minute and 20 settled purchases a UTC day; past either,
429rate_limit_errorwithRetry-After: 60. A key holder is under its workspace's usual rate limits instead. - An agent workspace is a workspace: its receipts, its usage and its balance are kept like every other workspace's, and DecisionNode may pause or close it under the Terms. A payment dispute freezes it like every workspace.
Paying on /v1/decide#
When an agent workspace's balance is empty, POST /v1/decide answers 402 insufficient_credit with details.reason balance_empty (Errors). That answer also carries the challenges of this page, for $5.00, bound to the key's workspace, with Cache-Control: no-store; its body and content type stay the contract's. A standard MPP client that meets it pays and retries the same call, and that works: the same request with the credential settles the payment, books the credit, then runs the decision, and the answer carries Payment-Receipt beside its own headers. The credit is booked whatever the decision then answers, so a 422 on the request still bought the credit. A plan's allowance_used carries the same challenges; plan_required and trial_allowance_used are not a payment matter and carry none.
HTTP/1.1 402 Payment RequiredCache-Control: no-storeContent-Type: application/jsonWWW-Authenticate: Payment id="CJ4X1O4aTDmS59hfdhnhBtxIQjWDOf0bcrhsswwMOW8", realm="api.decisionnode.com", method="stripe", intent="charge", request="eyJhbW91bnQiOiI1MDAiLCJjdXJyZW5jeSI6InVzZCIsImRlc2NyaXB0aW9uIjoi...", description="DecisionNode prepaid credit, $5.00. Paying accepts https://decisionnode.com/legal/terms", expires="2026-10-26T12:10:00Z", header="Payment-Authorization", opaque="eyJuIjoiWHE3cFI0a0wyTW5WOWpUM0J3Iiwid3MiOiJ3c18wZjNhOWMyZS0uLi4ifQ"WWW-Authenticate: Payment id="...", realm="api.decisionnode.com", method="tempo", intent="charge", request="...", description="DecisionNode prepaid credit, $5.00. Paying accepts https://decisionnode.com/legal/terms", expires="2026-10-26T12:10:00Z", header="Payment-Authorization", opaque="..."PAYMENT-REQUIRED: ...{"detail":{"error_type":"insufficient_credit","message":"Your prepaid balance is empty. Add credit in the console to continue.","details":{"reason":"balance_empty"}}}- Only when blocked. A credential on a decision call whose workspace has credit is not read: the decision runs as usual and nothing is charged. To buy ahead, call
/v1/credit. - A refused credential on
/v1/decideis the problem body of Refusals, with fresh challenges, and no decision runs. A suspended workspace is403before any payment is looked at. - Every other route's
402(uploads, sessions, collections, knowledge) carries the same challenge headers and nothing else changes; pay at/v1/credit, then retry it.
Refusals#
A credential that cannot be settled is refused with the draft's problem body: application/problem+json, type under https://paymentauth.org/problems/https://paymentauth.org/, title, status, a detail sentence, and error_type, the same code with underscores, to branch on. Every refusal has Cache-Control: no-store, and a 402 carries fresh challenges beside it. The calls that never reach a settlement (a bad amount, the wrong key, a suspended workspace) answer the API's usual {"detail": {...}} body (Errors).
{
"type": "https://paymentauth.org/problems/invalid-challenge",
"title": "Invalid Payment Challenge",
"status": 402,
"detail": "This payment was already applied.",
"error_type": "invalid_challenge"
}| Status | error_type | When | What to do |
|---|---|---|---|
402 | payment_required | No credential: the challenge itself, not a refusal | Pay one of the challenges and retry |
402 | malformed_credential | Not Payment <base64url>, not JSON, over 16 KiB, or a challenge without its required parameters | Fix the encoding; echo the challenge whole |
402 | invalid_challenge | The id does not bind what was echoed; realm or header is not ours; the challenge was issued to another caller or workspace; or the payment was presented before ("This payment was already applied."). One still being settled by another call is "This payment is being settled; retry in a moment." with Retry-After: 2 | Take a fresh challenge with this key; never present a spent token or hash again |
402 | payment_expired | Past expires (10 minutes) | Take a fresh challenge |
400 | method_unsupported | A method or intent the challenges did not offer | Use a method from the challenges |
402 | invalid_payload | The method's payload is missing or malformed: spt not a Shared Payment Token id, hash not 32 bytes of hex, a Tempo transaction (pull mode: "This server takes push mode: broadcast the transfer and send its hash.") or proof payload | Fix the payload |
400 | invalid_payload | An x402 payload that is not ours: another network, asset or payTo, accepted.amount unequal to authorization.value or outside the bounds, a Permit2 or ERC-7710 transfer. PAYMENT-RESPONSE says success: false, errorReason invalid_payload | Sign the accepts entry as it came, for an amount within the bounds |
402 | verification_failed | Stripe did not confirm the payment: a declined token ("The payment was declined (<code>)."), a transfer it could not find or that paid less; the x402 facilitator found the payment invalid (PAYMENT-RESPONSE errorReason, such as insufficient_funds); or the settled amount or currency is not the challenge's | Pay with another card, wallet or amount; nothing was credited |
402 | payment_action_required | The card needs an authentication (3-D Secure), which a machine cannot do; Retry-After: 60 | Use another card, or a stablecoin |
402 | payment_insufficient | The challenge's amount is under $5.00; never issued by this server, so only a tampered challenge, which fails the binding first | Take a fresh challenge |
500 | internal_payment_error | An x402 payment settled on chain that Stripe would not record. The detail names the transaction; the payment is never charged again, and nothing is credited until an operator reconciles it | Write to hello@bynn.com with the transaction hash |
529 | overloaded_error | Stripe or the facilitator cannot answer, or refuses our key; Retry-After: 2, 5 while a stablecoin payment is still being verified, 15 while an x402 settlement is pending | Send the same credential again after Retry-After: a payment already made is found and resumed, never made twice |
429 | rate_limit_error | A keyless caller past 20 challenges a minute or 20 purchases a day per client address; Retry-After: 60 | Wait, then retry; or call with a key |
400 | api_usage_error | An amount outside $5.00 to $5,000.00 or not in whole cents; both credential headers at once; a dedicated workspace ("Dedicated workspaces are invoiced monthly and take no prepaid credit.") | Fix the request; the message says what |
403 | key_scope_denied | A Decisions-only or Publishable key | Call with a Full access or Manage device keys key, or without a key |
403 | workspace_suspended | The workspace is paused, or frozen after a payment dispute; no payment lifts it | Check the console or write to hello@bynn.com |
405 | Any method but POST; Allow: POST | Use POST |
A 529 also carries retry-after-ms. A payment that was made but whose answer you lost is safe to present again: the same token or hash finds its own record and the answer is the booking it made, or invalid_challenge "This payment was already applied." once booked. Credentials, tokens and signatures are never logged, never in a receipt and never in an audit entry.
Discovery#
GET https://api.decisionnode.com/ is public, needs no key and is cached a minute: an OpenAPI 3.1 document in the shape of the payment discovery draft, so an agent or a registry finds where and how to pay. x-service-info names the docs, this page's llms.txt included; paths./ lists one offer per method on (amount null: the body sets it), and paths./v1/decide.post carries the same offers with the note that a decision is paid from credit. Offers appear and go with the methods the deployment has on; read the document rather than assuming all three.
{
"openapi": "3.1.0",
"info": { "title": "DecisionNode API", "version": "..." },
"servers": [
{ "url": "https://api.decisionnode.com" }
],
"x-service-info": {
"categories": ["developer-tools", "compute"],
"docs": {
"homepage": "https://decisionnode.com/docs",
"llms": "https://decisionnode.com/llms.txt",
"apiReference": "https://decisionnode.com/docs/api"
}
},
"paths": {
"/v1/credit": {
"post": {
"operationId": "buyCredit",
"x-payment-info": {
"offers": [
{
"intent": "charge",
"method": "stripe",
"amount": null,
"currency": "usd",
"description": "A card through a Stripe Shared Payment Token. ..."
},
{
"intent": "charge",
"method": "tempo",
"amount": null,
"currency": "0x20c000000000000000000000b9537d11c60e8b50",
"description": "USDC on Tempo (chain 4217), push mode. ..."
},
{
"intent": "charge",
"method": "x402",
"amount": null,
"currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"description": "x402 exact on eip155:8453 (USDC), PAYMENT-SIGNATURE header. ..."
}
]
}
}
},
"/v1/decide": {
"post": {
"operationId": "decide",
"x-payment-info": {
"offers": ["..."]
}
}
}
}
}