> ## Documentation Index
> Fetch the complete documentation index at: https://docs.venice.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Giving an Agent a Wallet and a Budget

> Pay for inference from a wallet with no API key, and cap what the agent can spend.

export const AuthorByline = ({name, date}) => {
  return <p style={{
    marginTop: "-1rem",
    marginBottom: "1.5rem"
  }}>
      <small>
        Originally written by {name} - {date}
      </small>
    </p>;
};

<AuthorByline name="Sabrina Aquino" date="21 August 2026" />

An agent with an API key can spend whatever the key can spend. That is fine when a person is watching it and awkward when nobody is. The usual fixes live outside the agent, in a dashboard or a billing alert that tells you about the problem after it happened.

Venice supports a second way in. Instead of a key, the agent holds a wallet. It authenticates by signing a message, pays for each request out of a USDC balance attached to that wallet address, and every charge lands in a ledger it can read back. There is no account, no dashboard, and no key to leak. The ceiling is the balance, and you decide what to put there.

This guide builds an agent that does exactly that, under a budget it enforces on itself.

<Card title="Run this notebook in Google Colab" icon="notebook" href="https://colab.research.google.com/github/veniceai/api-docs/blob/main/notebooks/wallet-budget-agent.ipynb">
  Every step below as an executable notebook. It runs without a funded wallet and stops at the payment wall, so you can see the whole flow before spending anything.
</Card>

## How It Works

Four moving parts, three of which are just HTTP:

| Step              | Endpoint                       | Why                                                    |
| ----------------- | ------------------------------ | ------------------------------------------------------ |
| Prove who you are | Any, via `SIGN-IN-WITH-X`      | A signed message replaces the API key                  |
| Put money in      | `/x402/top-up`                 | Discover the rails, then settle a signed USDC transfer |
| Check the balance | `/x402/balance/{address}`      | What is left, and whether it is enough to transact     |
| Read the charges  | `/x402/transactions/{address}` | Per request ledger of every debit                      |

Inference itself is the ordinary `/chat/completions` call. The only difference is which header you send.

## What It Costs to Start

Two numbers matter and they are not the same number.

The **minimum top-up is five dollars**. That is the smallest amount `/x402/top-up` will settle, and it is returned in the discovery response rather than hardcoded anywhere, so read it rather than trusting this page.

The **minimum balance to make a call is ten cents**. A wallet holding less than that gets a `402` back from inference even though it holds money.

So five dollars is the smallest wallet worth funding, and five dollars is what this guide gives the agent. Worth knowing what that buys: a short question to `qwen3-5-9b` costs about twenty seven input tokens and twenty six output tokens, which at that model's prices is roughly seven millionths of a dollar. Five dollars is on the order of three quarters of a million questions. The budget here is not a tight constraint, it is a blast radius.

## Setting Up

The x402 SDK does the payment signing. Do not hand-roll it: the transfer authorization is EIP-712 typed data and a reused nonce fails verification in ways that are tedious to debug.

```bash theme={"system"}
pip install "x402[evm]" eth-account requests
```

<Note>
  The x402 Python SDK requires Python 3.10 or newer. Colab is fine. A system Python that shipped with macOS may not be.
</Note>

Create `agent.py` with the configuration. `BUDGET_USD` is the ceiling the agent enforces on itself, set here to the whole wallet. Lower it and the agent stops before the money does, which is the only knob you are likely to change.

```python theme={"system"}
import base64
import json
import os
import secrets
from datetime import datetime, timedelta, timezone

import requests
from eth_account import Account
from eth_account.messages import encode_defunct

BASE_URL = "https://api.venice.ai/api/v1"
DOMAIN = "api.venice.ai"
CHAIN_ID = 8453          # Base mainnet
MODEL = "qwen3-5-9b"
BUDGET_USD = 5.00
```

## A Wallet the Agent Owns

The agent needs a keypair. In production this is a wallet you funded deliberately and whose key lives in a secret manager. While you are building, generating a throwaway is the right move, because a wallet with no money cannot do anything expensive by accident.

```python theme={"system"}
key = os.environ.get("WALLET_KEY")
account = Account.from_key(key) if key else Account.create()

print(f"wallet {account.address}")
print("funded" if key else "disposable, cannot pay yet")
```

Keep the private key out of the notebook. In Colab, put it in Secrets and read it with `userdata.get("WALLET_KEY")`.

## Signing In Instead of Authenticating

There is no key to send, so each request carries a proof that the wallet owner made it. The proof is an [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) message, signed, then base64 encoded into the `SIGN-IN-WITH-X` header.

The message format is exact. Venice rebuilds these bytes on its side and verifies your signature against them, so a stray blank line means a rejected signature rather than a helpful error.

```python theme={"system"}
def siwx_header():
    now = datetime.now(timezone.utc)
    stamp = lambda t: t.isoformat(timespec="milliseconds").replace("+00:00", "Z")
    issued_at, expires_at = stamp(now), stamp(now + timedelta(minutes=4))
    nonce = secrets.token_hex(8)

    message = (
        f"{DOMAIN} wants you to sign in with your Ethereum account:\n"
        f"{account.address}\n\nSign in to Venice AI\n\n"
        f"URI: https://{DOMAIN}\nVersion: 1\nChain ID: {CHAIN_ID}\n"
        f"Nonce: {nonce}\nIssued At: {issued_at}\nExpiration Time: {expires_at}"
    )
    signature = account.sign_message(encode_defunct(text=message)).signature.hex()

    payload = {
        "address": account.address,
        "message": message,
        "signature": signature if signature.startswith("0x") else "0x" + signature,
        "chainId": CHAIN_ID,
    }
    return base64.b64encode(json.dumps(payload).encode()).decode()
```

Three rules govern these headers, and all three exist to stop replay. The signature is valid for **five minutes** from `Issued At`. Each **nonce is single use** for about five and a half minutes. And the signer must match the wallet in the path, so one wallet cannot inspect another and gets a `403` for trying.

The practical consequence is that you sign a fresh header per request rather than caching one. Signing is local and free, so this costs nothing.

```python theme={"system"}
def wallet_get(path, **params):
    response = requests.get(
        f"{BASE_URL}{path}",
        headers={"SIGN-IN-WITH-X": siwx_header()},
        params=params,
        timeout=30,
    )
    response.raise_for_status()
    return response.json()["data"]


def balance():
    return wallet_get(f"/x402/balance/{account.address}")


print(balance())
```

On a fresh wallet:

```json theme={"system"}
{
  "walletAddress": "0xc5048ea84939bb7eb4c611b88ea17d5ee4f11a0c",
  "balanceUsd": 0,
  "canConsume": false,
  "minimumTopUpUsd": 5,
  "suggestedTopUpUsd": 10
}
```

`canConsume` is the field to branch on. It accounts for the ten cent floor, so you do not have to.

## Putting Money In

Topping up is two requests. The first asks what Venice accepts and is unauthenticated, because there is nothing to authenticate yet. The second carries a signed transfer authorization.

```python theme={"system"}
from x402.client import SpendControls, x402ClientSync
from x402.http import PAYMENT_SIGNATURE_HEADER, encode_payment_signature_header
from x402.mechanisms.evm import EthAccountSigner
from x402.mechanisms.evm.exact.client import ExactEvmScheme
from x402.schemas.payments import PaymentRequired


def top_up():
    discovery = requests.post(f"{BASE_URL}/x402/top-up", timeout=30)
    required = PaymentRequired.model_validate(discovery.json())

    rail = next(a for a in required.accepts if a.network.startswith("eip155"))
    print(f"{rail.network}: {int(rail.amount) / 1e6:.2f} USDC to {rail.payTo}")

    client = x402ClientSync()
    client.register(rail.network, ExactEvmScheme(EthAccountSigner(account)))
    # The SDK caps one payment at $1 by default, which is below the $5 minimum
    # top-up, so every rail gets rejected until this is raised.
    client.set_spend_controls(SpendControls(max_amount_per_payment="$5", allowed_assets=True))

    payload = client.create_payment_payload(required)
    settlement = requests.post(
        f"{BASE_URL}/x402/top-up",
        headers={PAYMENT_SIGNATURE_HEADER: encode_payment_signature_header(payload)},
        timeout=90,
    )
    return settlement.json()
```

Discovery returns one entry per rail. Base and Solana today:

```json theme={"system"}
{
  "x402Version": 2,
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:8453",
      "amount": "5000000",
      "asset": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
      "payTo": "0x2670b922ef37c7df47158725c0cc407b5382293f",
      "maxTimeoutSeconds": 300,
      "extra": { "name": "USD Coin", "version": "2" }
    },
    {
      "scheme": "exact",
      "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
      "amount": "5000000",
      "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      "payTo": "8qUL23aSj7mDWdoLMXGHFvnVCT9wd7jXcysiekroADEL",
      "maxTimeoutSeconds": 300,
      "extra": { "name": "USD Coin", "version": "2", "feePayer": "BFK9TLC3edb13K6v4YyH3DwPb5DSUpkWvb7XnqCL9b4F" }
    }
  ]
}
```

Two details in there are easy to walk past. `amount` is in base units, and USDC has six decimals, so `5000000` is five dollars and not five million of anything. On the Solana rail, `extra.feePayer` is a Venice operated account that covers the transaction fee, which is what lets a wallet pay without holding SOL.

<Warning>
  The default spend control is the first thing that will stop you. The SDK ships with `max_amount_per_payment` set to one dollar, and the Venice minimum top-up is five, so an unmodified client rejects every rail on offer and raises `NoMatchingRequirementsError` before it ever contacts the network. Raise the cap deliberately rather than turning spend controls off.
</Warning>

Settling from a wallet with no USDC returns a `400` with `PAYMENT_VERIFICATION_FAILED`. That is the expected shape of failure: the signature was fine and the transfer was not.

## Paying Per Call

With a balance in place, inference is a normal request that happens to carry a signature. Turning off the Venice system prompt matters more than it looks: it is worth about seventeen hundred input tokens per call, which is two orders of magnitude more than the question itself.

```python theme={"system"}
def ask(question):
    response = requests.post(
        f"{BASE_URL}/chat/completions",
        headers={"SIGN-IN-WITH-X": siwx_header(), "Content-Type": "application/json"},
        json={
            "model": MODEL,
            "messages": [{"role": "user", "content": question}],
            "max_completion_tokens": 150,
            "venice_parameters": {
                "include_venice_system_prompt": False,
                "disable_thinking": True,
            },
        },
        timeout=90,
    )
    if response.status_code == 402:
        body = response.json()
        raise RuntimeError(
            f"balance ${body.get('currentBalanceUsd', 0)} is under the "
            f"${body.get('minimumBalanceUsd')} floor. Minimum top-up is "
            f"${body['topUpInstructions']['minimumAmountUsd']}."
        )
    response.raise_for_status()
    return response.json()["choices"][0]["message"]["content"].strip()
```

Handling `402` as a normal outcome rather than an exception is the whole design. An agent paying its own way will run out of money eventually, and running out of money is not a crash.

## Reading What It Spent

The ledger is authoritative. Rather than estimating from token counts, ask what was actually charged.

```python theme={"system"}
def charges():
    """Every debit against this wallet, newest first."""
    ledger = wallet_get(f"/x402/transactions/{account.address}", limit=100)
    return [t for t in ledger["transactions"] if t["type"] == "CHARGE"]
```

Each row links back to the call that caused it:

```json theme={"system"}
{
  "id": "ledger_01H...",
  "amount": -0.0000066,
  "balanceAfter": 4.9999934,
  "type": "CHARGE",
  "createdAt": "2026-08-21T18:22:10.000Z",
  "requestId": "chatcmpl-...",
  "modelId": "qwen3-5-9b"
}
```

`TOP_UP` and `REFUND` rows appear here too, with positive amounts. Filtering to `CHARGE` gives you spend.

## The Budgeted Run

Now the loop. Before each call the agent checks what it has spent, and it declines to start work it cannot pay for.

```python theme={"system"}
TASKS = [
    "Name one concrete tradeoff of vector search versus keyword search. One sentence.",
    "In one sentence, when is a bloom filter the wrong choice?",
    "Give one reason CRDTs are hard to debug in production. One sentence.",
    "What is one failure mode of exponential backoff without jitter? One sentence.",
    "Name one thing consistent hashing does not solve. One sentence.",
    "Why is p99 latency more useful than the mean? One sentence.",
]


def run(budget=BUDGET_USD):
    opening = balance()
    print(f"balance ${opening['balanceUsd']:.4f}, budget ${budget:.4f}")

    if not opening["canConsume"]:
        print(f"cannot transact yet, minimum top-up is ${opening['minimumTopUpUsd']}")
        return

    baseline = sum(abs(c["amount"]) for c in charges())
    spent = 0.0

    for number, task in enumerate(TASKS, 1):
        if spent >= budget:
            print(f"\nstopped before task {number}: ${spent:.6f} of ${budget:.4f} spent")
            return

        answer = ask(task)
        spent = sum(abs(c["amount"]) for c in charges()) - baseline
        print(f"\n{number}. {task}")
        print(f"   {answer}")
        print(f"   ${spent:.6f} spent, ${budget - spent:.6f} left")

    print(f"\nfinished all {len(TASKS)} tasks for ${spent:.6f}")
    if spent:
        print(f"at this rate ${budget:.2f} covers about {int(budget / (spent / len(TASKS))):,} calls")
```

Run it with the full five dollars and the budget never binds, which is the honest result at these prices. To watch the ceiling actually work, set one that a single call will breach:

```python theme={"system"}
run()              # $5.00, finishes every task
run(budget=1e-5)   # stops partway, having spent about $0.000007 per call
```

## Where This Leaves You

The agent holds its own money, proves its identity with a signature, and cannot exceed a limit you set, all without an account existing anywhere. For a scheduled job, a serverless function, or anything you would rather not hand a long-lived key to, that is a materially different security posture.

Some things worth doing next:

<CardGroup cols={2}>
  <Card title="Cap it in the protocol" icon="shield">
    Spend controls in the SDK are per payment, not per session. Pair them with the budget loop above so a bug in one cannot defeat the other.
  </Card>

  <Card title="Refill on empty" icon="refresh">
    Catch the `402`, top up, and retry. This is what `venice-x402-client` does for you on the TypeScript side.
  </Card>

  <Card title="Pay on Solana" icon="currency-solana">
    Same flow, different rail. Sign Ed25519 and set the returned `feePayer` so the wallet needs no SOL.
  </Card>

  <Card title="Give it real work" icon="tools">
    Swap the task list for a tool-calling loop and the ledger starts showing you what each decision cost.
  </Card>
</CardGroup>

For the full endpoint reference, see [x402 top-up](/api-reference/endpoint/x402/top-up) and [Using x402 with the Venice API](/guides/integrations/x402-venice-api).
