# Direct HTTP integration (/guides/http)

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](https://app.recuut.com/). 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:

```bash
python -m venv .venv
. .venv/bin/activate
python -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.

```bash
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:

```python title="direct_http.py"
import os
from time import perf_counter
from typing import Annotated, Any

import httpx
from fastapi import FastAPI, Header
from fastapi.responses import JSONResponse, Response
from pydantic import BaseModel

RECUUT_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: str


def 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 headers


async 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:

```bash
uvicorn direct_http:app --reload
```

## 3. Prove the unpaid path

In another terminal, call the endpoint without a wallet:

```bash
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:

```text
HTTP/1.1 402 Payment Required
PAYMENT-REQUIRED: 
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:

```bash
npx awal@2.12.1 auth login "you@example.com"
npx awal@2.12.1 auth verify "" ""
npx awal@2.12.1 status
```

Print the wallet address:

```bash
npx awal@2.12.1 address
```

Open the [Circle Faucet](https://faucet.circle.com/), 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:

```bash
npx awal@2.12.1 status
npx awal@2.12.1 balance --chain base-sepolia
npx awal@2.12.1 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:

```text
HTTP/1.1 200 OK
PAYMENT-RESPONSE: 
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:

```bash
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](/api-reference/payments/payments.access/), [Complete a transaction](/api-reference/transactions/payments.transactions.complete/), and [Get a transaction](/api-reference/transactions/payments.transactions.get/). For maintained endpoint adapters, continue with [FastAPI](/sdks/python/fastapi/) or [Django](/sdks/python/django/).
