plugins/radius/skills/dripping-faucet/SKILL.md
Request testnet or mainnet tokens from a Radius Network faucet. Use when the user says "fund my wallet", "get testnet tokens", "get mainnet tokens", "drip SBC", "use the faucet", "get test funds", "fund my wallet on mainnet", "get SBC on mainnet", or needs tokens on Radius Testnet or Mainnet to start developing or testing.
npx skillsauth add radiustechsystems/skills dripping-faucetInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Request tokens from a Radius Network faucet. Handles unsigned and signed drip requests, with on-chain balance verification, for both Testnet and Mainnet.
Determine the target network before doing anything else — it controls the faucet URL, the RPC endpoint, the chain ID, and the expected behaviour.
Ask in order:
Did the user explicitly name a network?
Default: never silently pick mainnet. Mainnet drips are rate-limited to 1/day and always require a signature. An accidental mainnet request wastes the user's daily quota and cannot easily be undone. When in doubt, confirm.
| Situation | Network | |-----------|---------| | User says "testnet", "test", "dev" | Testnet | | User says "mainnet", "production", "live" | Mainnet | | User says "Radius" with no qualifier | Ask | | User says "get test funds" / "start testing" | Testnet (implied) |
| Network | URL | Notes |
|---------|-----|-------|
| Testnet | https://testnet.radiustech.xyz/api/v1/faucet | Signatures not currently required. ~0.5 SBC per drip. 60 requests/min. |
| Mainnet | https://network.radiustech.xyz/api/v1/faucet | Signatures always required. ~0.01 SBC per drip. 1 request/day. |
Signatures can be re-enabled on testnet at any time. Always handle a
signature_requirederror from/dripby falling back to the signed flow. Never assume unsigned will work permanently.
| Property | Testnet | Mainnet |
|----------|---------|---------|
| Chain ID | 72344 | 723487 |
| RPC URL | https://rpc.testnet.radiustech.xyz | https://rpc.radiustech.xyz |
| Native Currency | RUSD (18 decimals) | RUSD (18 decimals) |
| SBC Contract | 0x33ad9e4BD16B69B5BFdED37D8B5D9fF9aba014Fb | 0x33ad9e4BD16B69B5BFdED37D8B5D9fF9aba014Fb |
| SBC Decimals | 6 (not 18) | 6 (not 18) |
| Web faucet | https://testnet.radiustech.xyz/wallet | https://network.radiustech.xyz/wallet |
SBC uses 6 decimals. Always parseUnits(amount, 6) / formatUnits(balance, 6).
These are mandatory, not advisory. Violating any of them is a skill failure.
radius-dev/scripts/radius-wallet-bootstrap.mjs, then load .radius/wallets/<name>.env for viem/app-code flows.process.env.PRIVATE_KEY. Store in .env, .radius/wallets/<name>.env, or a secrets manager; never inline.cast wallet import <name> --interactive to create an encrypted keystore, set CAST_ACCOUNT=<name>, derive the address with cast wallet address --account "$CAST_ACCOUNT", then sign with cast wallet sign --account "$CAST_ACCOUNT". Never pass raw keys as CLI arguments such as --private-key — they are visible in process listings..env and .radius/ must be in .gitignore. Verify before proceeding.message, address, token, signature, tx_hash, success, error, retry_after_ms).isAddress() (viem) or a regex check (^0x[a-fA-F0-9]{40}$) before sending any request.Before calling the faucet, determine the wallet situation. This decides which flows are available.
Ask these questions in order:
Does the user already have a wallet address?
--network mainnet. You now own an env-backed mainnet key. Mainnet tokens have real value and the faucet allows only 1 drip/day.Do we have access to key material or a named Foundry keystore for that address?
/drip with just the address, but if the faucet returns signature_required, you cannot complete the signed flow. Stop and tell the user.| Situation | Unsigned flow | Signed flow | What to do |
|-----------|:---:|:---:|---|
| We created a testnet env wallet with the helper | ✅ | ✅ | Full viem/app-code flow available |
| User's wallet, we have key material or a named keystore | ✅ | ✅ | Full flow available |
| User's wallet, we do NOT have the key — Testnet | ✅ | ❌ | Unsigned only — if signature_required, ask the user to provide the key or use the testnet web faucet |
| User's wallet, we do NOT have the key — Mainnet | ⚠️ | ❌ | Unsigned will almost certainly fail (signature_required). Ask for the key upfront, or direct the user to the mainnet web faucet before attempting anything. |
Key rule: never attempt the signed flow without confirmed signing access through app-code key material or a Foundry keystore account. On mainnet, if you only have an address, proactively tell the user that a signature will be required and ask for signing access before making any requests.
1. POST /drip with address + token (no signature)
→ success? → verify on-chain balance > 0 → done
→ signature_required? → continue to signed flow
→ rate_limited? → wait retry_after_ms, then retry
2. Signed flow (only if step 1 returns signature_required, OR when targeting mainnet and we know a signature is required):
a. Check status → rate_limited? → wait, then retry
b. Get challenge → extract "message" field only
c. Sign challenge (EIP-191 personal_sign)
d. POST /drip with address + token + signature
e. Evaluate: drip.success === true?
→ yes: verify on-chain balance > 0 → done
→ no: check error code → adapt and retry (max 2 retries)
On testnet today, step 1 succeeds without a signature. But always implement the full flow — signatures can be re-enabled at any time.
On mainnet, step 1 will always return signature_required. If you already know the target network is mainnet and you have the key, you may skip straight to the signed flow to avoid the extra round-trip. If you don't have the key, stop immediately and direct the user to the mainnet web faucet.
When running bash commands as an agent (e.g. in Claude Code), every shell invocation is a new process — variables do not persist between calls. Either:
&& or ;), orcurl and cast so the agent can see and use the output in subsequent commands.Every curl and cast call in the examples below includes an explicit echo of its output. This is not optional — without it, the agent sees (No output) and cannot proceed.
import { defineChain, createPublicClient, http, erc20Abi, isAddress, formatUnits } from 'viem';
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
// --- Network configuration ---
type Network = 'testnet' | 'mainnet';
const NETWORK_CONFIG: Record<Network, { faucetUrl: string; chain: Chain }> = {
testnet: {
faucetUrl: 'https://testnet.radiustech.xyz/api/v1/faucet',
chain: defineChain({
id: 72344,
name: 'Radius Testnet',
nativeCurrency: { decimals: 18, name: 'RUSD', symbol: 'RUSD' },
rpcUrls: { default: { http: ['https://rpc.testnet.radiustech.xyz'] } },
blockExplorers: {
default: { name: 'Radius Testnet Explorer', url: 'https://testnet.radiustech.xyz' },
},
fees: radiusFees,
}),
},
mainnet: {
faucetUrl: 'https://network.radiustech.xyz/api/v1/faucet',
chain: defineChain({
id: 723487,
name: 'Radius Mainnet',
nativeCurrency: { decimals: 18, name: 'RUSD', symbol: 'RUSD' },
rpcUrls: { default: { http: ['https://rpc.radiustech.xyz'] } },
blockExplorers: {
default: { name: 'Radius Explorer', url: 'https://network.radiustech.xyz' },
},
fees: radiusFees,
}),
},
};
const SBC_CONTRACT = '0x33ad9e4BD16B69B5BFdED37D8B5D9fF9aba014Fb' as const;
const SBC_DECIMALS = 6;
const radiusTestnet = defineChain({
id: 72344,
name: 'Radius Testnet',
nativeCurrency: { decimals: 18, name: 'RUSD', symbol: 'RUSD' },
rpcUrls: { default: { http: ['https://rpc.testnet.radiustech.xyz'] } },
blockExplorers: {
default: { name: 'Radius Testnet Explorer', url: 'https://testnet.radiustech.xyz' },
},
});
// --- Wallet setup ---
// Option A: We have an existing key (user's wallet, stored in .env)
// const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
// Option B: We only have an address (no key — unsigned flow only; mainnet will always fail)
// const addressOnly = '0x...' as `0x${string}`;
// Option C: Create a new wallet (we own the key)
const privateKey = generatePrivateKey();
const account = privateKeyToAccount(privateKey);
// SECURITY: only log the address, never the key
console.log('Wallet address:', account.address);
// If using Option B, set account to null — the signed fallback will not be available.
// The dripWithRetry function below handles this.
// --- Faucet drip with eval loop ---
async function dripWithRetry(
address: string,
/** Pass null if we don't have the private key — signed fallback will be skipped. */
signer: { signMessage: (args: { message: string }) => Promise<string> } | null,
network: Network = 'testnet',
maxAttempts = 3
): Promise<{ success: boolean; network: Network; tx_hash?: string; balance?: string; error?: string }> {
if (!isAddress(address)) {
return { success: false, network, error: `Invalid address: ${address}` };
}
const { faucetUrl, chain } = NETWORK_CONFIG[network];
// On mainnet, signature is always required. If we have no signer, fail fast
// rather than wasting the user's 1-per-day quota on a request that will be rejected.
if (network === 'mainnet' && !signer) {
return {
success: false,
network,
error: 'mainnet_signature_required_but_no_key',
};
}
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
// 1. Try unsigned drip first (skipping straight to signed flow on mainnet is an
// optimisation you may apply, but the unsigned attempt is safe to make here
// since the signed fallback is implemented below).
const dripRes = await fetch(`${faucetUrl}/drip`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, token: 'SBC' }),
});
let drip = await dripRes.json();
// 2. If signature required, fall back to signed flow (only if we have a signer)
if (drip.error === 'signature_required') {
if (!signer) {
return {
success: false,
network,
error: 'signature_required_but_no_key',
};
}
console.log('Signature required — switching to signed flow');
// Check status
const statusRes = await fetch(`${faucetUrl}/status/${address}?token=SBC`);
const status = await statusRes.json();
if (status.rate_limited) {
const waitMs = status.retry_after_ms ?? 60_000;
console.log(`Rate limited. Waiting ${waitMs}ms (attempt ${attempt}/${maxAttempts})`);
// On mainnet, retry_after_ms can be ~86_400_000 (24 hours). Do not loop — report to user.
if (waitMs > 3_600_000) {
return { success: false, network, error: `rate_limited_long_wait_ms:${waitMs}` };
}
await new Promise((r) => setTimeout(r, waitMs));
continue;
}
// Get challenge — extract only the "message" field
const challengeRes = await fetch(`${faucetUrl}/challenge/${address}?token=SBC`);
const challenge = await challengeRes.json();
const message: string = challenge.message;
// Sign (EIP-191)
const signature = await signer.signMessage({ message });
// Retry drip with signature
const signedRes = await fetch(`${faucetUrl}/drip`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, token: 'SBC', signature }),
});
drip = await signedRes.json();
}
// 3. Evaluate
if (drip.success) {
// Verify on-chain (the receipt is ground truth, not the API response)
const publicClient = createPublicClient({ chain, transport: http() });
const balance = await publicClient.readContract({
address: SBC_CONTRACT,
abi: erc20Abi,
functionName: 'balanceOf',
args: [address as `0x${string}`],
});
const formatted = formatUnits(balance, SBC_DECIMALS);
console.log(`SBC balance (${network}): ${formatted}`);
return { success: true, network, tx_hash: drip.tx_hash, balance: formatted };
}
// Critique: map error to action
console.error(`Attempt ${attempt} failed: ${drip.error} — ${drip.message ?? ''}`);
if (drip.error === 'rate_limited') {
const waitMs = drip.retry_after_ms ?? 60_000;
// On mainnet, a rate_limited response means ~24h. Stop immediately.
if (waitMs > 3_600_000) {
return { success: false, network, error: `rate_limited_long_wait_ms:${waitMs}` };
}
await new Promise((r) => setTimeout(r, waitMs));
continue;
}
if (drip.error === 'invalid_signature') {
// Re-fetch challenge in case it rotated
continue;
}
if (['faucet_empty', 'sbc_not_configured', 'internal_error'].includes(drip.error)) {
return { success: false, network, error: drip.error };
}
}
return { success: false, network, error: 'max_attempts_exceeded' };
}
// Testnet — create a throwaway wallet, no signature needed today
const testnetResult = await dripWithRetry(account.address, account, 'testnet');
console.log('Testnet result:', JSON.stringify(testnetResult, null, 2));
// Mainnet — use an existing wallet whose key is available; signature always required
// const mainnetAccount = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
// const mainnetResult = await dripWithRetry(mainnetAccount.address, mainnetAccount, 'mainnet');
// console.log('Mainnet result:', JSON.stringify(mainnetResult, null, 2));
// If you only have an address and no key on testnet (unsigned-only):
// dripWithRetry(addressOnly, null, 'testnet');
// NOTE: dripWithRetry(addressOnly, null, 'mainnet') will return immediately with
// mainnet_signature_required_but_no_key — mainnet always requires a signature.
For a fresh wallet in an agent demo, use the radius-dev helper and then run the TypeScript/viem flow above:
node plugins/radius/skills/radius-dev/scripts/radius-wallet-bootstrap.mjs --name radius-demo --network testnet
set -a; . .radius/wallets/radius-demo.env; set +a
For a mainnet wallet, pass --network mainnet — the helper writes the mainnet chain ID and RPC into the env file.
Use OWNER as the faucet address. If the faucet requires a signature, use the env-backed viem signer from PRIVATE_KEY; do not switch to cast wallet sign unless the user already has a Foundry keystore account.
#!/usr/bin/env bash
set -euo pipefail
# Set NETWORK to "testnet" or "mainnet". Default: testnet.
NETWORK="${NETWORK:-testnet}"
if [ "$NETWORK" = "mainnet" ]; then
FAUCET_URL="https://network.radiustech.xyz/api/v1/faucet"
RPC_URL="https://rpc.radiustech.xyz"
WEB_FAUCET="https://network.radiustech.xyz/wallet"
else
FAUCET_URL="https://testnet.radiustech.xyz/api/v1/faucet"
RPC_URL="https://rpc.testnet.radiustech.xyz"
WEB_FAUCET="https://testnet.radiustech.xyz/wallet"
fi
SBC_CONTRACT="0x33ad9e4BD16B69B5BFdED37D8B5D9fF9aba014Fb"
# Use an existing Foundry keystore account. You can provide it as CAST_ACCOUNT
# or as the first argument. Create it beforehand with:
# cast wallet import <name> --interactive
CAST_ACCOUNT="${CAST_ACCOUNT:-${1:-}}"
if [ -z "$CAST_ACCOUNT" ]; then
echo "ERROR: set CAST_ACCOUNT or pass a Foundry keystore account name."
echo "Create one first with: cast wallet import <name> --interactive"
echo "Or use the web faucet: $WEB_FAUCET"
exit 1
fi
ADDRESS="${OWNER:-$(cast wallet address --account "$CAST_ACCOUNT")}"
echo "Wallet ($NETWORK): $ADDRESS"
# 1. Try unsigned drip first
# On mainnet this will return signature_required immediately — that is expected.
DRIP=$(curl -s -X POST "$FAUCET_URL/drip" \
-H "Content-Type: application/json" \
-d "{\"address\": \"$ADDRESS\", \"token\": \"SBC\"}")
echo "Drip response: $DRIP"
ERROR=$(echo "$DRIP" | jq -r '.error // empty')
# 2. If signature required, fall back to signed flow
if [ "$ERROR" = "signature_required" ]; then
echo "Signature required — switching to signed flow"
# Check status
STATUS=$(curl -s "$FAUCET_URL/status/$ADDRESS?token=SBC")
echo "Status response: $STATUS"
if [ "$(echo "$STATUS" | jq -r '.rate_limited')" = "true" ]; then
WAIT=$(echo "$STATUS" | jq -r '.retry_after_ms // 60000')
echo "Rate limited. Retry after ${WAIT}ms"
# On mainnet, WAIT is ~86400000 (24 hours) — do not loop, just report.
echo "If this is mainnet, your daily quota is exhausted. Try again tomorrow or use: $WEB_FAUCET"
exit 1
fi
# Get challenge — extract message only
CHALLENGE=$(curl -s "$FAUCET_URL/challenge/$ADDRESS?token=SBC")
echo "Challenge response: $CHALLENGE"
MESSAGE=$(echo "$CHALLENGE" | jq -r '.message')
# Sign with keystore (never pass raw keys on the CLI)
SIGNATURE=$(cast wallet sign --account "$CAST_ACCOUNT" "$MESSAGE")
echo "Signature: $SIGNATURE"
# Retry drip with signature
DRIP=$(curl -s -X POST "$FAUCET_URL/drip" \
-H "Content-Type: application/json" \
-d "{\"address\": \"$ADDRESS\", \"token\": \"SBC\", \"signature\": \"$SIGNATURE\"}")
echo "Signed drip response: $DRIP"
fi
# 3. Evaluate
SUCCESS=$(echo "$DRIP" | jq -r '.success')
if [ "$SUCCESS" != "true" ]; then
echo "Drip failed: $(echo "$DRIP" | jq -r '.error') — $(echo "$DRIP" | jq -r '.message // empty')"
exit 1
fi
echo "TX hash: $(echo "$DRIP" | jq -r '.tx_hash')"
# 4. Verify balance on-chain
# cast call returns decimal with annotation e.g. "500000 [5e5]" — extract first word, then divide by 1e6
BALANCE_RAW=$(cast call "$SBC_CONTRACT" "balanceOf(address)(uint256)" "$ADDRESS" --rpc-url "$RPC_URL")
echo "Balance raw: $BALANCE_RAW"
BALANCE_UNITS=$(echo "$BALANCE_RAW" | awk '{print $1}')
echo "SBC balance ($NETWORK): $(echo "scale=6; $BALANCE_UNITS / 1000000" | bc) SBC"
If you only have an address and no private key, you can only use the unsigned flow.
#!/usr/bin/env bash
set -euo pipefail
# Set NETWORK to "testnet" or "mainnet". Default: testnet.
NETWORK="${NETWORK:-testnet}"
if [ "$NETWORK" = "mainnet" ]; then
echo "ERROR: address-only (unsigned) flow cannot be used on mainnet."
echo "Mainnet always requires a signature. Provide the private key/keystore, or use:"
echo " https://network.radiustech.xyz/wallet"
exit 1
fi
FAUCET_URL="https://testnet.radiustech.xyz/api/v1/faucet"
SBC_CONTRACT="0x33ad9e4BD16B69B5BFdED37D8B5D9fF9aba014Fb"
RPC_URL="https://rpc.testnet.radiustech.xyz"
ADDRESS="${1:?Usage: $0 <address>}"
echo "Funding (unsigned only, testnet): $ADDRESS"
# Unsigned drip — the only option without a key
DRIP=$(curl -s -X POST "$FAUCET_URL/drip" \
-H "Content-Type: application/json" \
-d "{\"address\": \"$ADDRESS\", \"token\": \"SBC\"}")
echo "Drip response: $DRIP"
ERROR=$(echo "$DRIP" | jq -r '.error // empty')
if [ "$ERROR" = "signature_required" ]; then
echo "ERROR: Faucet requires a signature but we don't have the private key for $ADDRESS."
echo "Ask the user to provide the key/keystore, or use the web faucet: https://testnet.radiustech.xyz/wallet"
exit 1
fi
SUCCESS=$(echo "$DRIP" | jq -r '.success')
if [ "$SUCCESS" != "true" ]; then
echo "Drip failed: $(echo "$DRIP" | jq -r '.error') — $(echo "$DRIP" | jq -r '.message // empty')"
exit 1
fi
echo "TX hash: $(echo "$DRIP" | jq -r '.tx_hash')"
# Verify balance on-chain
BALANCE_RAW=$(cast call "$SBC_CONTRACT" "balanceOf(address)(uint256)" "$ADDRESS" --rpc-url "$RPC_URL")
echo "Balance raw: $BALANCE_RAW"
BALANCE_UNITS=$(echo "$BALANCE_RAW" | awk '{print $1}')
echo "SBC balance (testnet): $(echo "scale=6; $BALANCE_UNITS / 1000000" | bc) SBC"
First-time Foundry keystore setup (for existing CLI wallets, only run once, interactively):
cast wallet import my-wallet --interactive
# Paste private key when prompted — it never appears in shell history or ps output
export CAST_ACCOUNT=my-wallet
OWNER="$(cast wallet address --account "$CAST_ACCOUNT")"
Manual temporary testnet wallet setup (prefer the bootstrap helper for agents):
# Generate a new keypair if you need a disposable testnet wallet.
# The command prints sensitive key material. Do not echo, paste into chat,
# commit, or log the private key.
cast wallet new
# Then import the generated key interactively. Do not pass it as a CLI argument.
cast wallet import faucet-testnet --interactive
export CAST_ACCOUNT=faucet-testnet
OWNER="$(cast wallet address --account "$CAST_ACCOUNT")"
# Run the drip flow using --account "$CAST_ACCOUNT".
These mistakes are easy to make and have been observed in practice:
| Pitfall | Wrong | Right |
|---------|-------|-------|
| Logging wallet output | echo "$WALLET_OUT" or echo "key length: ${#PRIVATE_KEY}" exposes the key | Only echo "Wallet: $ADDRESS" |
| Silent curl | curl -sf captures to variable but agent sees (No output) | curl -s + echo "Response: $VAR" on the next line |
| Parsing cast wallet new fields | awk '{print $2}' → gets key: not the key ("Private key:" is two words) | awk '/^Private key:/{print $NF}' |
| Importing via stdin | echo "$KEY" \| cast wallet import … → pipe is silently ignored, keystore file never created | cast wallet import faucet-tmp --interactive |
| Assuming keystore passwords persist | CAST_UNSAFE_PASSWORD="" cast wallet sign … → may still prompt or fail depending on keystore setup | Let cast wallet sign --account … "$MESSAGE" prompt, or use the secure local keystore workflow already configured for your machine |
| Signing for personal_sign | cast wallet sign --no-hash "$MESSAGE" → --no-hash is for raw 32-byte hashes | cast wallet sign --account … "$MESSAGE" (default adds EIP-191 prefix) |
| Reusing a fixed keystore name | Importing a new key into an existing account name can leave signing tied to the stale key → invalid_signature | Use a unique CAST_ACCOUNT name per CLI wallet, or use the bootstrap helper for agent-created testnet env wallets |
| Parsing cast call balance output | int("500000 [5e5]", 16) → ValueError | Extract first word (awk '{print $1}'), it is decimal not hex |
| Variables across shells | Setting FAUCET_URL=... in one agent bash call, using $FAUCET_URL in the next → empty | Run the entire flow in one command, or inline all values |
| Wrong network after copy-paste | Copying a testnet example without updating FAUCET_URL / RPC_URL → drip hits testnet faucet but on-chain check queries testnet RPC; mainnet balance stays 0 | Always set both FAUCET_URL and RPC_URL from the same NETWORK variable |
| Unsigned flow on mainnet | Sending a /drip request without a signature to the mainnet faucet and waiting for it to succeed | Mainnet always returns signature_required. Either go straight to the signed flow, or fail fast if you don't have the key |
| Retrying after mainnet rate limit | Looping on a rate_limited error from mainnet with the same wait-and-retry logic used on testnet | Mainnet retry_after_ms is ~86 400 000 ms (24 hours). Stop immediately, report the wait time to the user, and do not retry in-process |
| Using testnet chain for mainnet on-chain check | Hardcoding chain: radiusTestnet in createPublicClient regardless of network → balanceOf query goes to the wrong chain, always returns 0 | Derive the chain from the network parameter; use NETWORK_CONFIG[network].chain |
| Creating a wallet you'll forget about | Generating a fresh mainnet wallet and losing track of .radius/wallets/<name>.env | Mainnet tokens have real value — generate the wallet only when you mean to keep it |
When an agent executes this skill, it should follow the evaluator-optimizer pattern:
drip.success === true in the API responsebalanceOf returns a value greater than zero for the target address, queried against the correct network's RPC| Error | Root Cause | Agent Action |
|-------|-----------|--------------|
| rate_limited (testnet) | Too many requests from this address | Wait retry_after_ms, then retry |
| rate_limited (mainnet) | Daily quota exhausted | Stop. Report to user. Retry tomorrow. Do not loop. |
| signature_required | Faucet has signatures enabled (always on mainnet) | Fall back to signed flow — but only if we have the private key. If not, stop and tell the user. |
| invalid_signature | Wrong key or stale challenge | Re-fetch challenge, re-sign, retry |
| faucet_empty | Faucet wallet is drained | Stop. Report to user. Retry later. |
| sbc_not_configured | Server misconfiguration | Stop. Report to user. |
| internal_error | Server-side failure | Retry once, then stop. |
| Balance is 0 after success response | TX may be pending or RPC lag | Wait 2s, re-check balance once |
| Balance is 0 and network is wrong | On-chain check used wrong chain/RPC | Verify publicClient is using the same network as the faucet request |
Return this shape so callers can programmatically evaluate:
{
"success": true,
"network": "testnet",
"address": "0x...",
"token": "SBC",
"tx_hash": "0x...",
"balance": "0.5",
"attempts": 1,
"error": null
}
The network field is required — callers must be able to verify that the correct network was targeted without inspecting logs.
Maximum 3 attempts total. If all fail, return the structured output with success: false and the last error. Do not retry infinitely. On mainnet, a rate_limited response with retry_after_ms > 3_600_000 counts as an immediate terminal failure — do not consume retry budget waiting 24 hours.
See references/faucet-api.md for full endpoint specifications, request/response shapes, and the complete error code catalog.
tools
Integrate x402 HTTP payment protocol on Radius. Use when the user wants to: monetize an API with per-request micropayments, add HTTP 402 payment gating to endpoints, consume a paid x402 API, sign x402 payment headers, integrate with a facilitator service, implement EIP-2612 permit + Permit2 payment signing, build pay-per-call services on Radius using SBC token, or set up x402 middleware. Covers both server-side (protect your endpoints with payment gating) and client-side (sign and pay for x402-protected endpoints). Use viem for app code signing, or Foundry cast for one-off CLI payment access.
tools
End-to-end Radius Network development playbook. Stablecoin-native EVM with sub-second finality. Uses plain viem (defineChain, createPublicClient, createWalletClient) for all TypeScript integration. wagmi for React wallet integration. Foundry for smart contract development and testing. Also covers Hardhat/ethers.js compatibility and EIP-7966 synchronous transactions. Micropayment patterns (pay-per-visit content, real-time API metering, streaming payments), x402 protocol integration, Radius x402 facilitators (Permit2 + EIP-2612), stablecoin-native fees via Turnstile, ERC-20 operations, event watching, production gotchas, and EVM compatibility differences from Ethereum.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.