# Pay once, keep the original authorization, recover the same purchase

Use `https://getrugshield.com` for HTTP and `https://getrugshield.com/mcp/` for MCP (Model Context Protocol, the tool interface). Paying on Base or Solana buys **Solana analysis**. It does not enable analysis of other chains. The exact offer returned for your actual request determines the amount and available payment networks.

This walkthrough uses the Python `x402` SDK (payment client library) **2.24.0**, whose installed imports and call signatures were checked locally. Its client examples below are exercised with controlled HTTP and MCP responses; that is local verification, not evidence of a new real payment. A wallet with USDC on the selected network is required. Supply its account or keypair through your own approved wallet integration; never paste a private key into an API request, support email or log.

Install the dependencies in a dedicated environment:

```sh
python -m pip install 'x402[httpx,evm,svm]==2.24.0' 'mcp>=1.30,<2' 'solana<0.40' 'solders>=0.27,<0.30'
```

The Solana client needs a reachable Solana mainnet RPC (blockchain read interface). The server's selected Solana offer must contain `extra.feePayer`: the facilitator uses that account to pay the payment transaction's network fee. Do not invent this value, substitute your wallet address, or copy it from an old quote. If it is missing, stop before signing and obtain a fresh unpaid quote or contact support. HTTP and MCP each need their own current offer.

## 1. Select exactly one network and review the offer

This code only defines helpers. It does not sign, send a request, or spend when loaded. `BASE` selects Base mainnet; `SOLANA` selects Solana mainnet. Call `make_wallet` with the corresponding account object from your wallet integration. `review_quote` preserves the server's payment metadata and passes exactly one checked offer to the SDK, so the SDK cannot silently choose the other network.

For a locally controlled Base wallet, the account object is created by `eth_account.Account.from_key` from the secret your approved secret store loaded into memory. For a locally controlled Solana wallet, the keypair object is created by `solders.keypair.Keypair.from_base58_string` from its base58 secret. Supply the resulting object as `evm_account` or `solana_keypair`, respectively; never serialize that object into a service call. Managed wallets may require a different supported signer integration. Loading a wallet is separate from selecting and confirming a purchase.

```python
import copy
import json
import re
import secrets
from urllib.parse import urlsplit

from solders.pubkey import Pubkey
from x402 import x402Client
from x402.schemas import PaymentRequired
from x402.http.utils import decode_payment_required_header, encode_payment_signature_header
from x402.mcp.utils import attach_payment_to_meta

BASE = "eip155:8453"
SOLANA = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
USDC = {
    BASE: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    SOLANA: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
}

def make_wallet(network, *, evm_account=None, solana_keypair=None, solana_rpc_url=None):
    wallet = x402Client()
    if network == BASE and evm_account is not None:
        from x402.mechanisms.evm.exact.register import register_exact_evm_client
        from x402.mechanisms.evm.signers import EthAccountSigner
        register_exact_evm_client(wallet, EthAccountSigner(evm_account), networks=BASE)
    elif network == SOLANA and solana_keypair is not None:
        from x402.mechanisms.svm.exact.register import register_exact_svm_client
        from x402.mechanisms.svm.signers import KeypairSigner
        register_exact_svm_client(wallet, KeypairSigner(solana_keypair), networks=SOLANA, rpc_url=solana_rpc_url)
    else:
        raise ValueError("Supply the wallet account for the selected supported network")
    return wallet

def review_quote(required, *, network, expected_pay_to, approved_atomic):
    if network not in USDC or type(approved_atomic) is not int or approved_atomic <= 0:
        raise ValueError("Choose one supported network and an explicit positive USDC limit")
    if required.get("x402Version") != 2:
        raise ValueError("Expected an x402 version 2 quote")
    choices = [item for item in required.get("accepts", [])
               if item.get("network") == network and item.get("scheme") == "exact"]
    if len(choices) != 1:
        raise ValueError("Expected exactly one offer for the selected network")
    chosen = choices[0]
    normalize = str.lower if network == BASE else str
    if (normalize(chosen.get("asset", "")) != normalize(USDC[network]) or
            normalize(chosen.get("payTo", "")) != normalize(expected_pay_to)):
        raise ValueError("The asset or recipient differs from the reviewed purchase")
    amount = chosen.get("amount", "")
    if not isinstance(amount, str) or not re.fullmatch(r"[0-9]+", amount) or not 0 < int(amount) <= approved_atomic:
        raise ValueError("Quoted charge exceeds the approved amount or is invalid")
    if network == SOLANA:
        Pubkey.from_string(chosen.get("extra", {}).get("feePayer", ""))
    return PaymentRequired.model_validate({**copy.deepcopy(required), "accepts": [copy.deepcopy(chosen)]})

def canonical_paid_url(url, method):
    parts = urlsplit(url)
    if (parts.scheme != "https" or parts.netloc != "getrugshield.com" or
            parts.username or parts.password or parts.fragment):
        raise ValueError("Use the canonical HTTPS origin, without credentials or fragment")
    if method == "GET" and parts.path.startswith(("/v1/token-risk/", "/v1/risk-panel/")):
        Pubkey.from_string(parts.path.rsplit("/", 1)[-1])
    elif method != "POST" or parts.path != "/v1/tx-guard":
        raise ValueError("Choose an advertised paid operation and exact method")
    return url

async def get_http_quote(http, method, url, *, body=None):
    canonical_paid_url(url, method)
    if method == "POST" and (not isinstance(body, dict) or not isinstance(body.get("transaction"), str) or not body["transaction"]):
        raise ValueError("Supply the actual unsigned transaction; an empty quote is not a payable request")
    content = json.dumps(body, separators=(",", ":")).encode() if body is not None else None
    headers = {"Content-Type": "application/json"} if content is not None else {}
    response = await http.request(method, url, content=content, headers=headers, follow_redirects=False)
    if response.status_code != 402:
        raise ValueError("Read the returned input/status instructions; do not authorize a payment")
    required = decode_payment_required_header(response.headers["PAYMENT-REQUIRED"])
    return {"method": method, "url": url, "content": content, "headers": headers,
            "required": required.model_dump(by_alias=True, exclude_none=True)}

async def prepare_http_purchase(quote, *, wallet, network, expected_pay_to, approved_atomic, confirm=False):
    if confirm is not True:
        raise ValueError("Explicit purchase confirmation is required before signing")
    selected = review_quote(quote["required"], network=network, expected_pay_to=expected_pay_to, approved_atomic=approved_atomic)
    payload = await wallet.create_payment_payload(selected)
    headers = {**quote["headers"], "Idempotency-Key": secrets.token_urlsafe(32),
               "PAYMENT-SIGNATURE": encode_payment_signature_header(payload)}
    return {"quote": copy.deepcopy(quote), "headers": headers}

async def send_http_original(http, purchase):
    quote = purchase["quote"]
    return await http.request(quote["method"], quote["url"], content=quote["content"],
                              headers=purchase["headers"], follow_redirects=False)
```

`approved_atomic` is an explicitly approved cap in millionths of USDC: 10,000 units equal 0.01 USDC. It is **not** a new advertised price. Obtain the current exact amount and recipient from the quote, review them in your purchase policy, then supply your approved values. Preserve the entire `quote` object unchanged between quote and payment, including method, URL/query and encoded JSON body. `prepare_http_purchase` creates one authorization without sending it. Keep its returned purchase context privately **before** calling `send_http_original`, so a lost response does not lose the original proof and key. Do not print that context or send it to support. Do not put `prepare_http_purchase` in an automatic retry loop.

Call `get_http_quote` using the user's actual mint URL, the exact selected-panel URL, or `POST /v1/tx-guard` with the user's actual unsigned serialized transaction. The quote call is free and does not execute a paid analysis. The actual transaction input is for simulation only; RugShield does not sign or broadcast it. Use an HTTP client such as `httpx.AsyncClient(timeout=120, follow_redirects=False)` inside an `async with` block. First inspect the returned quote; only then call `prepare_http_purchase(..., confirm=True)` with the explicitly selected `BASE` or `SOLANA` wallet and approved purchase values. Store the returned context privately, then pass it to `send_http_original`.

## 2. Read the response before deciding the next action

| Observed response | Meaning and next action | Effect of that next action |
|---|---|---|
| Own token/transaction HTTP 200 with confirmed `PAYMENT-RESPONSE` and useful result | Read the result and advisory limitations. Keep the returned real job ID and original purchase context | No extra call or charge is required |
| Own check HTTP 503, `charged:false`, `status:processing`, real `job_id` | GET the supplied `status_url` without a payment header; honor `Retry-After` or `retry_after_s` | Free lookup; no new analysis or settlement |
| Own check job becomes `ready`, payment is known not settled | Call `send_http_original` with the original purchase context and its original idempotency key | May settle the **original** authorization and release the existing analysis; no new signature |
| Panel HTTP 202, `charged:true`, receipt and `job_id` | Poll `status_url` for free until the final result is available | Analysis continues in its existing job; payment already settled; no second signed request is needed |
| Paid job result already available | GET `status_url` or use MCP `get_job_result`; the same settled result and receipt are recoverable | Free read, without another payment |
| Payment `confirmation_pending`, `reconciliation_required` or `unconfirmed` | Follow `agent_guidance`; preserve the actual job ID, poll only when instructed, or request support review | Do not create another authorization to resolve uncertainty; no automatic refund/reconciliation endpoint exists |
| Failed own token/transaction job with allowed free retry | POST `/v1/jobs/{actual_job_id}/retry` or use MCP `retry_failed_job` | Attempts the same job's allowed free retry; server may reject an exhausted/ineligible retry |
| Paid panel failure or `needs_review` | Keep its job ID and contact `rugshield@fastsoftware.uk` | Manual review; no automatic refund and no new payment |

### Free polling: identifiers and receipt placement

For HTTP, call **GET `/v1/jobs/{job_id}`** with the actual server-returned `job_id`,
without a payment proof. A paid job's outer JSON uses `job_id`; after confirmed
settlement its original receipt is in the **`PAYMENT-RESPONSE` HTTP header**.
Read the conditional `result` field for released analysis. No new authorization,
analysis or settlement is initiated by this lookup.

For MCP, call **`get_job_result` with `{"job_id": "the actual server-returned ID"}`**,
without payment metadata; the quoted ID here is descriptive, not an executable
example. Fresh free job observations expose **`job_id` plus legacy `id` when
present**. The original receipt is in the tool response **body**:
`payment_receipt` is the decoded receipt when available; otherwise retain and
inspect `receipt_header`, which can be an encoded HTTP receipt or a historical
JSON receipt string. Do not assume every free observation has a decoded receipt.
An unsettled observation does not release a settlement receipt.

The confirmed receipt from a **paid MCP token/transaction tool call** uses
response `_meta["x402/payment-response"]`. A **free `get_job_result` call does
not create that paid-call receipt metadata**: inspect the body fields above
instead. Both HTTP and MCP polling retrieve the same existing purchase without
another payment; receipt placement differs by transport.

### Cleanup threshold and availability

Default cleanup targets terminal (completed/failed) jobs **seven days after
completion or failure**, subject to pending work and payment-review safeguards.
Unresolved claiming/uncertain payment records are retained for review. This is a
cleanup threshold, **not guaranteed availability**: recovery depends on
database/storage availability. **Save your own result, receipt and job-ID copies**
when received; polling is not a permanent archive. An expired/unknown handle
does not establish that its earlier payment failed or authorize a replacement
payment.

Use the real server-returned job ID, never the text `{actual_job_id}`. The legacy top-level status/`charged` fields are retained; read `agent_guidance.payment` separately from `agent_guidance.analysis`. `charged:null` means unconfirmed, not free. The recommended next action is `agent_guidance.request_example`; its `explanation`, `effects.payment`, `effects.analysis` and prerequisites describe what the action does. Historical saved replies retain their original bytes; their fresh free job lookup supplies current guidance.

If the first signed response is lost, recover that original purchase with `send_http_original`; never call `prepare_http_purchase` again to generate a replacement proof. If no original idempotency key was used, the API also accepts the returned `X-RugShield-Job-ID` with the original proof; do not send both recovery headers. An expired/unknown job, a new 402 or a timeout does not prove the old payment failed. Reconcile the old receipt or seek support before authorizing anything new. A receipt proves settlement, not analysis usefulness or that the client read a result.

## 3. Use the same explicit network selection through MCP

Connect with the official `mcp.client.streamable_http.streamablehttp_client` transport and `mcp.ClientSession`: open `async with streamablehttp_client("https://getrugshield.com/mcp/") as (read, write, _)`, then `async with ClientSession(read, write) as session`. Call `await session.initialize()` and `await session.list_tools()` before calling tools. An MCP HTTP 200 can contain `isError:true`; inspect the tool result, not the transport status alone. The following helpers use the same wallet and reviewed-offer function defined above. They do not automatically pay a challenge.

```python
def mcp_body(result):
    if isinstance(result.structuredContent, dict):
        return result.structuredContent
    for item in result.content:
        if item.type == "text":
            return json.loads(item.text)
    raise ValueError("Expected a structured tool response")

async def get_mcp_quote(session, name, arguments):
    if name not in ("check_solana_token_risk", "check_solana_transaction"):
        raise ValueError("The panel quote tool is free and hands off to HTTP checkout")
    original = copy.deepcopy(arguments)
    result = await session.call_tool(name, original)
    required = mcp_body(result)
    if result.isError is not True or required.get("x402Version") != 2 or not required.get("accepts"):
        raise ValueError("Inspect the tool response; it is not a payable quote")
    return {"name": name, "arguments": original, "required": required}

async def prepare_mcp_purchase(quote, *, wallet, network, expected_pay_to, approved_atomic, confirm=False):
    if confirm is not True:
        raise ValueError("Explicit purchase confirmation is required before signing")
    selected = review_quote(quote["required"], network=network, expected_pay_to=expected_pay_to, approved_atomic=approved_atomic)
    payload = await wallet.create_payment_payload(selected)
    arguments = {**copy.deepcopy(quote["arguments"]), "idempotency_key": secrets.token_urlsafe(32)}
    signed = attach_payment_to_meta({"name": quote["name"], "arguments": arguments}, payload)
    return {"signed": signed}

async def send_mcp_original(session, purchase):
    signed = purchase["signed"]
    return await session.call_tool(signed["name"], signed["arguments"], meta=signed["_meta"])
```

For a token tool, supply `arguments` containing the actual Solana `mint`. For the transaction tool, supply `transaction` containing the actual unsigned serialized Solana transaction as base64; optional signer/intent inputs are listed in `tools/list`. A free panel estimate uses `quote_solana_risk_panel`; follow its canonical HTTP URL to get the authoritative 402 and pay through the HTTP flow. There is no paid panel-execution MCP tool.

Prepare with `prepare_mcp_purchase(..., confirm=True)`, keep its returned context privately, then use `send_mcp_original` for the first signed request. This preserves the original authorization even if that request loses its response. The SDK sends the original payment proof in request `_meta["x402/payment"]`; a confirmed paid tool response carries its receipt in response `_meta["x402/payment-response"]`. Read and retain that receipt privately. Poll with `session.call_tool("get_job_result", {"job_id": real_job_id})` **without** payment metadata. If an own-check job is ready but still unpaid, `send_mcp_original` reuses the original signed arguments and idempotency key; it may settle that original authorization. If no key was supplied, the original paid tool also accepts the returned `job_id` alongside unchanged inputs and original proof. Do not change the mint/transaction/intent or select a new rail during recovery.

The examples intentionally stop short of choosing a buyer's mint, transaction, account, recipient approval or spending policy. Those values must come from the actual task and wallet user; sample inputs must not become accidental purchases. [OpenAPI](/openapi.json), [the agent guide](/llms.txt), and the [advisor catalog](/v1/advisors) provide the current service contract.

Primary protocol and SDK references: [x402 Python SDK](https://github.com/x402-foundation/x402/tree/main/python/x402), [HTTP transport](https://github.com/x402-foundation/x402/blob/main/specs/transports-v2/http.md), [MCP transport](https://github.com/x402-foundation/x402/blob/main/specs/transports-v2/mcp.md), and [Solana x402 payments](https://solana.com/docs/payments/agentic-payments/x402). Upstream examples can target testnets or newer SDK releases; choose the exact offered mainnet network and verify compatibility before authorizing a payment.
