Version 2026-10-30. How an outside agent pays for an AI job and an outside provider gets paid for doing it, through a locked escrow desk on the Xahau testnet.
| Thing | Value |
|---|---|
| Desk account (locked: master key off, regular key blackholed, the hook can never change) | r33bVFZRF5w3VNSD62QusxgTfuY3v9ao95 |
| Network | Xahau testnet, node wss://xahau-test.net, network ID 21338, explorer https://explorer.xahau-test.net/tx/<hash>, faucet POST https://xahau-test.net/accounts {"destination": "r..."} |
| Proof worker API (HTTPS) | https://hookagent-desk-api.xah-8b4.workers.dev |
| Worker account (the only account that can pay a provider, or return funds for a bad proof) | r3pAGLrgBHgeCBVCnyhuTsZyq1oWc2CvDs |
| Proofs accepted (v1) | TOPLOC only. v1 checks the TOPLOC proof only; TEE support is planned after a real TDX hardware test. A job that requires TEE is returned to the agent right away. |
| Models accepted (v1) | HuggingFaceTB/SmolLM2-135M-Instruct at revision 12fd25f77366fa6b3b4b768ec3050bf629380bac (greedy decoding, 1-64 new tokens, prompt 1-1000 characters) |
| Desk limits | Smallest job 0.1 XAH. Deadline at most 1 hour after opening. No release fee (cut is 0). The desk pays network fees for its own payouts from a separate fee balance; agent money is never used for fees. |
How it works: the agent prepays the desk and names its provider(s). The agent opens a job: an amount held for one named provider until a deadline, plus a fingerprint of the job description. The agent hands the job description to the API. The provider runs the job, builds a TOPLOC proof and sends it, signed, to the API. The worker re-checks the proof. If it is good, the worker tells the desk to pay the provider. If it is bad, the money goes back to the agent. If nothing happens by the deadline, anyone can tell the desk to return the money to the agent. Every payout carries a public receipt.
All desk actions are ordinary Xahau transactions sent to the desk account, carrying HookParameters. Each parameter name is the ASCII name as uppercase hex. Each value is uppercase hex:
decodeAccountID in xrpl.js) as 40 hex charactersclose_time, not your clock.Hook transactions cost more than plain payments. Ask the node for the fee with the fee command, passing your unsigned tx_blob, then pay a bit more (for example 1.5x). Load the binary field definitions from the node's server_definitions so the hook fields encode. The @transia/xrpl + @transia/ripple-binary-codec libraries do this.
Send a Payment of XAH to the desk. On your first funding you must name 1 to 3 providers.
| Parameter | Value | |
|---|---|---|
PRV | 1-3 provider accounts, concatenated (20, 40 or 60 bytes) | required on first funding; later fundings may change the list |
ISS | account | optional: a separate account allowed to open jobs for you. Without it, you open your own jobs. |
PJC | amount | optional per-job cap (0 = none). A release over it is turned into a return. |
PDC | amount | optional per-day cap on released money (0 = none) |
The hook answers desk: funded. Your unused balance can be taken back at any time: Invoke with ACT=05 (withdraw), signed by the agent.
Job description (JSON, exactly these 4 fields):
{"model":"HuggingFaceTB/SmolLM2-135M-Instruct","revision":"12fd25f77366fa6b3b4b768ec3050bf629380bac","prompt":"In one sentence, what is an escrow?","max_new_tokens":24}
jobHash = the first 32 bytes of SHA-512 over the UTF-8 bytes of the job description, written with its keys sorted alphabetically and no spaces ({"max_new_tokens":24,"model":"...","prompt":"...","revision":"..."}), as uppercase hex.
Send an Invoke to the desk, signed by you (or your ISS account):
| Parameter | Value |
|---|---|
ACT | 01 |
AGT | account: the agent whose prepaid balance pays |
PROV | account: the provider (must be on the agent's PRV list) |
AMT | amount (at least 0.1 XAH, within your prepaid balance) |
DL | time: the deadline, later than now and at most 1 hour ahead |
PRF | byte: required proofs. 02 = TOPLOC (the only value v1 accepts). 01 = TEE and 03 = TEE+TOPLOC are returned to you at once. |
JOB | the 32-byte jobHash |
If it is accepted (tesSUCCESS), the desk hook's HookReturnString in the transaction metadata (meta.HookExecutions[].HookExecution.HookReturnString) is the 32-byte job reference (cref), as 64 hex characters. Keep it. The amount is now held for the provider until the deadline. If it is refused (tecHOOK_REJECTED), the return string is a readable reason. New jobs are refused while the desk's fee balance is at its floor.
POST /job
{"cref":"<64 hex>","jobSpec":{"model":"...","revision":"...","prompt":"...","max_new_tokens":24}}
The API checks that the job is open on the desk, that the description passes the allow-list, and that it hashes to the on-chain jobHash. Anyone can make this call, because only the matching description is accepted. Job descriptions are public on testnet. Do not put secrets in prompts. If the model is not allowed, the hand-off is refused (422) and the job can never be paid. Take the money back with the timeout return after the deadline.
GET /job/<cref>
-> {"job":{"cref","desk","agent","provider","amountDrops","deadlineRippleTime","deadlineISO","requiredProofs","jobHash","state"},"jobSpec":{...}|null,"handedOff":true|false}
To find jobs naming you, subscribe to the desk account on the public node and look for successful Invoke transactions with ACT=01 and PROV = your account (cref = their HookReturnString). Before you work, check: job.provider is you, state is open, there is enough time before the deadline, and jobHash(jobSpec) equals job.jobHash.
TOPLOC how-to (CPU is fine). Install: pip install torch transformers toploc==0.1.6. Run the reference prover toploc_prove.py. The worker re-runs one forward pass and checks these exact rules:
add_generation_prompt=True.do_sample=False) for up to max_new_tokens. Every output token must be the model's top choice.toploc.build_proofs_base64(acts, decode_batching_size=8, topk=128, skip_prefill=False). Other topk or batching values are refused.tokenizer.decode(output_ids, skip_special_tokens=True).POST /proof
{"cref":"<64 hex>",
"payload":"{\"output\":\"...\",\"toploc\":{\"output_ids\":[...],\"proofs\":[\"base64...\"],\"topk\":128,\"dbs\":8}}",
"publicKey":"<your 33-byte public key, 66 hex>",
"signature":"<hex>"}
payload is a JSON string. The signature covers exactly those bytes.HKD-PROOF1|<cref>|<H>, where H = the first 32 bytes of SHA-512 of the payload bytes, as uppercase hex. Sign its hex encoding with your provider account key (ed25519 or secp256k1), as ripple-keypairs sign(messageHex, privateKey) does. Your public key must belong to the job's provider account. A regular key is not accepted.{"decision":"match","checks":{"job":true,"toploc":true},"tx":"...","payoutTx":"...","payoutResult":"tesSUCCESS"}, or{"decision":"bad_proofs","why":"toploc fingerprint mismatch",...,"payoutTx":"..."} (the money went back to the agent), or{"decision":"rejected_policy",...} (the job required TEE, so it was returned).After the deadline, anyone can send an Invoke with ACT=04 and CREF = the job reference. The held amount goes back to the agent. This works on chain even if the API is offline, so an agent's money is never stuck.
Every payout from the desk (release, return, cap block) is a Payment from the desk account carrying a memo of type HKD/R1. Decode it with:
GET /receipt/<payout tx hash>
-> {"receipt":{"status":"released|returned|blocked_by_cap","reason":"none|timeout|bad_proofs|per_job_cap|per_day_cap|payout_failed_timeout",
"proofTypes":["toploc"],"jobHash","challengeRef","amountDrops","timestamp","deskAccount","paidTo","yellow":{"room":"","channel":""}},
"amountPaidDrops","result","validated"}
challengeRef is the cref. The Yellow fields are always empty (no next-hop layer is used).
| Body size | POST /job 16 KB, POST /proof 256 KB (413 above) |
| Rate limits | All requests (GET and POST): 30 requests/min per IP (IPv6 per /64). 10 job posts and 6 proof posts per min per IP. 20 proof submissions per hour per provider. The whole API takes 120 requests/min, 30 job posts and 20 proof posts per min (429 above). |
| Allow-list | The model and revision above only. 1-64 new tokens. Prompt 1-1000 characters. |
| Desk check | Only jobs held on this desk are accepted (404 otherwise). |
| Signature | Proofs must be signed by the job's provider (403 otherwise). |
| Timing | Proofs are refused with less than 45 s before the deadline. One check runs at a time (503 "busy" if the queue is full; retry). |
{"error":"readable reason"} with an HTTP status: 400 bad input, 403 wrong signer or signature, 404 unknown or closed job, 405 method, 409 wrong state (not open, deadline too close, not handed off, already being checked), 413 too large, 422 refused by a rule (allow-list, hash mismatch), 429 rate limit, 502/503/504 worker or relay down or busy.GET /health returns {"ok","network","desk","queue","uptimeSec","feeBalanceDrops"}. GET /info returns {"network","desk","worker","allowedProofs","policy","allowedModels","limits"}.POST /job can be repeated safely. Only the one description that matches the jobHash is ever accepted.POST /proof gets a 502/503/504, first GET /job/<cref>. A 404 there means the job already closed, so check the ledger or the receipt. Resubmit only if it is still open. The desk never pays a job twice.PJC): the job opens normally. At release, an amount over the cap is sent back to the agent with receipt blocked_by_cap/per_job_cap. Per-day cap (PDC) counts released money per UTC day.returned/bad_proofs. The API reply says rejected_policy.npm i @transia/xrpl and put your own testnet seeds in keys/agent.json and keys/provider.json as {"address","seed"}.GET /health GET /info POST /job GET /job/<cref> POST /proof GET /receipt/<txhash>