Direct HTTP integration

Build and verify one paid endpoint with the Merchant API and a Base Sepolia wallet.

This is the complete non-SDK path. It uses one tested Python server as the concrete example, but every server call is ordinary HTTP and can be translated to another language. The payer uses Coinbase Agentic Wallet (AWAL).

Before you start

You need Python 3.11 or newer, curl, Node.js 24 or newer, an email address for AWAL, and access to a Recuut seller workspace. The commands use a POSIX shell on macOS or Linux; on Windows, use WSL. Sign in to the Recuut app. If your account has no workspace, create one when prompted; if access is unavailable, ask your Recuut contact for an invitation before continuing.

This guide uses a fixed 0.01 USD test price. Recuut asks the wallet for 10,000 atomic units of Base Sepolia USDC because USDC has six decimal places. The wallet command caps the request at 100,000 atomic units, or 0.10 USDC.

This guide pins AWAL 2.12.1, the version exercised by its tested command snippets. You are ready for the paid retry when the endpoint returns its unpaid 402, AWAL status shows an authenticated wallet, its address command prints an address, and its Base Sepolia balance shows at least 0.01 USDC.

1. Create the test payment policy

In the Recuut app:

  1. Open Resources, choose Create resource, set Kind to API, and name it Text generation API.
  2. Open the resource and choose Create revision. Name it Text generation v1. Add output values with keys input_tokens and output_tokens, label both clearly, choose token as their unit, and publish the revision.
  3. Copy the published revision ID beginning with acr_.
  4. Under Revision pricing, create and publish a fixed Price named Test request, in USD, with a flat fee of 0.01. Turn on Require payment for the revision.
  5. Open Organization → API keys, reveal the test key, and copy it once. Keep it on the server only.

The IDs are generated by Recuut. Your values will differ from examples in the API Reference.

2. Run the protected endpoint

Create an empty directory and install the server dependencies:

python -m venv .venv. .venv/bin/activatepython -m pip install fastapi httpx uvicorn

Set the generated revision ID and test key. PROTECTED_RESOURCE_URL must be the URL the payer calls; the local value below is correct for this guide.

The localhost URL works when AWAL and the server run on the same machine. From a container or remote environment, use an HTTPS URL that the machine running AWAL can reach.

export RECUUT_API_URL="https://api.recuut.com"export RECUUT_API_KEY="recuut_test_..."export RECUUT_REVISION_ID="acr_..."export RECUUT_SETTLEMENT_ASSET="USDC"export PROTECTED_RESOURCE_URL="http://127.0.0.1:8000/generate"

Create the complete server:

direct_http.py
import osfrom time import perf_counterfrom typing import Annotated, Anyimport httpxfrom fastapi import FastAPI, Headerfrom fastapi.responses import JSONResponse, Responsefrom pydantic import BaseModelRECUUT_API_URL = os.environ["RECUUT_API_URL"]RECUUT_API_KEY = os.environ["RECUUT_API_KEY"]RECUUT_REVISION_ID = os.environ["RECUUT_REVISION_ID"]RECUUT_SETTLEMENT_ASSET = os.environ["RECUUT_SETTLEMENT_ASSET"]PROTECTED_RESOURCE_URL = os.environ["PROTECTED_RESOURCE_URL"]app = FastAPI()class GenerateRequest(BaseModel):    prompt: strdef relay_merchant_response(response: httpx.Response) -> Response:    headers = {        name: response.headers[name]        for name in ("PAYMENT-REQUIRED", "Retry-After")        if name in response.headers    }    return Response(        content=response.content,        status_code=response.status_code,        headers=headers,        media_type="application/json",    )def merchant_headers(payment_signature: str | None) -> dict[str, str]:    headers = {"Authorization": f"Bearer {RECUUT_API_KEY}"}    if payment_signature:        headers["PAYMENT-SIGNATURE"] = payment_signature    return headersasync def report_failed_work(    merchant: httpx.AsyncClient,    transaction_id: str,    payment_signature: str,    started_at: float,) -> None:    await merchant.post(        f"/merchant/v1/transactions/{transaction_id}/complete",        headers=merchant_headers(payment_signature),        json={            "outcome": "failed",            "failure_code": "merchant_operation_failed",            "revision_id": RECUUT_REVISION_ID,            "resource_execution": {                "duration_ms": int((perf_counter() - started_at) * 1_000),                "response_status": 500,            },        },    )@app.post("/generate")async def generate(    payload: GenerateRequest,    payment_signature: Annotated[str | None, Header(alias="PAYMENT-SIGNATURE")] = None,) -> Response:    async with httpx.AsyncClient(        base_url=RECUUT_API_URL,        timeout=10,    ) as merchant:        access = await merchant.post(            "/merchant/v1/access",            headers=merchant_headers(payment_signature),            json={                "resource": {                    "url": PROTECTED_RESOURCE_URL,                    "mime_type": "application/json",                },                "revision_id": RECUUT_REVISION_ID,                "settlement_asset": RECUUT_SETTLEMENT_ASSET,            },        )        if access.status_code != 200:            return relay_merchant_response(access)        transaction_id = access.json()["transaction_id"]        if transaction_id is None:            return JSONResponse({"text": payload.prompt.upper()})        if payment_signature is None:            return JSONResponse(                {"code": "payment_signature_missing"},                status_code=502,            )        started_at = perf_counter()        try:            result = {                "text": payload.prompt.upper(),                "transaction_id": transaction_id,            }        except Exception:            await report_failed_work(                merchant,                transaction_id,                payment_signature,                started_at,            )            raise        protected_response = JSONResponse(result)        completion = await merchant.post(            f"/merchant/v1/transactions/{transaction_id}/complete",            headers=merchant_headers(payment_signature),            json={                "outcome": "succeeded",                "revision_id": RECUUT_REVISION_ID,                "resource_execution": {                    "duration_ms": int((perf_counter() - started_at) * 1_000),                    "response_content_type": "application/json",                    "response_size_bytes": len(protected_response.body),                    "response_status": protected_response.status_code,                },                "values": {                    "input_tokens": str(len(payload.prompt.split())),                    "output_tokens": str(len(result["text"].split())),                },            },        )        if completion.status_code != 200:            return relay_merchant_response(completion)        completed: dict[str, Any] = completion.json()        payment_response = completion.headers.get("PAYMENT-RESPONSE")        if completed.get("status") != "succeeded" or not payment_response:            return JSONResponse(                {"code": "payment_not_completed"},                status_code=502,            )        protected_response.headers["PAYMENT-RESPONSE"] = payment_response        return protected_response

This server validates the request, relays Recuut’s 402 response, performs the uppercase operation only after access succeeds, completes the transaction, and releases the result only with a successful PAYMENT-RESPONSE. HTTP 202 is relayed without the protected result. A production operation with side effects must also be idempotent and persist its private result by transaction ID.

Start it in the same terminal that holds the environment variables:

uvicorn direct_http:app --reload

3. Prove the unpaid path

In another terminal, call the endpoint without a wallet:

curl --include \  --request POST \  --url http://127.0.0.1:8000/generate \  --header 'Content-Type: application/json' \  --data '{"prompt":"paid testnet response"}'

The header value will be long, but the response shape should be:

HTTP/1.1 402 Payment RequiredPAYMENT-REQUIRED: <base64 payment instructions>content-type: application/json{"code":"payment_required","message":"Payment authorization is required."}

Stop here if the response is not HTTP 402, if PAYMENT-REQUIRED is missing, or if the uppercase result appears. Check the server key, revision ID, published Price, and Require payment setting before funding a wallet.

4. Create and fund the Base Sepolia wallet

AWAL stores its own key. Sign in by email and enter the flow ID and one-time code returned by the preceding command:

npx [email protected] auth login "[email protected]"npx [email protected] auth verify "<flow-id>" "<one-time-code>"npx [email protected] status

Print the wallet address:

npx [email protected] address

Open the Circle Faucet, select USDC and Base Sepolia, paste that AWAL address, and choose Send 20 USDC. Then check the Base Sepolia balance with the first two commands in the next snippet.

This guide’s fixed exact-USDC payment is gasless, so the AWAL address does not need Base Sepolia ETH. It needs at least 0.01 test USDC. If the balance is still zero, wait for the faucet transaction to confirm, verify that Base Sepolia—not Base—was selected, then check again.

5. Run the paid retry

Set PROTECTED_RESOURCE_URL in this terminal too, then let AWAL make the initial request, read the 402, authorize the payment, and retry:

npx [email protected] statusnpx [email protected] balance --chain base-sepolianpx [email protected] x402 pay "$PROTECTED_RESOURCE_URL" \  -X POST \  -d '{"prompt":"paid testnet response"}' \  --max-amount 100000 \  --json

Approve only if the wallet shows Base Sepolia USDC and no more than 0.10 USDC. A successful endpoint response has this shape:

HTTP/1.1 200 OKPAYMENT-RESPONSE: <base64 payment receipt>content-type: application/json{"text":"PAID TESTNET RESPONSE","transaction_id":"txn_..."}

If the endpoint returns HTTP 202, payment is still settling or reconciling. The response deliberately contains no uppercase result and no PAYMENT-RESPONSE. Keep the transaction ID, wait briefly, and check the saved transaction instead of treating the request as paid.

6. Verify the receipt

Copy the transaction_id from the successful response or pending body into TRANSACTION_ID, then query it with the same server key:

curl --request GET \  --url "$RECUUT_API_URL/merchant/v1/transactions/$TRANSACTION_ID" \  --header "Authorization: Bearer $RECUUT_API_KEY"

Completion is proven only when status is succeeded and receipt is present. The receipt should name test, Base Sepolia (eip155:84532), USDC, the resource, and the 10,000-atomic-unit payment. Pending or failed records have no receipt.

For field-by-field details, use Authorize resource access, Complete a transaction, and Get a transaction. For maintained endpoint adapters, continue with FastAPI or Django.

On this page