Quickstart

Start the whole z402 stack with Docker Compose, pay every starter service, run an agent against two providers and list the catalog.

On this page

This page starts the whole z402 stack on one machine, then pays for real: an agent makes shielded deposits on a private Zcash chain and signs a voucher for each call. The chain is private to your machine, so the coins are not real money.

docker compose up and a paid smoke run

Before you start

Run the stack

  1. Start the stack

    Run this in the repository root, the folder that holds compose.yaml. No .env file and no other step is needed.

    Bash
    docker compose up -d
    docker compose ps

    up -d builds five images, pulls the rest and starts everything in dependency order. The first start downloads about 1 GB of images and two small models (about 580 MB) from Hugging Face. On a 4-CPU VM with a fast link, building the images from nothing took about 1.5 minutes. Once they were built, up -d returned after 45 to 75 seconds, and every service was healthy within two minutes.

    ContainerWhat it is
    zebra, zaino, zalletThe private Zcash chain: a node, its indexer, and the agent's wallet.
    clockMines one block every 25 seconds. Nothing confirms or expires without blocks.
    facilitatorVerifies vouchers, reads deposits, keeps the ledger and hosts the catalog of paid APIs.
    apiThe demo paid API. GET /chunk costs 500 zat.
    llm, embeddings, dropboxStarter services run by the operator. llm and embeddings use local models.
    markdownThe starter service URL to markdown, run as an independent provider with its own address and API key.
    llm-model, embed-modelThe llama.cpp servers that hold the two models.
    webThe website on port 3000.

    Five one-shot jobs prepare the chain, the wallet, the keys and the registration of provider 2: chain-init, zallet-init, wallet-fund, keygen and markdown-register. They show as Exited (0) in docker compose ps -a, and they run again on every up, changing nothing once their work is done. wallet-fund mines 110 blocks and shields 62.5 ZEC to the agent's wallet.

    llm and embeddings show health: starting until their model is loaded. Nothing else waits for them.

  2. Look at an offer

    An unpaid call to the demo API gets the price instead of an answer.

    Bash
    curl -s -o /dev/null -w '%{http_code}\n' http://localhost:4021/chunk
    Text
    402

    The terms travel in the PAYMENT-REQUIRED header as base64 JSON. This decodes the offer (it needs curl, base64 and jq):

    Bash
    curl -s -D - -o /dev/null http://localhost:4021/chunk | tr -d '\r' \
      | awk -F': ' 'tolower($1) == "payment-required" { print $2 }' | base64 -d | jq '.accepts[0]'
    JSON
    {
      "scheme": "prepaid-voucher",
      "network": "zcash:regtest",
      "amount": "500",
      "asset": "ZEC",
      "payTo": "uregtest1...",
      "maxTimeoutSeconds": 60,
      "extra": {
        "windowZat": "100000",
        "creditRatioBps": 2500,
        "creditCapZat": "200000",
        "zeroConfMinExpiryBlocks": 20,
        "termsVersion": "<16 hex characters>"
      }
    }

    Each call costs 500 zat. The agent deposits windowZat, 100,000 zat, at a time. How it works explains every field.

  3. Pay every starter service

    Bash
    docker compose run --rm smoke

    smoke waits until llm, markdown, embeddings and dropbox are healthy (on a first start, until the models are loaded). It then makes a deposit from the agent's wallet to each provider and runs one short script on every service: an llm stream of up to three chunks, one page, one embedding, and a drop uploaded, held, fetched and deleted. It checks every answer, prints one line per step, and exits 0 if all passed.

    Text
    llm start 200 31842 ms chunk 0 "Once upon a time, there lived a lighthouse keeper named Elias." (200 zat)
    llm continue 200 1910 ms chunk 1 " Every night he climbed the stairs to light the lamp" (200 zat)
    llm continue 200 1895 ms chunk 2 "..." (done, length) (200 zat)
    markdown get 200 412 ms "Example Domain" 149 chars (1000 zat)
    embeddings embed 200 96 ms 2 vectors, 768 dimensions, model nomic-embed-text, 5 tokens (100 zat)
    dropbox upload 201 40 ms id q3Zs0mYbT7cWpAxK1nGhUw, 1192 bytes, held until block 1052 (500 zat)
    dropbox hold 200 75 ms held until block 1062 (500 zat)
    dropbox fetch 200 5 ms 1192 bytes identical, expires at block 1062
    dropbox delete 204 4 ms deleted, then 404
    session uregtest1qqq…qqqqqq cumulative 1700 zat
    session uregtest1ppp…pppppp cumulative 1000 zat
    7 paid calls over 2 sessions
    smoke ok: 9 steps passed on llm, markdown, embeddings, dropbox

    Your times, texts and ids will differ. Each line reads service step status time detail. The first line is slow because it includes the first deposit: the wallet has to build a proof, which takes 5 to 10 seconds. The two session lines are the two providers: the operator, which runs llm, embeddings and dropbox, and provider 2, which runs markdown. Each has its own deposit and its own running total. A failed step says FAIL and whether the call was charged.

    markdown fetches https://example.com, so the markdown container needs internet access. To fetch another page, add --page:

    Bash
    docker compose run --rm smoke --page https://www.rfc-editor.org/rfc/rfc9110.html
  4. Run an agent against two providers

    The demo agent is the stock x402 client plus the z402 scheme. This run calls the demo API and URL to markdown in turn, five times each:

    Bash
    docker compose run --rm agent --calls 5 \
      --url http://api:4021/chunk \
      --url "http://markdown:4032/v1/markdown?url=https://example.com"

    Inside the compose network the services have their container names and ports (api, markdown). From your own machine, the same services are on localhost and the published ports.

    Text
    wallet: 6,249,460,000 zat spendable
    z402 demo: 5 calls to each of 2 targets, in turn, every 1000 ms on zcash:regtest
      [1] http://api:4021/chunk
      [2] http://markdown:4032/v1/markdown?url=https://example.com
      * session 7c1e…9d on zcash:regtest
      * deposit D1 started, paid inline
      * deposit D1 broadcast 3f2a…c1 in 6,888 ms
      * deposit 3f2a…c1 acknowledged by the merchant
    #001 [1] 200  c=500  limit=100,000  conf=0  pend=100,000  credit=0  D1 3f2a…c1 pending
      * session 2b90…e4 on zcash:regtest
      * deposit D1 started, paid inline
      * deposit D1 broadcast 91d0…7a in 6,214 ms
      * deposit 91d0…7a acknowledged by the merchant
    #002 [2] 200  c=1,000  limit=100,000  conf=0  pend=100,000  credit=0  D1 91d0…7a pending
    #003 [1] 200  c=1,000  limit=100,000  conf=0  pend=100,000  credit=0  D1 3f2a…c1 pending
    #004 [2] 200  c=2,000  limit=100,000  conf=0  pend=100,000  credit=0  D1 91d0…7a pending
    ...
    per target:
      [1] 5 paid  c=2,500  session 7c1e…9d  payTo uregtest1qqq…qqqqqq  deposits 1
      [2] 5 paid  c=5,000  session 2b90…e4  payTo uregtest1ppp…pppppp  deposits 1
    done

    How to read it:

    • [1] and [2] tag the two targets. They have different payTo addresses, so the agent opens one session, with its own session key and its own deposit, for each. Each provider sees only its own session.
    • c= is the running total the agent has signed for with that provider. It grows by the price of each call: 500 zat at the demo API, 1,000 zat at URL to markdown.
    • limit= is the most that provider will accept right now. conf=, pend= and credit= are its three parts: confirmed deposits, one unconfirmed deposit, and credit.
    • D1 3f2a…c1 pending is the deposit and its status. Call 1 is served while the deposit is still in the mempool. Once a block confirms it, the status becomes confirmed, and the limit grows by the 25 percent credit.

    The same agent has more modes:

    docker compose run --rm agent --calls 20

    The cheat run makes one deposit and refuses to make another. About 200 calls run on the unconfirmed deposit, the agent waits for a block, about 50 more run on credit, and the provider then refuses at its limit of 125,000 zat with z402_deposit_required. That is the most a cheating agent can ever get: one unconfirmed window plus the credit line.

  5. List the services

    Services list themselves in the facilitator's catalog, the x402 Bazaar, and repeat it every 30 seconds. discover reads the catalog the way any agent would, with no wallet:

    Bash
    docker compose run --rm discover
    Text
    Demo API            GET   http://localhost:4021/chunk              500 zat per chunk         up, 5 paid today
    LLM inference       POST  http://localhost:4031/v1/generate        200 zat per output chunk  up, 3 paid today
    URL to markdown     GET   http://localhost:4032/v1/markdown        1,000 zat per page        up, 6 paid today
    Text embeddings     POST  http://localhost:4033/v1/embeddings      100 zat per 1k tokens     up, 1 paid today
    Encrypted drop box  POST  http://localhost:4034/v1/drops           500 zat per 10 blocks     up, 1 paid today
    Encrypted drop box  POST  http://localhost:4034/v1/drops/:id/hold  500 zat per 10 blocks     up, 1 paid today

    Each line ends with the route's description, left out here. up means the service declared itself in the last 90 seconds. paid today counts calls settled since 00:00 UTC. After the two runs above, llm shows the three chunks of the smoke run, and markdown shows its one smoke page plus the five the agent fetched. The demo API's GET /chunk is listed too, as Demo API, and shows the five calls the agent paid it. The URLs are the services' public addresses: reach them from your machine, not from inside the compose network.

    --intent ranks the list on your side by word overlap, so the catalog never learns what you are looking for:

    Bash
    docker compose run --rm discover --intent "turn a web page into markdown"

    The website shows the same list at http://localhost:3000/services. These docs are served by the same site, at /docs.

  6. Stop or reset

    Bash
    docker compose stop      # stop the containers, keep everything
    docker compose start
    docker compose down -v   # delete every volume

Ports and settings

ServicePortSetting
web3000Z402_WEB_PORT
api4021Z402_API_PORT
llm, markdown, embeddings, dropbox4031 to 4034Z402_LLM_PORT, Z402_MARKDOWN_PORT, Z402_EMBEDDINGS_PORT, Z402_DROPBOX_PORT
facilitator4022not published. The website proxies its public catalog reads.

Copy .env.example to .env to change a port, the block time (Z402_BLOCK_SECONDS) or the models. Published ports bind to every interface by default, so a server serves them. On a laptop, set Z402_PUBLISH_HOST=127.0.0.1 so only your machine reaches them. Docker's published ports bypass ufw.

Next steps

References

  • docs/deploy.md: prerequisites, first start, ports, the agent and how to stop and wipe.
  • compose.yaml: the services, the one-shot jobs and the profiles of agent, discover and smoke.
  • .env.example: the compose settings.
  • apps/smoke/README.md: what smoke runs and how its output reads.
  • apps/agent/src/flags.ts: --url, --calls, --interval-ms and --cheat.
  • apps/agent/src/log.ts: the format of the agent's log lines.
  • apps/agent/src/discover.ts: the discover command and its list format.
  • docs/demo.md: the cheat run and the numbers behind it.
  • docs/research/live-run.md: deposit timings measured on the regtest stack.
  • services/README.md: the starter services and how they list themselves.