FTPFTP Tech LLC

canton-x402

Let your AI agent pay for x402 APIs on Canton.

Two paths: a self-custody MCP where the agent holds its own key, and a hosted HTTP flow any agent can call with no install and no signing. Every payment settles on-ledger on Canton MainNet.

Products

01

Two ways to pay

Self-custody MCP

The agent generates and holds its own Canton wallet. Connect the server once to a host that can run a local process, then the agent funds and pays itself.

Best for: desktop and IDE agents, self-hosted agents.

Set up the MCP

Hosted HTTP

No install

The agent calls our endpoints over plain HTTP. Works with any agent that can make an HTTP request, including browser-only hosts like ChatGPT. Payment settles from a hosted purse or a hosted ephemeral wallet, not from the agent’s own self-custody wallet.

Best for: ChatGPT, no-install agents, one-off calls.

Quickstart below
02

Which should I use?

Self-custody MCPHosted HTTP
InstallOwner runs one connect commandNone
TransportLocal stdioHTTP
Works in ChatGPT webNoYes
Who holds the keyThe agent, locallyFTP Tech (hosted)
Whose fundsThe agent's own walletHosted purse or hosted ephemeral wallet
Spend capsOwner-set (daily cap, allowlist, funded ceiling)Server-side daily budget
03

Hosted quickstart

One paid call (simplest)

One HTTP call, settled from a hosted purse on Canton MainNet. No wallet to provision.

POST /v1/demo/ask
curl -s https://pay.ftptech.xyz/v1/demo/ask \
  -H "content-type: application/json" \
  -d '{"prompt":"a photorealistic red panda coding at a laptop"}'

Returns 200 with { answer, model, spentCc, updateId, remainingDailyCc }, plus an image when the merchant returns one. Errors are flat { ok: false, error: <code> }, where <code> is one of bad_request, rate_limited, budget_exhausted, proxy_busy, paid_but_no_response, upstream_error.

GET / to self-discover
curl -s https://pay.ftptech.xyz/

Returns the live service descriptor: network, the one-shot method and path, and docs.

Agent gets its own funded wallet (two steps)

Use this when the agent should hold a real, funded wallet for the call. Every POST is async: it returns 202 with a job id. Poll the matching GET every 3 seconds until the status is terminal. Do not re-POST in a loop.

Step 1 creates and funds a Canton wallet for the agent. You get back its party id, a balance, and a secret, which is the wallet’s private key. Keep the secret; it is the only key to that wallet.

Paste into your agent's chat
Step 1: create your wallet
STEP 1 of 2: create and fund a Canton wallet via an HTTP API. Base URL: https://pay.ftptech.xyz. This step is async: the POST returns HTTP 202 with a job id, then you GET the poll URL every 3 seconds until status is terminal ("funded" or "failed"). Each HTTP call returns quickly; the slow work happens in the background, so just keep polling. All requests/responses are JSON.

1. POST https://pay.ftptech.xyz/v1/quest/wallet/create with header `content-type: application/json` and body `{}`. Read `walletJobId` from the 202 response.
2. Poll GET https://pay.ftptech.xyz/v1/quest/wallet/result?walletJobId=<walletJobId> every 3s:
   - {"status":"pending"} -> keep polling.
   - {"ok":false,...} -> stop and report the `error`.
   - {"status":"funded",...} -> done. SAVE `walletToken`, `party`, `balanceCc`, and `secret` (the secret is the wallet's PRIVATE KEY — it IS the wallet; whoever holds it controls the funds).

Do NOT re-POST create in a loop; if you must retry, re-issue the SAME POST (the server coalesces a create retry, so it never makes a second wallet). On 429 report rate_limited; on 503 report budget_exhausted.

REPORT BACK: party + balanceCc, then a "WALLET BACKUP — SAVE THIS" section that hands your user the FULL `secret` PEM in a code block plus the restore instructions from the response's `backup` field. The wallet belongs to the USER and keeps working after this chat; delivering the secret to them here IS the backup — without it the wallet is unrecoverable. Never send the secret to any URL or tool, and if the user asks for a backup later, give them this SAME saved secret — never create a new wallet for a backup. KEEP the walletToken; you need it for step 2.

Step 2 generates an image and settles its 0.25 CC payment on Canton from that wallet. You get back the image plus an updateId, the on-ledger transaction that is independently verifiable on the ledger.

Paste into your agent's chat
Step 2: generate an image, settle on Canton
STEP 2 of 2: generate an image and settle the on-ledger payment for it from the wallet you funded in step 1. The merchant is the CanTrust image API; one call settles 0.25 CC and returns a generated image. Same base URL https://pay.ftptech.xyz and the same async pattern (the POST returns HTTP 202 with a job id, then you GET the poll URL every 3 seconds until terminal). Use the `walletToken` you saved in step 1 (it is valid for about 15 minutes, so do this promptly). Do NOT send the secret.

3. POST https://pay.ftptech.xyz/v1/quest/wallet/pay with header `content-type: application/json` and body `{"walletToken":"<walletToken from step 1>","prompt":"<describe the image you want>"}`. Read `payJobId` from the 202 response.
4. Poll GET https://pay.ftptech.xyz/v1/quest/wallet/pay-result?payJobId=<payJobId> every 3s (this step is slower, allow about 20 to 30 seconds):
   - {"status":"pending"} -> keep polling.
   - {"ok":false,...} -> stop and report the `error`.
   - {"status":"done",...} -> done. SAVE `image` (the URL of the generated image) and `updateId` (the on-ledger settlement proof).
5. (Optional) Confirm it: GET https://pay.ftptech.xyz/v1/quest/verify?updateId=<updateId> and check `valid==true`.

If a pay POST returns 404 it means wallet_not_found (the walletToken expired or was already used); 409 means already_used (re-poll your existing pay job instead of re-POSTing). Do NOT re-POST pay in a loop; the server rejects a duplicate pay, so it never makes a second transaction.

REPORT BACK to the user and LEAD with the image: render it inline as a markdown image so the user actually SEES the picture -> `![generated image](<paste the image URL here>)` -> then put the raw `image` URL on its own line, then `updateId` and verify.valid. The generated image is the whole point of this task; never omit it or replace it with a text summary. End with a wallet reminder: the step-1 wallet still holds the change and KEEPS WORKING — if the "WALLET BACKUP" block (the step-1 `secret` PEM + restore instructions) has not been delivered to the user yet, deliver it now; any later backup request means handing over that SAME saved secret, never creating a new wallet.

The secret is the wallet private key. It is returned exactly once, in the step-1 funded body, and never again. The walletToken is single-use and expires in about 15 minutes. The featured merchant is the CanTrust image API, which settles 0.25 CC and returns an image. Single-call variant: POST /v1/quest/run then GET /v1/quest/result runs both legs in one job if you do not need the wallet token between steps.

04

API reference

Base https://pay.ftptech.xyz. Every call is async: POST returns 202 plus a job id, then you poll the GET every 3 seconds. 429 means wait, 503 means the daily budget is reached. Do not re-POST in a loop.

POST/v1/quest/wallet/create

Body {} → 202 { walletJobId }.

GET/v1/quest/wallet/result?walletJobId=...

Poll to status:"funded" party, balanceCc, secret, walletToken.

POST/v1/quest/wallet/pay

Body { walletToken, prompt } → 202 { payJobId }. No secret is sent. The server pays from your funded wallet.

GET/v1/quest/wallet/pay-result?payJobId=...

Poll to status:"done"(about 20 to 30 seconds) → { party, updateId, answer, image?, model, balanceCc }.

GET/v1/quest/verify?updateId=...

Public, no-auth → { valid, party, completedAt }.

Pay-result errors: 404 is wallet_not_found (the token expired or was used), 409 is already_used (re-poll the existing job, do not re-POST).

Step 1 (curl)
# Kick off, returns 202 { walletJobId }
curl -s https://pay.ftptech.xyz/v1/quest/wallet/create \
  -H "content-type: application/json" -d '{}'

# Poll every 3s until status is "funded" (or "failed")
curl -s "https://pay.ftptech.xyz/v1/quest/wallet/result?walletJobId=<walletJobId>"
Step 2 (curl)
# Generate an image and settle 0.25 CC, returns 202 { payJobId }. No secret is sent.
curl -s https://pay.ftptech.xyz/v1/quest/wallet/pay \
  -H "content-type: application/json" \
  -d '{"walletToken":"<walletToken from step 1>","prompt":"a photorealistic red panda coding at a laptop"}'

# Poll every 3s until status is "done" (the slow leg, about 20 to 30 seconds). The result has an "image" URL.
curl -s "https://pay.ftptech.xyz/v1/quest/wallet/pay-result?payJobId=<payJobId>"

# Verify the on-ledger proof (public, no auth)
curl -s "https://pay.ftptech.xyz/v1/quest/verify?updateId=<updateId>"
05

Run it from a terminal

Prefer to run it by hand, with no agent? This script does both steps with curl and jq. Verified working end to end.

Run in your terminal
#!/usr/bin/env bash
# Canton wallet + payment in one paste. Needs: curl, jq.  Run it in your terminal.
set -euo pipefail
BASE="https://pay.ftptech.xyz"

echo "STEP 1 - create + fund a Canton wallet..."
WJ=$(curl -s "$BASE/v1/quest/wallet/create" -H "content-type: application/json" -d '{}' \
  | jq -r .walletJobId)

while :; do
  R=$(curl -s "$BASE/v1/quest/wallet/result?walletJobId=$WJ")
  S=$(echo "$R" | jq -r '.status // empty')
  [ "$(echo "$R" | jq -r '.ok')" = "false" ] && { echo "FAILED: $(echo "$R" | jq -r .error)"; exit 1; }
  [ "$S" = "funded" ] && break
  sleep 3
done
TOKEN=$(echo "$R" | jq -r .walletToken)
PARTY=$(echo "$R" | jq -r .party)
echo "$R" | jq -r .secret > canton-wallet-key.pem && chmod 600 canton-wallet-key.pem
echo "  funded wallet: party=$PARTY  balance=$(echo "$R" | jq -r .balanceCc) CC"
echo "  wallet PRIVATE KEY saved to ./canton-wallet-key.pem - back it up; it IS the wallet."
echo "  restore later: CANTON_AGENT_HOME=<empty dir> npx @ftptech/canton-agent-wallet import \\"
echo "    --relay-url https://facilitator.ftptech.xyz --key-file canton-wallet-key.pem"

echo "STEP 2 - generate an image and settle 0.25 CC on Canton..."
PJ=$(curl -s "$BASE/v1/quest/wallet/pay" -H "content-type: application/json" \
  -d "{\"walletToken\":\"$TOKEN\",\"prompt\":\"a photorealistic red panda coding at a laptop\"}" | jq -r .payJobId)

while :; do
  R=$(curl -s "$BASE/v1/quest/wallet/pay-result?payJobId=$PJ")
  S=$(echo "$R" | jq -r '.status // empty')
  [ "$(echo "$R" | jq -r '.ok')" = "false" ] && { echo "FAILED: $(echo "$R" | jq -r .error)"; exit 1; }
  [ "$S" = "done" ] && break
  sleep 3
done
UPDATE=$(echo "$R" | jq -r .updateId)
IMG=$(echo "$R" | jq -r '.image // empty')
echo "  generated image: $IMG"
echo "  on-ledger updateId=$UPDATE"

echo "VERIFY..."
curl -s "$BASE/v1/quest/verify?updateId=$UPDATE" | jq '{valid,party,completedAt}'
echo "Done. image=$IMG  updateId=$UPDATE"

For agent frameworks / discovery:

Back to ftptech.xyz