Ootle Payment Gateway for WordPress
Complete Setup Guide
This guide assumes no prior Tari or Ootle experience. It starts from downloading the wallet daemon on a blank server and ends with a completed test payment in WooCommerce. Every link is clickable. Every command is copy-paste ready.
You need: a Linux/macOS/Windows server with a shell · PHP 7.4+ with curl enabled · WordPress 6.0+ with WooCommerce 6.0+ installed · outbound HTTPS access to the Ootle indexer and your wallet daemon · testnet tTARI (free from the faucet) or real TARI for production.
1. Overview — how the pieces fit together
The plugin is one of three moving parts. WooCommerce handles the storefront and orders. The wallet daemon holds your keys and signs transactions. The indexer reads on-chain state and streams events. Nothing works until all three are running and reachable.
Checkout flow, end to end: customer clicks “Pay with Ootle” → plugin calls the wallet daemon’s JSON-RPC to create a payment → customer pays on the Ootle network → an OrderPlaced event lands in the indexer → your webhook listener POSTs to ?wc-api=ootle_webhook → the plugin verifies the signature and marks the order complete.
2. Install the Ootle wallet daemon
The wallet daemon is the only process that holds your private keys. It signs every transaction the gateway sends. Everything else is read-only. Run it somewhere your WordPress server can reach over the network, and nowhere else.
2.1 · Download the wallet daemon
The release ships a standalone tari_ootle_walletd binary per platform. Note: there is no tari_ootle_wallet_cli binary in the release — the CLI is not published as a prebuilt asset. You interact with the daemon directly over JSON-RPC (which is what the plugin does).
| Platform | Asset filename (v0.45.0) |
|---|---|
| Linux x86_64 | tari_ootle_walletd-0.45.0-9c62606-linux-x86_64 |
| Linux arm64 | tari_ootle_walletd-0.45.0-9c62606-linux-arm64 |
| macOS arm64 (Apple Silicon) | tari_ootle_walletd-0.45.0-9c62606-macos-arm64 |
| Windows x64 | tari_ootle_walletd-0.45.0-9c62606-windows-x64.exe |
Get the current filenames and hashes from the releases page — the build hash (9c62606) changes with each release.
tari_ootle_walletd asset for your OS from the releases page..sha256 file. Compare before running:
# Linux example — paste the hash from the release's .sha256 file
echo "8cfdae03cc7dc7987752b2f1ae94046fc5c15258e1e26e0587977f7230712fae tari_ootle_walletd-0.45.0-9c62606-linux-x86_64" | sha256sum -c -
chmod +x tari_ootle_walletd in the download directory.~/.local/bin/ or /usr/local/bin/ (any directory already on PATH) so you can call it from anywhere.2.2 · Choose your network
| Network | Flag | Use case | Funds |
|---|---|---|---|
| esmeralda | --network esmeralda |
Testing, development, staging (default) | Free faucet |
| mainnet | --network mainnet |
Live production payments | Real TARI |
Mistakes on testnet cost nothing. Get the entire flow working on esmeralda first, then repeat the same steps on mainnet with a fresh, funded account and a production API key.
2.3 · Start the daemon
This command runs the daemon on the esmeralda testnet, bound to localhost so only this machine reaches it. The web UI is served on the same host at port 5100.
tari_ootle_walletd --network esmeralda --listen-on 127.0.0.1:5100 --authentication none --enable-vite-dev-port 5173
| Option | Meaning |
|---|---|
| –network esmeralda | Connect to the esmeralda testnet (the default). Use mainnet for production. |
| –listen-on 127.0.0.1:5100 | Bind address. Use 127.0.0.1:5100 if the plugin runs on the same machine; 0.0.0.0:5100 to accept remote connections. |
| –authentication none | Disables WebAuthn passkey login so you can drive the daemon over JSON-RPC. Test/disposable wallets only. |
| –enable-vite-dev-port 5173 | Sets the WebAuthn origin to http://localhost:5173 and enables permissive CORS — fixes the “invalid domain” error in the web UI. |
| Default ports | 5100 = JSON-RPC + web UI (the plugin connects here) |
The command above runs without additional daemon-level authentication — appropriate only for a disposable testnet wallet. The daemon logs a loud warning saying so, and it is correct. Never expose an unauthenticated daemon holding real value. In production, keep the daemon bound to 127.0.0.1 or a private network, and front it with an HTTPS reverse proxy plus a firewall.
Open http://localhost:5100 in your browser to reach the wallet web UI — create accounts, check balances, claim testnet funds, and manage API keys from there.
3. Create an account and fund it
3.1 · Create a merchant account
There is no wallet CLI binary in the release — you drive the daemon over its JSON-RPC endpoint. First bootstrap a bearer token (with --authentication none, this needs no passkey), then create the account.
# 1. Bootstrap an Admin bearer token (auth=none: no passkey, no WebAuthn) TOKEN=$(curl -s -X POST http://127.0.0.1:5100/json_rpc -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"auth.request","params":{"permissions":["Admin"],"credentials":{"None":null}},"id":1}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['token'])") # 2. Create the merchant account (returns the account + its Ootle address) curl -s -X POST http://127.0.0.1:5100/json_rpc -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{"jsonrpc":"2.0","method":"accounts.create","params":{"name":"merchant"},"id":2}' \ | python3 -m json.tool
Note the owner_public_key and the otl_esm_… address from the response — you paste the public key into the plugin’s Merchant Public Key field later.
Under --authentication none, auth.request expects a tagged enum credential. {"None":null} is the variant that satisfies it — this is the documented bootstrap that replaces the WebAuthn passkey flow, and it is what makes the “invalid domain” error irrelevant for programmatic access.
3.2 · Claim testnet funds (testnet only)
Two ways, both free on esmeralda:
| Method | How |
|---|---|
| Web UI | Click “Claim Testnet Funds” at http://localhost:5100 |
| JSON-RPC | Call accounts.create_free_test_coins against the daemon (below) |
The account argument is a tagged enum, not a bare name — pass the component address as {"ComponentAddress":"…"}. A claim also needs a max_fee.
ADDR="component_4293145b…" # your account's component_address curl -s -X POST http://127.0.0.1:5100/json_rpc -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d "{\"jsonrpc\":\"2.0\",\"method\":\"accounts.create_free_test_coins\",\"params\":{\"account\":{\"ComponentAddress\":\"$ADDR\"},\"max_fee\":1000},\"id\":3}"
The shared esmeralda faucet runs dry. Before claiming, check it: accounts.get_faucet_balance returns balance and claim_amount. If balance < claim_amount the claim fails with “the testnet faucet does not hold enough funds.” That is a testnet top-up issue on Tari’s side, not a client bug — ask in the Tari Discord #development channel and retry once refilled. The alternative is claiming tTARI by burning XTM on L1, which needs real funds.
3.3 · Confirm your balance
curl -s -X POST http://127.0.0.1:5100/json_rpc -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d "{\"jsonrpc\":\"2.0\",\"method\":\"accounts.get_balances\",\"params\":{\"account\":{\"ComponentAddress\":\"$ADDR\"}},\"id\":4}" \ | python3 -m json.tool
You must see a non-zero balance before moving on. An empty account cannot pay transaction fees, and every Ootle transaction pays fees in TARI.
4. Mint an API key
The plugin authenticates to the wallet daemon with a long-lived, permission-scoped API key. An Admin mints it once; the plugin then presents it as a Bearer token on every JSON-RPC call. There is no interactive login, no JWT exchange, and no session to refresh.
4.1 · How the key works
| Aspect | Detail |
|---|---|
| Format | 32 random bytes, prefixed with tw_, base64-encoded |
| Shown | Exactly once at creation — the daemon cannot display it again |
| Stored | Only a SHA-256 hash — a database leak does not expose the key |
| Sent as | Authorization: Bearer tw_... on every request |
| Revocation | Immediate — the next request after revoke fails, no cache |
The raw key is displayed once, in the creation dialog, and then never again. Close the dialog and it is gone. Paste it directly into your password manager — not a text file, not a ticket, not chat.
4.2 · Choose the smallest scope set
Grant only the permissions the plugin actually needs. An agent cannot self-promote: permissions come from the stored key row, not from anything the plugin sends.
| Plugin role | Recommended scopes |
|---|---|
| Monitoring only | AccountInfo, TransactionGet |
| Full payment gateway | AccountInfo, TransactionSend_<your-account>, TransactionGet |
Admin lets the key mint, list, and revoke other keys. Grant it only with a deliberate reason, and only with the explicit confirmation flag. For a payment gateway, the transaction-send scope for your one merchant account is enough.
4.3 · Create the key via Web UI (optional)
The web UI works too, but only if WebAuthn can reach it — open http://localhost:5100/api-keys and log in via passkey. If you hit an “invalid domain” error (WebAuthn rejects non-localhost origins), use the JSON-RPC method in 4.4 instead — it needs no browser at all.
/api-keys route in the wallet web UI.wordpress-gateway.4.4 · Create the key over JSON-RPC (no CLI, no passkey)
With --authentication none, mint the key directly against the daemon using the bootstrap token. The raw key prints to the response once — pipe it straight into your secret store.
# Basic key with two scopes curl -s -X POST http://127.0.0.1:5100/json_rpc -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{"jsonrpc":"2.0","method":"auth.create_api_key","params":{"name":"wordpress-gateway","permissions":["AccountInfo","TransactionGet"]},"id":3}' \ | python3 -m json.tool # With an expiry (unix seconds; None = never) curl -s -X POST http://127.0.0.1:5100/json_rpc -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{"jsonrpc":"2.0","method":"auth.create_api_key","params":{"name":"wordpress-gateway","permissions":["AccountInfo","TransactionGet"],"expires_at":1893456000},"id":4}' \ | python3 -m json.tool # Granting Admin requires the explicit confirm_admin flag curl -s -X POST http://127.0.0.1:5100/json_rpc -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{"jsonrpc":"2.0","method":"auth.create_api_key","params":{"name":"full-access","permissions":["Admin"],"confirm_admin":true},"id":5}' \ | python3 -m json.tool
| Task | RPC method |
|---|---|
| List keys | auth.list_api_keys — metadata only, never raw keys |
| Revoke | auth.revoke_api_key with {"id":<n>} — effective immediately |
4.5 · What the daemon protects you from
- Wrong, revoked, or unknown key — identical error message and timing for all three; an attacker cannot enumerate valid hashes.
- Out-of-scope request — the stored scope set is enforced against every endpoint; the request fails with
InsufficientPermissions. - Non-admin minting keys — rejected; only
Adminreaches the key-management endpoints. - Expired key — silently filtered out; the next request fails exactly like a revoked key.
5. Install the plugin
ootle-payment-gateway.zip file and click Install Now. The plugin registers itself under gateway id ootle via the woocommerce_payment_gateways filter and flushes rewrite rules so the webhook endpoint resolves. Nothing else is written to your database until you save the settings.
6. Configure the plugin
Go to WooCommerce → Settings → Payments. You’ll see the Payment providers list — find the Ootle Payment Gateway row (badged “Test mode”) and click its Manage button to open the settings form. Fill in every field below, then Save changes at the bottom of the page.
| Field | What to enter | Notes |
|---|---|---|
| Enable Ootle Gateway | Check the box | Required off by default |
| Payment Method Title | Pay with Ootle | Shown to customers at checkout |
| Description | Pay securely via the Tari Ootle network | Shown under the title |
| API Key | The tw_... key from section 4 |
Paste exactly — no quotes, no extra spaces |
| API Secret | Leave blank unless signing is enabled | Only needed if you turn on HMAC request signing |
| Webhook Secret | A random 32+ character string you generate | See 6.3 below |
| API Base URL | http://your-daemon-host:5100 |
Your wallet daemon JSON-RPC endpoint |
| Test Mode | Check for testnet | Disables SSL verification for local/self-signed daemons |
6.1 · Stealth (Confidential) Payments — the six fields
Checking Stealth (Confidential) Payments turns on private settlement: every order settles to a
one-time stealth address with a blinded amount (Pedersen commitment) and an encrypted on-chain
memo. Six fields control this. Fill them from your wallet daemon’s JSON-RPC responses.
| Field | What to enter | Where to get it |
|---|---|---|
| Stealth (Confidential) Payments | Check the box | Required to enable confidential settlement |
| Merchant Account | Your account component_… address |
From accounts.create or accounts.list — the component_address field |
| Merchant Public Key | The account owner_public_key |
From accounts.create / accounts.list — used to derive stealth addresses |
| Resource Address | The resource_… address of your token |
From accounts.associate_stealth_resource — the confidential resource you accept |
| Resource View Key | The resource view_key |
From accounts.associate_stealth_resource — lets the merchant decrypt received amounts |
| Max Fee (µT) | Maximum fee per transaction, in micro-Tari | Your choice — e.g. 1000 (1,000,000 µT = 1 TARI). Set a ceiling so fees never surprise you |
Getting the values from the daemon
With your Admin token from section 3, run these against http://127.0.0.1:5100/json_rpc:
{
"jsonrpc": "2.0",
"method": "accounts.list",
"params": { "offset": 0, "limit": 10 },
"id": 1
}
Copy component_address → Merchant Account, and owner_public_key → Merchant Public Key.
{
"jsonrpc": "2.0",
"method": "accounts.associate_stealth_resource",
"params": {
"account": { "ComponentAddress": "component_…" },
"resource_address": "resource_…"
},
"id": 2
}
The response returns the resource address and view_key. Paste them into
Resource Address and Resource View Key.
{
"jsonrpc": "2.0",
"method": "accounts.get_balances",
"params": { "account": { "ComponentAddress": "component_…" } },
"id": 3
}
Ensure the account holds enough TARI to cover the Max Fee you set, plus settlement. On testnet,
claim from the faucet with accounts.create_free_test_coins.
address from your Merchant Public Key, blinds the amount into a Pedersen commitment, encrypts the order
reference into the memo, then submits via
accounts.stealth_transfer. Only you (holding the view key)can decrypt the amount and memo — observers cannot link orders or read prices. The merchant account args are
tagged enums: use
{"ComponentAddress":"component_…"}, never a bare string.
6.2 · Finding your API base URL
| Daemon location | API Base URL value |
|---|---|
| Same machine as WordPress | http://127.0.0.1:5100 |
| Separate server on your LAN | http://192.168.x.x:5100 |
| Remote VPS with a domain | https://ootle.yourdomain.com (reverse-proxy the daemon) |
In production, always front the daemon with a reverse proxy (nginx or Caddy) and a valid certificate, so the API key is never sent over plain HTTP. Test Mode disables SSL verification — it must be off in production.
6.3 · Generate a webhook secret
Run this in your terminal and paste the output into the Webhook Secret field. This secret signs outgoing webhook payloads so the plugin can prove they came from a trusted source. It is never sent to the wallet daemon.
openssl rand -hex 32
7. Connect the webhook
Without a webhook, orders stay in “pending” forever — the plugin never learns the payment succeeded. Your indexer listener (or any monitoring service) must POST to the endpoint below whenever an on-chain payment event matches an order.
| Item | Value |
|---|---|
| Webhook URL | https://yoursite.com/?wc-api=ootle_webhook |
| Method | POST |
| Content-Type | application/json |
| Auth header | X-Ootle-Signature: HMAC-SHA256 of the raw body, keyed with your webhook secret |
The payload carries an event type and a session id. The plugin looks up the order by session id and updates its status. Six event types are supported:
| Event | Effect on the order |
|---|---|
| payment.completed | Completed payment_complete() — transaction hash logged |
| payment.failed | Failed order marked failed |
| payment.cancelled | Cancelled order marked cancelled |
| payment.refunded | Refund WooCommerce refund created, hash stored |
| dispute.created | Note dispute note added to order |
| dispute.resolved | Resolved order updated per ruling |
Orders remain pending, which is expected. The payment flow still works — you complete orders manually from WooCommerce → Orders until the listener is running.
8. Test a payment
8.1 · Add a test product
8.2 · Place a test order
8.3 · Troubleshoot connection failures
| Symptom | Likely cause | Fix |
|---|---|---|
| cURL error 35: TLS connect error | Self-signed cert on a local daemon | Enable Test Mode in plugin settings |
| cURL error 7: connection refused | Daemon not running, or wrong host/port | Start the daemon; verify API Base URL and port |
| API error 401 | API key wrong, mistyped, or revoked | Re-copy the key; check it was not revoked |
| API error 403: InsufficientPermissions | API key missing a required scope | Add the scope and mint a new key |
| Order stuck on pending | No webhook listener running | Set up the listener (§7) or complete orders manually |
9. Going to production
9.1 · Pre-launch checklist
- Switch the network — restart the daemon with
--network mainnet. - Turn off Test Mode — uncheck it in plugin settings so SSL verification is enforced.
- Update the API Base URL — point at your production daemon endpoint.
- Use HTTPS — front the daemon with a reverse proxy and a valid certificate.
- Rotate the API key — mint a production key with production scopes; revoke the testnet key.
- Set a key expiry — long but finite, so a leaked key eventually dies.
- Fund the account — confirm the merchant account has mainnet TARI for fees.
- Place one live order — a small real purchase before announcing the gateway.
9.2 · Security rules that are not optional
| Rule | Why |
|---|---|
| Never run an unauthenticated daemon with real funds | Anyone who can reach the port can drain the account |
| Never commit the API key or webhook secret | Version control history is permanent |
| Use the smallest scope set possible | A leaked read-only key cannot drain the account |
| Set key expiry | Limits how long a leaked key is useful |
| Revoke unused keys | Fewer live keys means less attack surface |
| Firewall the daemon port | Only the WordPress server should reach port 5100 |
10. Reference — settings at a glance
| Plugin field | Testnet value | Production value |
|---|---|---|
| Enable Ootle Gateway | yes | yes |
| Payment Method Title | Pay with Ootle | Pay with Ootle |
| Description | Pay securely via the Tari Ootle network | Pay securely via the Tari Ootle network |
| API Key | tw_... (testnet key) |
tw_... (production key) |
| API Secret | blank unless signing enabled | blank unless signing enabled |
| Webhook Secret | openssl rand -hex 32 output |
different random output |
| API Base URL | http://127.0.0.1:5100 |
https://ootle.yourdomain.com |
| Test Mode | yes skips SSL verify | no enforces SSL verify |
11. Where to get help
| Resource | URL |
|---|---|
| Ootle documentation | ootle.tari.com |
| API key guide | ootle.tari.com/guides/agent-api-keys |
| Wallet setup guide | ootle.tari.com/guides/setup-a-wallet |
| Transaction overview | ootle.tari.com/guides/transaction-overview |
| Source code & issues | github.com/tari-project/tari-ootle |
| Releases (binaries) | github.com/tari-project/tari-ootle/releases |
| Community forum | community.tari.com |
If a step fails, start from the symptom table in section 8.3 before escalating. Most first-time failures are a wrong port, an unchecked Test Mode box, or a missing API key scope — all three are fixable in under a minute.