A company that pays people for approved claims (suppliers, couriers, field workers) runs its own payouts facilitator. It runs on the company's own Cloudflare account, holds the company's own keys and pays from the company's own Stripe balance or Base USDC wallet. It charges nobody: it has no PAY, no card charging and no x402 routes. IXO charges no license fee for it.
This guide installs it with the @ixo/payouts-facilitator command-line tool, links it to the
company's entity on chain, trusts the oracle that verifies claims, and sets payout limits and
approvals. When you finish, a Portal such as asks.world can check the facilitator and send it
payouts. On mainnet, company payouts run only on a company's own facilitator: they are off in
IXO's facilitator.
Using Claude Code? Install the Asks plugin and say "set up bank payouts": its bank-payouts skill does these steps with you.
/plugin marketplace add ixoworld/ixo-plugins
/plugin install asks@ixo-pluginsFor practice money (testnet): /plugin install asks-testnet@ixo-plugins.
How it pays
- A verifier oracle decides a claim. The decision engine signs a receipt that names the payee and the amount.
- Your facilitator reads the receipt and checks it against your rules: the oracles you trust, the limits, and whether a person must approve it.
- It pays the payee on one of your rails and issues a signed payout receipt.
Two rails are installed by the tool:
stripe: a transfer from your Stripe balance to the payee's Stripe account, then to their bank. Stripe pays payees in your own country, or between the EEA, the US, the UK, Canada and Switzerland. Your Stripe balance must hold the money.usdc_base: USDC on Base from a payout wallet the tool creates for you. You fund it with USDC and a little ETH for gas.
Payees add their bank details or wallet on the Portal. On testnet and mainnet the default is
asks.world; --payee-origins sets your own.
Before you start
-
Node 24 LTS or newer. Check with
node --version. -
A Cloudflare account. A free account is enough. Run
npx --yes wrangler login. If the login has more than one account, setCLOUDFLARE_ACCOUNT_ID=<id>in front of every command below. -
A way to pay:
- for
stripe, a Stripe account that Stripe has approved for Connect, and a restricted key (below). Approval can take days and is Stripe's decision; - for
usdc_base, nothing yet: the tool creates the wallet, and you fund it.
- for
-
An IXO sign-in as an owner of the company entity (one of its controllers):
npx --yes @ixo/decision-oracle@0.4.0 login --network mainnetA browser opens: sign in and type your PIN. The session is saved to
~/.ixo/publish-session-<network>.json. Never open, print or share that file. -
Your company's entity DID (
did:ixo:entity:…) and thedid:webof each verifier oracle you trust.
Quickstart
The examples use these shell variables. Replace the values with yours.
NET=mainnet # devnet | testnet | mainnet
NAME=acme-payouts # the Worker name: also its database and cache
ORG=did:ixo:entity:<your company entity>
SESSION=~/.ixo/publish-session-$NET.jsonWrite each command in full. A $P deploy shorthand does not work in zsh, the default shell on a Mac.
Every command is idempotent: a re-run reuses what exists and changes only what differs. deploy and
anchor are previews until you add --yes. The last line of output is a JSON result, and --json
prints only that line.
1. Deploy the Worker
Preview first. It prints the plan and writes nothing:
npx --yes @ixo/payouts-facilitator@0.2.0 deploy --network $NET --name $NAME --entity $ORG --rails stripeThen install. For the stripe rail, pass your restricted key in the environment and let the tool
create the Stripe webhook. On a Mac you can read the key from the clipboard, so it is never typed or
pasted anywhere:
STRIPE_SECRET_KEY="$(pbpaste)" npx --yes @ixo/payouts-facilitator@0.2.0 deploy \
--network $NET --name $NAME --entity $ORG --rails stripe --stripe-webhook --yes --json
pbcopy < /dev/null # clear the clipboardUse --rails usdc_base for USDC only, or --rails stripe,usdc_base for both. Without --hostname
the facilitator runs on <name>.<your subdomain>.workers.dev; --hostname pay.acme.example uses
your own domain. Keep the url from the result: it is <url> below.
2. Link it to your company
This creates the facilitator's own entity and the two-way link between it and your company, signed with your session. Preview, then apply:
npx --yes @ixo/payouts-facilitator@0.2.0 anchor --network $NET --org $ORG --facilitator-url <url> --session $SESSION
npx --yes @ixo/payouts-facilitator@0.2.0 anchor --network $NET --org $ORG --facilitator-url <url> --session $SESSION --yesThen deploy once more, so the facilitator's did.json names its entity:
npx --yes @ixo/payouts-facilitator@0.2.0 deploy --network $NET --name $NAME --yes --json3. Fund the payout wallet (USDC only)
Skip this step for stripe alone. deploy printed the payout wallet's address. Send it USDC and a
little ETH on Base (Base Sepolia on devnet and testnet), then check it:
npx --yes @ixo/payouts-facilitator@0.2.0 payout-wallet --name $NAME --network $NET --check4. Trust the verifier
Only claims decided by an oracle you trust are paid. Add each verifier by its did:web:
npx --yes @ixo/payouts-facilitator@0.2.0 admin oracles add did:web:<verifier host> \
--url <url> --session $SESSION --network $NET5. Set limits and approvals
The default rule applies to every ask your company posts. --max is the most one payout can be.
Above --approval-over, a person on your team approves each payout. Leave it out for no approvals.
npx --yes @ixo/payouts-facilitator@0.2.0 admin defaults set --max 50.00 --currency USD \
--approval-over 20.00 --rails stripe --label "Field payouts" \
--url <url> --session $SESSION --network $NETUnless you set them, the limit per payee per day is --max and the limit per day for the whole
company is ten times --max. Set them with --per-payee-per-day and --per-day. To let someone
else approve, add them to the payout team:
npx --yes @ixo/payouts-facilitator@0.2.0 admin team add did:ixo:<their address> --role approver \
--url <url> --session $SESSION --network $NETRoles: rules, approver, reviewer, reader, payees, manager, exporter, oracles, all.
The controllers of the company entity are always admins.
6. Check it
This runs the same trust check a Portal runs before it sends payouts, from public data:
npx --yes @ixo/payouts-facilitator@0.2.0 trust-check --url <url> --org $ORG --network $NET --name $NAMEIt must end with "ok": true. An unfunded payout wallet shows as a warning and does not fail it.
7. Back up the secrets file
Copy ~/.config/ixo-payouts/<name>.secrets.json into a password manager or another safe place. See
the secrets file.
After you post an ask, the facilitator finds its claim collection on its own within a minute.
asks register <collectionId> --url <url> --network $NET asks it to look at once.
The secrets file
The tool keeps the facilitator's keys in ~/.config/ixo-payouts/<name>.secrets.json, mode 0600.
That file and your Cloudflare Worker are the only places they exist.
- The tool never prints a value, and never replaces one already in the file.
- Keep the file on every machine that runs
deploy. If the Worker has a secret the file lacks (a new laptop, a lost file),deployrefuses and pushes nothing. Restore the file from your backup. --new-secretsgenerates new values over the live ones. It destroys the payout wallet key (and the funds in it), the key to the stored payee data and the facilitator's identity. Use it only to start over.
| Secret | What it is |
|---|---|
SERVICE_ED25519_MNEMONIC | The facilitator's did:web key, anchored on its entity. Losing it means a new identity |
PAYEE_DATA_KEYS | Seals payee data. Losing it makes stored payee data unreadable |
PAYOUT_USDC_KEY | The Base payout wallet's key. It holds the funds |
STRIPE_SECRET_KEY | Your restricted Stripe key, copied from the environment on deploy |
STRIPE_CONNECT_WEBHOOK_SECRET | The Stripe webhook's signing secret, from deploy --stripe-webhook |
PAYOUTS_SCOPES_JSON | Generated empty: asks are discovered on chain |
A restricted Stripe key
Use a restricted key (rk_live_… on mainnet, rk_test_… elsewhere), not your account's secret key.
Create it in the Stripe Dashboard under Developers → API keys → Create restricted key, with these
permissions and everything else set to None:
| Resource | Access |
|---|---|
| Transfers | Write |
| Balance | Read |
| Connected accounts | Write |
| Account links | Write |
| Payouts | Read |
| Balance transactions | Read |
| Webhook endpoints | Write |
Set it up with an AI assistant
An AI assistant with a terminal, such as Claude Code or Codex, can run the whole setup for you. It stops where only you can act: signing in, Stripe, and the payout limits. Fill in the details at the top, then paste the prompt.
Set up a payouts facilitator for my company. Do the setup for me in this terminal. I only log in,
click and reply when you ask.
You are installing my company's own payouts service. It runs on our own Cloudflare account and pays
people from our own Stripe balance or Base USDC wallet. IXO never holds our keys.
Our details. Use exactly these:
- Company: <company name>
- Company entity: <did:ixo:entity:...>
- Network: <devnet | testnet | mainnet>
- Service name: <company>-payouts
- Rails: <stripe | usdc_base | stripe,usdc_base>
- Verifier oracles to trust: <did:web:...>
Rules:
- Run every command yourself, one at a time. Never ask me to type one. Run only the commands written
here; if one fails, tell me what is missing in one plain sentence and stop.
- Write every command in full: no $P shorthand.
- Secrets never go into this chat. Never open, print or summarize a key or a secrets file. If I paste
a key here, tell me to delete it in Stripe and make a new one.
- Where a command has a preview, run the preview first, tell me in one sentence what it will do, and
go on when I say yes.
- Start each step with its check. If it is already done, say so and move on.
- At each STOP, tell me exactly what to do and wait for me. Open the page for me when there is one.
- Talk to me in plain words: "your payouts service", "your company".
Step 1. Check the computer
node --version
It must say v24 or newer (Node 24 LTS or newer). If not, install the LTS from https://nodejs.org first.
Step 2. Log in to Cloudflare
npx --yes wrangler whoami
If it is not logged in:
npx --yes wrangler login
If the login has more than one account, ask which one is the company's and put
CLOUDFLARE_ACCOUNT_ID=<its id> in front of every command below.
STOP for me: a browser page opens. Sign in to Cloudflare and click Allow.
Step 3. Sign in as an owner of the company
Check who is signed in, without opening the file:
node -e 'try{console.log(JSON.parse(require("fs").readFileSync(require("os").homedir()+"/.ixo/publish-session-<network>.json","utf8")).address)}catch{console.log("no sign-in")}'
If it says no sign-in, or the address is not mine:
npx --yes @ixo/decision-oracle@0.4.0 login --network <network>
Never open, read or print ~/.ixo/publish-session-<network>.json.
STOP for me: a browser page opens. Sign in and type your PIN. You must be an owner of the company.
Step 4. Get Stripe ready (only when Rails includes stripe)
Stripe must have approved the company for Connect (https://dashboard.stripe.com/connect). If not, I
fill in Stripe's form and this setup stops until Stripe says yes.
Then I create a restricted key with: Transfers Write, Balance Read, Connected accounts Write,
Account links Write, Payouts Read, Balance transactions Read, Webhook endpoints Write. Everything
else None. On mainnet it starts with rk_live_, elsewhere rk_test_.
STOP for me: copy the new key, then say it is copied. Do not paste it into the chat.
Step 5. Install the payouts service
Preview (it changes nothing):
npx --yes @ixo/payouts-facilitator@0.2.0 deploy --network <network> --name <service name> --entity <company entity> --rails <rails>
Then install. With stripe, read the key from the clipboard (macOS pbpaste; Linux wl-paste or
xclip -o; Windows PowerShell Get-Clipboard):
STRIPE_SECRET_KEY="$(pbpaste)" npx --yes @ixo/payouts-facilitator@0.2.0 deploy --network <network> --name <service name> --entity <company entity> --rails <rails> --stripe-webhook --yes --json
Without stripe, the same command without STRIPE_SECRET_KEY and --stripe-webhook.
Clear the clipboard:
pbcopy < /dev/null
Keep the url from the last line of the result. Below it is <url>.
Step 6. Link the service to the company
Preview, then the same with --yes:
npx --yes @ixo/payouts-facilitator@0.2.0 anchor --network <network> --org <company entity> --facilitator-url <url> --session ~/.ixo/publish-session-<network>.json
npx --yes @ixo/payouts-facilitator@0.2.0 anchor --network <network> --org <company entity> --facilitator-url <url> --session ~/.ixo/publish-session-<network>.json --yes
Install once more, so the service names its link:
npx --yes @ixo/payouts-facilitator@0.2.0 deploy --network <network> --name <service name> --yes --json
Step 7. Fund the payout wallet (only when Rails includes usdc_base)
npx --yes @ixo/payouts-facilitator@0.2.0 payout-wallet --name <service name> --network <network> --check
STOP for me: send USDC and a little ETH on Base to the address it prints, then say it is sent.
Step 8. Trust the verifier and set the payout limit
For each verifier:
npx --yes @ixo/payouts-facilitator@0.2.0 admin oracles add <verifier did:web> --url <url> --session ~/.ixo/publish-session-<network>.json --network <network>
Ask: what is the most one payout can be, in dollars? And above what amount should a person at the
company approve each payout (or none)?
npx --yes @ixo/payouts-facilitator@0.2.0 admin defaults set --max <most> --currency USD --approval-over <amount> --rails <rails> --url <url> --session ~/.ixo/publish-session-<network>.json --network <network>
Leave out --approval-over when I say none.
STOP for me: say the most one payout can be, and the amount above which you approve each payout.
Step 9. Check it
npx --yes @ixo/payouts-facilitator@0.2.0 trust-check --url <url> --org <company entity> --network <network> --name <service name>
It must end with ok true. If not, say each problem in plain words and stop.
Step 10. Back up the secrets file
The file ~/.config/ixo-payouts/<service name>.secrets.json holds the keys of this service. It exists
only on this computer and at Cloudflare. Never open or print it.
STOP for me: copy that file into your password manager or another safe place. If it is lost, the
service must be set up again from scratch.
Step 11. Finish
If this setup signed in at step 3, offer to sign out. On yes:
npx --yes @ixo/decision-oracle@0.4.0 logout --network <network>
Tell me the <url> of my payouts service, and that payouts are paid from our Stripe balance or
payout wallet, so it must hold money.Run it
- Pause and resume all payouts:
admin pauseandadmin resume, with--url,--sessionand--network. - Update by running
deploy … --yesagain with a newer version of the tool. It keeps your entity, host, rails and engine from the previous deploy. - See the team and trusted oracles:
admin team listandadmin oracles list. npx --yes @ixo/payouts-facilitator@0.2.0 <command> --helplists every flag. Exit codes: 0 ok, 1 failed, 2 usage error.
Not installed by the tool: Visa Direct, the emails to payees and team members, and Sentry. Ask IXO if you need them.
Troubleshooting
deploy refuses because of missing secrets
The Worker has secrets that your secrets file does not. Restore the file from your backup, or pass
--secrets-file with its path. Do not use --new-secrets unless you mean to start over.
/health answers misconfigured
A rail is listed but its settings are missing, for example the Stripe key. deploy prints the
reasons (setting names only, never values). Fix them and deploy again.
trust-check reports problems
Most problems mean the link step is not finished. Run anchor … --yes, then deploy … --yes, then
the check again. The session must belong to a controller of the company entity.