Deposit once.
Pay a million times.
One deposit on Polygon opens a channel that pays any merchant. Every request after that is one EIP-712 signature from a hot session key — no gas, no transaction, no waiting for a block. The hub takes it in 32.3 µs and the response comes back paid.
Read https://paychannels.mudit.blog/PAYING.md and use x402 to buy https://paychannels.mudit.blog/api/paid/timeThat one line is the whole onboarding. The agent discovers the chain, funds itself, deposits, signs and buys.
curl -sL https://paychannels.mudit.blog/session.sh | SITE_URL=https://paychannels.mudit.blog bashNeeds bash, curl, jq, openssl and Foundry's cast. Nothing to build.
Two keys. The hot one can't lose your money.
The root key holds funds and signs the deposit — then goes cold. The session key signs every voucher and lives wherever your agent runs. The deposit binds signer[payer] = sessionKey on-chain, so a leaked session key can at worst spend the deposit you already made on the merchants you're already paying. It can never withdraw.
mkdir -p .wallets && chmod 700 .wallets
W="$(cast wallet new)"
PAYER=$(grep -oE '0x[0-9a-fA-F]{40}' <<<"$W" | head -1)
grep -oE '0x[0-9a-fA-F]{64}' <<<"$W" | head -1 > .wallets/payer.key && chmod 600 .wallets/payer.key
S="$(cast wallet new)"
SESSION_ADDR=$(grep -oE '0x[0-9a-fA-F]{40}' <<<"$S" | head -1)
grep -oE '0x[0-9a-fA-F]{64}' <<<"$S" | head -1 > .wallets/session.key && chmod 600 .wallets/session.key
unset W SOn the devnet the dispenser hands you gas and spend in one call. Fund once for the entire lifetime of the channel.
curl -s -X POST "$DISPENSER_URL/fund" -H 'content-type: application/json' \
-d "{\"address\":\"$PAYER\"}"
# 200 -> {"address":"0x...","pol_wei":"100000000000000000","usdc":"1000000000","pol_tx":"0x...","usdc_tx":"0x..."}The last transaction you'll send.
Approve the hub, deposit, bind the session key — two transactions, once. From here every payment is free and every merchant on the hub is reachable from this one balance.
DEPOSIT=500000000 # 500 USDC of your 1000 - leaves the rest for exact + headroom
cast send "$USDC" 'approve(address,uint256)' "$HUB" "$DEPOSIT" \
--private-key "$(cat .wallets/payer.key)" --rpc-url "$RPC_URL"
cast send "$HUB" 'deposit(address,address,uint256)' "$PAYER" "$SESSION_ADDR" "$DEPOSIT" \
--private-key "$(cat .wallets/payer.key)" --rpc-url "$RPC_URL"Zero on-chain transactions per payment. Forever.
Pay by signature.
A voucher is a cumulative ceiling: cumulative = what you've spent + this price, signed by the session key. The merchant forwards it, the hub checks the signature and the headroom, serves the request, and you persist the new counter. No round trip to a chain, no confirmation to wait for.
cat > /tmp/voucher.json <<EOF
{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],
"Voucher":[{"name":"payer","type":"address"},{"name":"merchant","type":"address"},{"name":"epoch","type":"uint64"},{"name":"cumulative","type":"uint256"}]},
"primaryType":"Voucher",
"domain":{"name":"ThunderpoltHub","version":"1","chainId":$CHAIN_ID,"verifyingContract":"$HUB"},
"message":{"payer":"$PAYER","merchant":"$MERCHANT","epoch":"$EPOCH","cumulative":"$NEW"}}
EOF
VSIG=$(cast wallet sign --private-key "$(cat .wallets/session.key)" --data --from-file /tmp/voucher.json)XP=$(jq -nc --arg net "eip155:$CHAIN_ID" --arg p "$PAYER" --arg m "$MERCHANT" --arg e "$EPOCH" \
--arg c "$NEW" --arg sig "$VSIG" --arg hub "$HUB" --arg id "$PAYID" \
'{x402Version:2,scheme:"batch-settlement",network:$net,
payload:{binding:"thunderpolt-hub/v1",voucher:{payer:$p,merchant:$m,epoch:$e,cumulative:$c},signature:$sig,hub:$hub},
extensions:{"payment-identifier":{id:$id}}}' | base64 | tr -d '\n')
CODE=$(curl -s -D /tmp/ch.hdr -o /tmp/ch.json -w '%{http_code}' "$SITE_URL/api/paid/time" -H "X-PAYMENT: $XP")
[ "$CODE" = 200 ] && echo "$NEW" > .wallets/cum.txtOne signature, sixteen payments in flight
Sign a ceiling K prices high and fire K requests with distinct payment-ids under it — one signature for the whole burst, arrival order irrelevant. This is how a laptop pushes hundreds of payments a second through a single payer.
K=16
CEIL=$((OLD + K * PRICE))
# ...sign ONE voucher at cumulative=$CEIL exactly as above (VSIG)...
for k in $(seq 1 "$K"); do
PAYID="pay_$(openssl rand -hex 14)"
# build XP from the SAME voucher+VSIG, this $PAYID, and curl in the
# background — all K may be in flight at once, arrival order is free
done
wait; echo "$CEIL" > .wallets/cum.txt # persist once the burst settlesOne script. Run it twice.
session.sh runs a complete payer session against this devnet: deposit, a stream of real X-PAYMENT requests, and the settlement proof at the end. Give it a REUSE_DIR and the second run skips funding and the deposit entirely — funded once, paid again, no chain.
REUSE_DIR=~/.tp-demo-wallet ./demo/portable-session.sh # opens the channel
REUSE_DIR=~/.tp-demo-wallet ./demo/portable-session.sh # same channel, no new chain setupThe same script, pointed at this origin: curl -sL https://paychannels.mudit.blog/session.sh | SITE_URL=https://paychannels.mudit.blog bash — everything else self-discovers from /meta (PORTABLE-SESSION.md).
Stress it.
The devnet is open and the dispenser is unlimited. Point loadgen at the public URL — no engine access, no operator — and climb by adding payers. Through the unoptimised reference facilitator one 8-core pair takes 59.6k payments/s; the hubs behind it were built for 5.84M.
./target/release/loadgen amoy \
--site-url "$SITE_URL" \
--dispenser-url "$DISPENSER_URL" \
--deployments deployments/amoy.json \
--rpc-url "$RPC_URL" --chain-id 80002 \
--wallets .wallets/amoy-pool.json \
--payers 32 --in-flight 32 \
--verification verified-by-cache \
--target-rate 2000 --exact-per-min 0 \
--mix "/api/paid/time=1" \
--duration-secs 60 --report-every-secs 15 \
--out-dir bench-resultsRead https://paychannels.mudit.blog/STRESS-TESTING.md and run a 60-second load test against https://paychannels.mudit.blog, then report the fully-verified payments/s and the p95.Read https://paychannels.mudit.blog/PAYING.md, open a channel on the Paychannels devnet, and pay for https://paychannels.mudit.blog/api/paid/time 100 times as fast as you can. Show me the receipts and the total on-chain transactions you sent.The devnet is open and needs no run registration: point loadgen at the public URL and read the receipts from the response headers.
PAYING.md · STRESS-TESTING.md · PORTABLE-SESSION.md · SKILL.md · llms.txt · get paid instead →