Klima x402 Endpoint
Co-developed by Carbonmark, this endpoint provides functionality to retire tokenized carbon credits on the Base blockchain through the Klima Protocol Retirement Aggregator straight from an AI agent or any HTTP client.
The endpoint discovers carbon liquidity, delivers live price quotes, hands back unsigned [approve, retire] calldata, executes retirement transactions, and resolves the public Carbonmark certificate once the transaction confirms.
- Chain
- Base mainnet (chainId=8453)
- Auth
- Free GET or POST; on-chain protocol fee only
- Pin a version
- v1.x402.klimalabs.com
- Agent manifest
- /.well-known/x402.json
Why x402
The endpoint is built for the x402 agent-payments ecosystem and plugs directly into Base MCP: an agent reads the catalog, prepares a retirement, and submits the batch through the user's Base Account wallet in a single approval. See the Base MCP setup guide for the connection steps.
All HTTP calls are free today. Use GET with query params on action paths (/discover, /quote, etc.) or POST JSON to /api with an action field — both return the same responses.
Two ways to retire:
- Build-your-own (free GET):
discover→quote→prepare/retirehands back unsigned[approve, retire]calldata that you submit from a Base Account (e.g. via Base MCP). Reads are free; you pay gas and submit the batch yourself. - Paid relay (sign once, no gas, no Base account):
prepare-auth→ sign one EIP-712 token authorization →actions/retire. A Klima executor relays the retirement on-chain and pays the gas, reimbursed from your signed budget. Any third-party wallet or agent can do this — see Paid retire (relay) below.
The endpoints
Reads never move funds. Where a row lists GET or POST, POST sends {"action":"…", …params} to /api. This table is generated from the x402 manifest, so it always matches the live API.
| Method | Path | Fee | Purpose |
|---|---|---|---|
| GET / POST | /discover | free | List retirable carbon classes, credits, and reference prices. |
| GET / POST | /quote | free | Live on-chain price for a tonnage in USDC or kVCM. |
| GET / POST | /prepare/retire | free | Unsigned approve + retire batch for atomic submission. |
| POST | /prepare-auth | free | Build the EIP-712 authorization to sign for a relayed retirement. |
| POST | /actions/retire | variable USDC | Relay a signed authorization on-chain and return the certificate. |
| GET / POST | /certificate | free | Resolve Carbonmark certificate URL(s) after confirmation. |
| GET | /api/catalog | 0.001 USDC | Paid snapshot of the most liquid supply: top classes and listings with prices. The same data is free via discover. |
| GET | /api/health | free | Liveness probe — always 200, never charges. |
| GET | /.well-known/x402.json | free | Agent-discovery manifest for x402 directories. |
1. GET or POST /discover. Discover retirement options.
GET /discover[?carbonClass=0x...][&creditToken=0x...][&maxUsdcPricePerTonne=20]
POST /api {"action":"discover", ...same optional filters}Lists protocol carbon classes, marketplace listings and their associated credits, each with a reference USDC/tonne price. Filters are optional and AND-combined (maxUsdcPricePerTonne=20 means ≤ $20/tonne).
priceUsdcPerTonne is the marginal/spot price (accurate near 1 tonne). Large orders walk up the AAM curve e.g. Biochar quoted ~$107.82/t at 1 t but ~$386.61/t for 100 t (≈ 58% of pool liquidity). Always call /quote for the true cost of your size.Marketplace Listing Note: Marketplaces may have different prices for the same credit, depending on the listing.
2. GET or POST /quote — live price
GET /quote?chainId=8453&inputToken=0x...&carbonClass=0x...&amount=1.5
[&creditToken=0x...][&vintage=2022][&tokenId=<id>]
POST /api {"action":"quote", "chainId":8453, ...same params}Returns the retirement price, on-chain fee, total (price + fee), suggestedMaxInput (total + 4% slippage), a humanSummary, the resolvedCredit the server selected (if no credit is specified), and alternatives.
When you don't pin a credit, the API picks the most liquid credit in the class that can cover the selected retirement amount.
{
"tonnesFormatted": "1.5",
"retirementPriceFormatted": "19.791336",
"feeFormatted": "0.01",
"totalFormatted": "19.801336",
"suggestedMaxInputFormatted": "20.593389",
"humanSummary": "1.5 tonnes @ 19.791336 USDC + 0.01 USDC fee = 19.801336 USDC (max 20.593389 USDC with 4% slippage)",
"resolvedCredit": { "creditToken": "0xe662…71b8", "tokenId": 0, "vintage": 2021 }
}3. GET or POST /prepare/retire. Returns prepared retirement calldata.
GET /prepare/retire?chainId=8453&inputToken=0x...&carbonClass=0x...&amount=1.5
[&creditToken=0x...][&vintage=2022][&tokenId=<id>]
[&maxInputTokenIn=<atomic>][&details=<urlencoded JSON>]
POST /api {"action":"prepare/retire", "chainId":8453, ...same params}Re-quotes on-chain and returns an ordered batch. The approval transaction for the credit's token standard followed by the retirement to be submitted atomically. The to field and approvalInstructions.spender are both the Settlement Contract. Read them from the response rather than hard-coding.
details is an optional URL-encoded JSON object for certificate metadata. The schema is strict (unknown keys → 400):
| Field | Type | Meaning |
|---|---|---|
| retiringAddress | string | address performing the retirement |
| retiringEntityString | string | retiring-entity display name |
| beneficiaryAddress | string | address credited on the certificate |
| beneficiaryString | string | beneficiary display name — shows on the certificate |
| retirementMessage | string | public message on the certificate |
| beneficiaryLocation | string | Toucan Puro only — required for Puro credits |
| consumptionCountryCode | string | Toucan Puro only — required for Puro credits |
| consumptionPeriodStart | string | number | Toucan Puro only — required for Puro credits |
| consumptionPeriodEnd | string | number | Toucan Puro only — required for Puro credits |
Attribution tip: beneficiaryString and retirementMessage are what make a certificate named. The certificate cannot be edited after the retirement confirms, so set them up front. (The certificate's on-chain retiringAddress reflects an internal settlement/relayer address, not the details.retiringAddress you pass.)
4. GET or POST /certificate. Returns public proof of retirement.
GET /certificate?txHash=0x...[&index=0]
POST /api {"action":"certificate", "txHash":"0x...", ...optional index}After the retirement confirms, returns the shareable Carbonmark certificate URL(s) for the transaction. index selects one retirement out of a multi-retirement transaction; omit it for all.
{
"retirementCount": 1,
"retirements": [{
"certificateUrl": "https://app.carbonmark.com/retirements/id/8453-0x4a7f…f4bf-0",
"amountInTonnes": "1",
"beneficiaryAddress": "0x1234567890123456789012345678901234567890",
"beneficiaryName": "testing",
"projectId": "UCR-423",
"creditId": "UCR-423-2022"
}]
}beneficiaryAddress is the address credited on-chain; check this one to verify attribution. retiringAddress on the same entry is the Retirement Aggregator's settlement address, not the party the retirement was made for.
A 404 retirement_not_found right after confirmation just means the subgraph hasn't indexed yet; retry in a few seconds.
Paid retire (relay). Sign once, no gas.
The relay path lets any wallet or agent retire without holding native ETH, without a prior token approval, and without a Base Account. You sign one standard EIP-712 token authorization; a Klima executor submits the on-chain transaction and is reimbursed for gas out of your signed budget.
TransferWithAuthorization (USDC) or EIP-2612 Permit (kVCM). Any x402-style payment will be able to handle either authorization, so client signing stays plain eth_signTypedData_v4 with no custom typed data to assemble.nonce is not random: it is keccak256(retirement, salt) — the exact credit, token id, amount, and attribution being authorized, plus a fresh salt the server mints per authorization. Because nonce is one of the six signed EIP-3009 fields, your signature covers what gets retired and who is credited, not just the dollar value. actions/retire rebuilds the retirement, re-hashes it, and returns 400 params_mismatch if it isn't the one you signed. Therefore it's impossible for anything between you and the endpoint to swap the retirement out after you sign.Post
actionsRetireRequest back verbatim, salt included. The kVCM (EIP-2612) path carries no commitment: Permit's nonce is the token's own counter, with no caller-chosen field to bind to.Flow:
1. POST /api prepare-auth → server resolves + prices, returns:
• typedData (EIP-712 object to sign)
• actionsRetireRequest (ready-to-send body,
incl. `salt` on the USDC path)
2. wallet signTypedData(typedData) ← the ONLY signing step
3. POST /api actions/retire (body + signature) → executor relays on-chain
4. POST /api certificate { txHash } → public proof (poll if pending)prepare-auth is the 200 alias of the 402 challenge that actions/retire returns when posted without an authPayload; either entry point gives you the same typedData. The signed budget (authValue) covers retirement + protocol fee + executor gas reimbursement, slippage-buffered. The signer needs only an input-token balance (USDC or kVCM). No ETH required.
Client SDK
The four steps are wrapped in a tiny, zero-dependency TypeScript client — sdk/klima-retire.ts. Drop the single file into your project (or npm i @klimadao/x402-retire when published. The API is the same). It's signer-agnostic: you pass a signTypedData callback, so it works with viem, ethers, a browser wallet, or an agent's signer.
import { createClient } from "./klima-retire";
import { privateKeyToAccount } from "viem/accounts"; // or ethers, or a browser wallet
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const klima = createClient(); // https://x402.klimalabs.com, chainId 8453
const { status, transactionHash, retirements } = await klima.retire({
from: account.address,
amount: "1",
carbonClass: "0xf4699531e0a5f6e9351a36de3753deaad329bf45", // from discover()
inputToken: "usdc", // or "kvcm" / an address
details: {
beneficiaryString: "Acme Corp", // shows on the certificate
beneficiaryAddress: "0x…", // credited on-chain, permanent
retirementMessage: "Net-zero 2026",
},
signTypedData: (td) => account.signTypedData(td as any), // your wallet's signer
});
for (const r of retirements) console.log(r.amountInTonnes, "t →", r.certificateUrl);details.beneficiaryAddress (the party the retirement is for), or beneficiaryIsPayer: true to credit the paying wallet deliberately. Omitting both returns 400 attribution_required. The beneficiary is indexed on-chain as a permanent grouping key and cannot be changed once the retirement confirms.retire() runs the whole flow: prepare-auth → sign → submit → poll the certificate and returns once settled (or pending_index with a transactionHash you resolve later). The same client also exposes discover(), quote(), and certificate().
actions/retire responses
| status | Meaning |
|---|---|
| settled | Mined and indexed — retirements[] carries the certificate URL(s). |
| pending_index | Mined (or broadcast) but subgraph not caught up — poll /certificate with transactionHash. |
| 400 unsupported_chain_id | chainId is not a supported network. Use 8453 (Base mainnet) or 84532 (Base Sepolia). |
| 400 unsupported_input_token | inputToken is not an accepted payment token on this chain. Use the USDC or kVCM address for the chain — see the manifest, or the addresses in the endpoint documentation. |
| 400 invalid_input_token | inputToken passed validation but matches neither settlement path (EIP-3009 USDC nor EIP-2612 kVCM), so no relay function applies. Use the chain's USDC or kVCM address. |
| 404 no_candidates | No credit in the carbon class matched the request filters, or the class holds no credits. Call discover to list live classes and credits, then retry with a carbonClass/creditToken from that response. Retryable because class inventory changes. |
| 404 credit_not_found | No credit carries the requested credit id, or no carbon class registers the one that does. Use a creditId from discover. If the error names a listingId, the credit is only available on the marketplace: retry with that listingId instead of credit. Retryable because class registration changes. |
| 409 credit_ambiguous | The requested credit id maps to more than one retirement route, so its price is not determined. Pick one of the routes in the error's carbonClasses or candidates and retry with carbonClass (plus creditToken if given). The server will not choose a price on your behalf. |
| 400 vintage_not_found | No credit in the class carries the requested vintage. Pick one of the years in the error's availableVintages, or omit vintage to let the server choose a liquid credit. |
| 422 insufficient_liquidity | The pool cannot fill the requested amount at any price right now. Reduce amount, choose another credit or class, or retry later. Retryable because pool depth changes block to block. |
| 422 amount_not_whole_tonnes | The credit's registry (Puro, ECO) retires in whole tonnes only, and amount has a fractional part. Send an integer amount (e.g. "2", not "2.5"). |
| 422 amount_below_increment | amount is smaller than the credit's minimum retirement unit. Raise amount to at least the minimum reported in the error body. |
| 400 puro_details_required | The credit is Puro-issued, whose registry requires consumption metadata that the request omitted. Add the fields named in the error body to details: beneficiaryLocation, consumptionCountryCode, consumptionPeriodStart, consumptionPeriodEnd. |
| 402 payment_required | Not a failure: the x402 challenge returned when actions/retire is posted without an authPayload. The body carries the EIP-712 typedData to sign and a ready-to-send actionsRetireRequest. Identical in shape to a prepare-auth 200. Sign typedData with the payer wallet, set authPayload.signature (or v/r/s), and POST actionsRetireRequest back — verbatim, including salt on the USDC path. |
| 400 attribution_required | A relayed retirement named no beneficiary. The beneficiary is indexed on-chain as a permanent grouping key and cannot be changed once the retirement confirms, so it is not defaulted silently. Set details.beneficiaryAddress to the party the retirement is for, or set beneficiaryIsPayer: true to credit the paying wallet deliberately. |
| 400 invalid_auth_payload | The authorization is structurally wrong for this request: authPayload.from is not the request from, authPayload.to is not the settlement contract, the payload shape doesn't match the input token's scheme (EIP-3009 for USDC, EIP-2612 for kVCM), or a USDC payload arrived without its top-level salt. Post the actionsRetireRequest from prepare-auth (or the 402 challenge) verbatim, adding only the signature. Do not rebuild the payload by hand. |
| 400 insufficient_authorized_value | The signed authPayload.value no longer covers retirement + fee + executor gas, usually because price or gas moved after signing. Relaying it would revert on-chain. Re-run prepare-auth (or re-request the 402 challenge) to size a fresh budget of at least requiredMinimum, then re-sign. The old authorization is unusable, not merely stale. |
| 400 params_mismatch | The submitted retirement is not the one that was authorized. On the USDC path authPayload.nonce is keccak256 of the retirement plus salt, so the signature binds the credit, amount, and attribution — not just the spend value. The rebuilt struct hashed to something else. Re-post actionsRetireRequest verbatim including creditToken, tokenId, details, and salt, or re-run prepare-auth and re-sign. A salt is single-use; one from an earlier authorization will not reproduce the nonce. The error echoes expectedNonce, receivedNonce, and the submitted values to diff against. |
| 422 contract_revert | A contract call reverted during simulation, so nothing was broadcast and no funds moved. selector and decoded.errorName identify the revert; contract, function, and args give the call context. Read decoded.errorName. Liquidity and slippage reverts are worth retrying with a fresh quote; validation and permission reverts are not. |
| 422 transaction_reverted | The relayed transaction mined but reverted, typically from a state change between simulation and inclusion. No retirement was recorded. Inspect transactionHash on a block explorer, then re-run prepare-auth and re-sign. The old authorization's nonce may already be consumed. |
| 404 listing_not_found | No marketplace listing with that id, or the seller has since cancelled it. Listings are removed as well as expired, so a previously valid id can stop resolving. Re-read discover.marketplaceListings[] and pick a current listingId. Do not retry the same id. |
| 422 listing_expired | The listing exists but its expiration has passed (or the seller deactivated it), so the marketplace will not fill it. Pick another listing for the same credit from discover.marketplaceListings[], or retire the equivalent protocol supply via carbonClass. |
| 422 below_min_fill | The requested amount is below the listing's minimum fill. The seller sets this per listing; it is not a protocol-wide floor. Raise the amount to at least minFill (echoed on the error and on every discover listing), or choose a listing with a smaller minimum. |
| 422 insufficient_listing_supply | The listing has less remaining supply than the requested amount. Partial fills by other buyers reduce it between discovery and settlement. Retry with an amount at or below leftToSell (echoed on the error), or spread the retirement across several listings. |
| 400 marketplace_requires_usdc | Marketplace listings settle in USDC only. kVCM is accepted for protocol supply but not for a listing fill, and would revert on-chain (MarketplaceInputTokenUnsupported). Resubmit with inputToken set to USDC, or retire the equivalent protocol supply via carbonClass to pay in kVCM. |
| 503 gas_estimate_unavailable | The executor's gas reimbursement could not be priced, so the authorization budget cannot be sized. No retirement was attempted. Retry with backoff. Nothing was signed or spent, so the request can be repeated unchanged. |
| 503 gas_limit_exceeds_cap | The simulated retirement needs more gas than the executor's configured ceiling (RELAY_GAS_CAP), so it was not broadcast. Nothing was signed or spent. Retry with backoff; a smaller amount or a less fragmented fill will usually estimate lower. If it persists, the operator needs to raise RELAY_GAS_CAP. |
Runnable examples (USDC + kVCM) live in examples/: a read-only discover + quote, the minimal SDK retire, and a raw-HTTP protocol walkthrough.
Quickstart
Examples for GET paths, POST JSON, TypeScript, and the Base MCP plugin. API calls are free; you sign and broadcast the returned batch yourself. The protocol fee is settled on-chain inside that transaction.
# List carbon classes (optional filters)
curl "https://x402.klimalabs.com/api/discover?maxUsdcPricePerTonne=15"
# Live price for a tonnage
curl "https://x402.klimalabs.com/api/quote?chainId=8453\
&inputToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\
&carbonClass=0xf4699531e0a5f6e9351a36de3753deaad329bf45&amount=1.5"
# Unsigned approve + retire batch (you broadcast it)
curl "https://x402.klimalabs.com/api/prepare/retire?chainId=8453\
&inputToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\
&carbonClass=0xf4699531e0a5f6e9351a36de3753deaad329bf45&amount=1.5\
&details=%7B%22beneficiaryString%22%3A%22Acme%20Corp%22%7D"
# Carbonmark certificate after confirmation
curl "https://x402.klimalabs.com/api/certificate?txHash=0xYOUR_RETIRE_TX_HASH"Inputs & contracts (Base mainnet)
| Contract | Address |
|---|---|
| Input token: USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA029136 decimals |
| Input token: kVCM | 0x00fbac94fec8d4089d3fe979f39454f48c71a65d18 decimals |
| Retirement Aggregator | 0xda0a793d7c32ab80bcdab7f8c725c96db22464f4 |
| AAM | 0x1C24239309398220883207681602BfF4D10fbde1 |
| Settlement Contract | read from the prepare response (to / approvalInstructions.spender) |
Addresses are read from /discover. Amount rules: decimal tonne string, minimum 0.001 t (1 kg). Toucan Puro credits retire in whole tonnes only. Amounts above a credit's liquidity are rejected.
Fees
API calls are free (except for catalog which is for indexing purposes and not required for any part of the workflow). Each retirement bakes in a fee max(floor, feeBps% of cost), floor denominated in USDC (converted to kVCM via the pool when paying in kVCM) which is collected on-chain by the Settlement Contract. It's always included in quote.fee and folded into total. The contract spends exactly retirementCost + fee and refunds any unused slippage budget to the user in the same transaction.
Versioning & releases
The API carries a semantic version — currently 1.3.6. The major is the compatibility contract, and the host is how you pin it.
| Host | Serves |
|---|---|
| x402.klimalabs.com | The latest release. Only moved with a future major bump. |
| v1.x402.klimalabs.com | The v1 major and every future 1.x minor and patch. Never moves to the next major. |
| v0.x402.klimalabs.com | The previous major, kept reachable for callers that have not migrated. Not extended. |
Everything on this page describes v1. Pinning the v<major>. host is the entire mechanism. There is no version header, query parameter, or Accept negotiation, and no way to pin a minor or patch. Within a major you always get the newest build.
How changes are bundled
| Bump | Contains | Reaches a pinned caller? |
|---|---|---|
| patch | Fixes that leave the request and response contract intact. | yes |
| minor | Additive only: new actions, new optional request fields, new response fields. | yes |
| major | Anything a working caller could notice as a break. | no |
Every release is an entry in the machine-readable /.well-known/x402-changelog.json with a breaking boolean, a per-change kind, and the action each change applies to. Breaking releases additionally carry a migration string stating concretely what to change. Both the manifest and the changelog publish apiVersion and a versionedHosts object, so an agent can resolve the right host without reading this table. Poll that rather versus diffing live responses.
Error reference
Every failure and the 402 challenge returns the same response envelope: a string error code, a human message, an x402FacilitatorVersion number, and code-specific context fields (issues on schema_validation, expectedNonce/receivedNonce on params_mismatch, and so on). Match on error, never on the message .
retry means the identical request can succeed later untouched; fix request means change it first. This table is read from /.well-known/x402-errors.json — build error handling against that document rather than this page.
| Request | ||
|---|---|---|
| 400 invalid_json | The request body was not parseable JSON. Send a JSON object with a content-type: application/json header. Note the API takes a single POST body, not form-encoded fields. | fix request |
| 400 unknown_action | The body's action field is missing or is not one of the supported actions. Set action to one of the values in supported (echoed in the error body), or GET the endpoint root for the action index. | fix request |
| 400 schema_validation | The body failed schema validation. issues carries the offending path and reason. Bodies are strict at the top level and inside details, so an unrecognized key is an error rather than being silently dropped. Read issues[].path and issues[].keys. For unrecognized_keys, check the key belongs where you put it — attribution fields go inside details, not at the top level. | fix request |
| 400 marketplace_requires_usdc | Marketplace listings settle in USDC only. kVCM is accepted for protocol supply but not for a listing fill, and would revert on-chain (MarketplaceInputTokenUnsupported). Resubmit with inputToken set to USDC, or retire the equivalent protocol supply via carbonClass to pay in kVCM. | fix request |
| 404 not_found | No route exists at the requested path. This API is a single POST multiplexer: POST the endpoint URL with an action field rather than using per-action paths. The 404 body carries the endpoint and action list. | fix request |
| 404 document_not_found | No documentation document with the requested id (from /api/docs?id=…). Use one of the ids in the error's available list, or fetch the index at /api/docs. | fix request |
| 500 internal_error | An unhandled server-side failure. Retry with backoff. If it persists, report it via the contact in /.well-known/security.txt with the request body. | retry |
| Credit resolution | ||
|---|---|---|
| 400 unsupported_chain_id | chainId is not a supported network. Use 8453 (Base mainnet) or 84532 (Base Sepolia). | fix request |
| 400 unsupported_input_token | inputToken is not an accepted payment token on this chain. Use the USDC or kVCM address for the chain — see the manifest, or the addresses in the endpoint documentation. | fix request |
| 400 invalid_input_token | inputToken passed validation but matches neither settlement path (EIP-3009 USDC nor EIP-2612 kVCM), so no relay function applies. Use the chain's USDC or kVCM address. | fix request |
| 400 vintage_not_found | No credit in the class carries the requested vintage. Pick one of the years in the error's availableVintages, or omit vintage to let the server choose a liquid credit. | fix request |
| 404 no_candidates | No credit in the carbon class matched the request filters, or the class holds no credits. Call discover to list live classes and credits, then retry with a carbonClass/creditToken from that response. Retryable because class inventory changes. | retry |
| 404 credit_not_found | No credit carries the requested credit id, or no carbon class registers the one that does. Use a creditId from discover. If the error names a listingId, the credit is only available on the marketplace: retry with that listingId instead of credit. Retryable because class registration changes. | retry |
| 404 listing_not_found | No marketplace listing with that id, or the seller has since cancelled it. Listings are removed as well as expired, so a previously valid id can stop resolving. Re-read discover.marketplaceListings[] and pick a current listingId. Do not retry the same id. | fix request |
| 409 credit_ambiguous | The requested credit id maps to more than one retirement route, so its price is not determined. Pick one of the routes in the error's carbonClasses or candidates and retry with carbonClass (plus creditToken if given). The server will not choose a price on your behalf. | fix request |
| 422 listing_expired | The listing exists but its expiration has passed (or the seller deactivated it), so the marketplace will not fill it. Pick another listing for the same credit from discover.marketplaceListings[], or retire the equivalent protocol supply via carbonClass. | fix request |
| Amount rules | ||
|---|---|---|
| 400 puro_details_required | The credit is Puro-issued, whose registry requires consumption metadata that the request omitted. Add the fields named in the error body to details: beneficiaryLocation, consumptionCountryCode, consumptionPeriodStart, consumptionPeriodEnd. | fix request |
| 422 insufficient_liquidity | The pool cannot fill the requested amount at any price right now. Reduce amount, choose another credit or class, or retry later. Retryable because pool depth changes block to block. | retry |
| 422 amount_not_whole_tonnes | The credit's registry (Puro, ECO) retires in whole tonnes only, and amount has a fractional part. Send an integer amount (e.g. "2", not "2.5"). | fix request |
| 422 amount_below_increment | amount is smaller than the credit's minimum retirement unit. Raise amount to at least the minimum reported in the error body. | fix request |
| 422 below_min_fill | The requested amount is below the listing's minimum fill. The seller sets this per listing; it is not a protocol-wide floor. Raise the amount to at least minFill (echoed on the error and on every discover listing), or choose a listing with a smaller minimum. | fix request |
| 422 insufficient_listing_supply | The listing has less remaining supply than the requested amount. Partial fills by other buyers reduce it between discovery and settlement. Retry with an amount at or below leftToSell (echoed on the error), or spread the retirement across several listings. | retry |
| Authorization (relay path) | ||
|---|---|---|
| 400 attribution_required | A relayed retirement named no beneficiary. The beneficiary is indexed on-chain as a permanent grouping key and cannot be changed once the retirement confirms, so it is not defaulted silently. Set details.beneficiaryAddress to the party the retirement is for, or set beneficiaryIsPayer: true to credit the paying wallet deliberately. | fix request |
| 400 invalid_auth_payload | The authorization is structurally wrong for this request: authPayload.from is not the request from, authPayload.to is not the settlement contract, the payload shape doesn't match the input token's scheme (EIP-3009 for USDC, EIP-2612 for kVCM), or a USDC payload arrived without its top-level salt. Post the actionsRetireRequest from prepare-auth (or the 402 challenge) verbatim, adding only the signature. Do not rebuild the payload by hand. | fix request |
| 400 insufficient_authorized_value | The signed authPayload.value no longer covers retirement + fee + executor gas, usually because price or gas moved after signing. Relaying it would revert on-chain. Re-run prepare-auth (or re-request the 402 challenge) to size a fresh budget of at least requiredMinimum, then re-sign. The old authorization is unusable, not merely stale. | fix request |
| 400 params_mismatch | The submitted retirement is not the one that was authorized. On the USDC path authPayload.nonce is keccak256 of the retirement plus salt, so the signature binds the credit, amount, and attribution — not just the spend value. The rebuilt struct hashed to something else. Re-post actionsRetireRequest verbatim including creditToken, tokenId, details, and salt, or re-run prepare-auth and re-sign. A salt is single-use; one from an earlier authorization will not reproduce the nonce. The error echoes expectedNonce, receivedNonce, and the submitted values to diff against. | fix request |
| 402 payment_required | Not a failure: the x402 challenge returned when actions/retire is posted without an authPayload. The body carries the EIP-712 typedData to sign and a ready-to-send actionsRetireRequest. Identical in shape to a prepare-auth 200. Sign typedData with the payer wallet, set authPayload.signature (or v/r/s), and POST actionsRetireRequest back — verbatim, including salt on the USDC path. | fix request |
| Settlement | ||
|---|---|---|
| 404 retirement_not_found | No indexed retirement for that transaction hash. Immediately after confirmation this means the subgraph has not caught up yet, not that the retirement failed. Poll every few seconds. If a retirement response returned pending_index, this is the expected interim state. | retry |
| 422 contract_revert | A contract call reverted during simulation, so nothing was broadcast and no funds moved. selector and decoded.errorName identify the revert; contract, function, and args give the call context. Read decoded.errorName. Liquidity and slippage reverts are worth retrying with a fresh quote; validation and permission reverts are not. | retry |
| 422 transaction_reverted | The relayed transaction mined but reverted, typically from a state change between simulation and inclusion. No retirement was recorded. Inspect transactionHash on a block explorer, then re-run prepare-auth and re-sign. The old authorization's nonce may already be consumed. | retry |
| Upstream | ||
|---|---|---|
| 503 gas_estimate_unavailable | The executor's gas reimbursement could not be priced, so the authorization budget cannot be sized. No retirement was attempted. Retry with backoff. Nothing was signed or spent, so the request can be repeated unchanged. | retry |
| 503 gas_limit_exceeds_cap | The simulated retirement needs more gas than the executor's configured ceiling (RELAY_GAS_CAP), so it was not broadcast. Nothing was signed or spent. Retry with backoff; a smaller amount or a less fragmented fill will usually estimate lower. If it persists, the operator needs to raise RELAY_GAS_CAP. | retry |
| 503 payment_unavailable | The paid catalog cannot issue a payment challenge on this deployment because its receiving address or Coinbase facilitator credentials are not configured. Nothing was charged. Use the free GET /api/discover for the full catalog. Operators: set X402_PAY_TO, CDP_API_KEY_ID and CDP_API_KEY_SECRET. | retry |
Every action can additionally return: invalid_json, unknown_action, not_found, document_not_found, schema_validation, internal_error.
On-chain failures all surface as contract_revert, with the specific revert decoded onto the response as selector and decoded.errorName.
Use it from an AI agent
The fastest path is the Klima Base MCP plugin — it wires this endpoint into any Base MCP–capable agent (e.g. Claude Code).
- Install — connect Base MCP and add the Klima plugin by following the Base MCP setup guide. Plugin: plugins/klima-retire.md.
- Ask in plain language — e.g. “Retire 2 tonnes of carbon under $15/tonne, beneficiary ‘Acme Corp’.”
- The agent runs
discover→quote→prepare/retire→ shows you the cost → submits one wallet approval → returns your certificate URL.
Prefer a direct integration (no MCP)? Call the GET or POST endpoints above yourself, then sign and submit the prepare/retire batch (approve + retirement) with your own wallet on Base — or use the gasless paid relay path (sign one authorization, a Klima executor submits it).
⚠ Retirement is irreversible
It permanently burns the carbon credit — no undo, refund, or resale once the transaction confirms. Always review the quoted tonnes, price, and fee before approving.
Resources
Agent plugin + setup docs
Base MCP setup guide, klima-retire plugin, and full integration docs.
Base MCP setup guide
Connect Base MCP and add the Klima retirement plugin.
Discovery manifest
Public x402.json that agent crawlers index automatically.
OpenAPI specification
The same endpoints as an OpenAPI document, for agents and codegen tools that prefer it to x402.json.
Changelog
Machine-readable release history — per-release breaking flag and a migration string on every breaking one.
Error registry
Every error code with its status, group, retryable flag, and remedy — build error handling against this.
Client SDK (klima-retire.ts)
Zero-dependency, signer-agnostic TypeScript client — one retire() call runs the whole relay flow.
Runnable examples
Discover + quote (no wallet), the minimal SDK retire, and a raw-HTTP protocol walkthrough.
Klima Aggregator USAGE
Direct on-chain access for callers who do not want the HTTP layer.
x402 protocol
The HTTP 402 Payment Required protocol this endpoint implements.
Base MCP
Base AI agent quickstart — wallet-connected agents on Base mainnet.