Skip to content

Commit 229fef4

Browse files
committed
docs(dripping-faucet): reflect live signature config
1 parent 2cbea65 commit 229fef4

2 files changed

Lines changed: 79 additions & 63 deletions

File tree

plugins/radius/skills/dripping-faucet/SKILL.md

Lines changed: 52 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Determine the target network **before** doing anything else — it controls the
3030
- "mainnet" / "production" / "live" → use **Mainnet**
3131
- Ambiguous (e.g. "fund my wallet", "get some SBC") → **ask the user** before proceeding.
3232

33-
2. **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.
33+
2. **Default: never silently pick mainnet.** Mainnet drips are rate-limited to 1/day and currently require a signature. An accidental mainnet request wastes the user's daily quota and cannot easily be undone. When in doubt, confirm.
3434

3535
| Situation | Network |
3636
|-----------|---------|
@@ -43,10 +43,10 @@ Determine the target network **before** doing anything else — it controls the
4343

4444
| Network | URL | Notes |
4545
|---------|-----|-------|
46-
| Testnet | `https://testnet.radiustech.xyz/api/v1/faucet` | Signatures not currently required. ~0.5 SBC per drip. 60 requests/min. |
47-
| Mainnet | `https://network.radiustech.xyz/api/v1/faucet` | Signatures **always** required. ~0.01 SBC per drip. 1 request/day. |
46+
| Testnet | `https://testnet.radiustech.xyz/api/v1/faucet` | Signatures currently required by server configuration. ~0.5 SBC per drip. 5 requests/min. |
47+
| Mainnet | `https://network.radiustech.xyz/api/v1/faucet` | Signatures currently required by server configuration. ~0.01 SBC per drip. 1 request/day. |
4848

49-
> **Signatures can be re-enabled on testnet at any time.** Always handle a `signature_required` error from `/drip` by falling back to the signed flow. Never assume unsigned will work permanently.
49+
> The OpenAPI request schema marks `signature` as optional because signature enforcement is a server-side configuration setting. Live verification on 2026-08-21 showed `signature_required` on both Testnet and Mainnet. Treat signing as required for the currently deployed services, while still handling configuration changes from the API response.
5050
5151
## Chain Configuration
5252

@@ -92,10 +92,10 @@ Before calling the faucet, determine the wallet situation. This decides which fl
9292
|-----------|:---:|:---:|---|
9393
| We created or selected a testnet `radius-cli` wallet ||| Full `radius-cli` signing flow available |
9494
| User's wallet, we have key material or an operator-approved signer ||| Full flow available |
95-
| 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](https://testnet.radiustech.xyz/wallet) |
96-
| 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](https://network.radiustech.xyz/wallet) before attempting anything. |
95+
| User's wallet, we do NOT have signing access**Testnet** | ⚠️ || The current deployment returns `signature_required`. Use an operator-approved signer, or use the [testnet web faucet](https://testnet.radiustech.xyz/wallet) |
96+
| User's wallet, we do NOT have signing access**Mainnet** | ⚠️ || The current deployment returns `signature_required`. Use an operator-approved signer, or direct the user to the [mainnet web faucet](https://network.radiustech.xyz/wallet) before attempting anything. |
9797

98-
**Key rule:** never attempt the signed flow without confirmed signing access through `radius-cli`, app-code key material, or another operator-approved signer. 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.
98+
**Key rule:** never attempt the signed flow without confirmed signing access through `radius-cli`, app-code key material, or another operator-approved signer. With the current configuration on either network, an address alone is insufficient. Never ask the user to paste a private key.
9999

100100
## Flow Overview
101101

@@ -105,7 +105,7 @@ Before calling the faucet, determine the wallet situation. This decides which fl
105105
→ signature_required? → continue to signed flow
106106
→ rate_limited? → wait retry_after_ms, then retry
107107
108-
2. Signed flow (only if step 1 returns signature_required, OR when targeting mainnet and we know a signature is required):
108+
2. Signed flow (when step 1 returns `signature_required`, as both deployments did during the latest verification):
109109
a. Check status → rate_limited? → wait, then retry
110110
b. Get challenge → extract "message" field only
111111
c. Sign challenge (EIP-191 personal_sign)
@@ -115,9 +115,9 @@ Before calling the faucet, determine the wallet situation. This decides which fl
115115
→ no: check error code → adapt and retry (max 2 retries)
116116
```
117117

118-
On testnet today, step 1 succeeds without a signature. But always implement the full flow — signatures can be re-enabled at any time.
118+
On both deployed services, step 1 currently returns `signature_required`. The unsigned probe is useful for configuration discovery and returns a challenge in `error.details.challenge`; callers may instead fetch `/challenge` directly when signing access is already confirmed.
119119

120-
**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](https://network.radiustech.xyz/wallet).
120+
With the current configuration on either network, callers with an approved signer may skip straight to the signed flow to avoid the unsigned probe. If signing access is unavailable, stop and direct the user to the matching web faucet.
121121

122122
### Agent execution note
123123

@@ -183,7 +183,7 @@ const radiusTestnet = defineChain({
183183
// Option A: We have an existing key (user's wallet, stored in .env)
184184
// const privateKey = process.env.PRIVATE_KEY as `0x${string}`;
185185

186-
// Option B: We only have an address (no keyunsigned flow only; mainnet will always fail)
186+
// Option B: We only have an address (no signercurrent deployments will reject it)
187187
// const addressOnly = '0x...' as `0x${string}`;
188188

189189
// Option C: Create a new wallet (we own the key)
@@ -198,7 +198,7 @@ console.log('Wallet address:', account.address);
198198
// --- Faucet drip with eval loop ---
199199
async function dripWithRetry(
200200
address: string,
201-
/** Pass null if we don't have the private key — signed fallback will be skipped. */
201+
/** Pass null if no operator-approved signer is available. */
202202
signer: { signMessage: (args: { message: string }) => Promise<string> } | null,
203203
network: Network = 'testnet',
204204
maxAttempts = 3
@@ -209,13 +209,13 @@ async function dripWithRetry(
209209

210210
const { faucetUrl, chain } = NETWORK_CONFIG[network];
211211

212-
// On mainnet, signature is always required. If we have no signer, fail fast
213-
// rather than wasting the user's 1-per-day quota on a request that will be rejected.
214-
if (network === 'mainnet' && !signer) {
212+
// Both deployed faucets currently require a signature. Fail fast when no
213+
// approved signer is available rather than making a request known to fail.
214+
if (!signer) {
215215
return {
216216
success: false,
217217
network,
218-
error: 'mainnet_signature_required_but_no_key',
218+
error: 'signature_required_but_no_signer',
219219
};
220220
}
221221

@@ -230,13 +230,17 @@ async function dripWithRetry(
230230
});
231231
let drip = await dripRes.json();
232232

233+
// Error responses currently use { error: { code, message, ... } }.
234+
let errorCode = typeof drip.error === 'string' ? drip.error : drip.error?.code;
235+
let errorMessage = typeof drip.error === 'string' ? drip.message : drip.error?.message;
236+
233237
// 2. If signature required, fall back to signed flow (only if we have a signer)
234-
if (drip.error === 'signature_required') {
238+
if (errorCode === 'signature_required') {
235239
if (!signer) {
236240
return {
237241
success: false,
238242
network,
239-
error: 'signature_required_but_no_key',
243+
error: 'signature_required_but_no_signer',
240244
};
241245
}
242246
console.log('Signature required — switching to signed flow');
@@ -270,6 +274,8 @@ async function dripWithRetry(
270274
body: JSON.stringify({ address, token: 'SBC', signature }),
271275
});
272276
drip = await signedRes.json();
277+
errorCode = typeof drip.error === 'string' ? drip.error : drip.error?.code;
278+
errorMessage = typeof drip.error === 'string' ? drip.message : drip.error?.message;
273279
}
274280

275281
// 3. Evaluate
@@ -288,42 +294,42 @@ async function dripWithRetry(
288294
}
289295

290296
// Critique: map error to action
291-
console.error(`Attempt ${attempt} failed: ${drip.error} — ${drip.message ?? ''}`);
297+
console.error(`Attempt ${attempt} failed: ${errorCode} — ${errorMessage ?? ''}`);
292298

293-
if (drip.error === 'rate_limited') {
294-
const waitMs = drip.retry_after_ms ?? 60_000;
299+
if (errorCode === 'rate_limited') {
300+
const waitMs = drip.error?.retry_after_ms ?? drip.retry_after_ms ?? 60_000;
295301
// On mainnet, a rate_limited response means ~24h. Stop immediately.
296302
if (waitMs > 3_600_000) {
297303
return { success: false, network, error: `rate_limited_long_wait_ms:${waitMs}` };
298304
}
299305
await new Promise((r) => setTimeout(r, waitMs));
300306
continue;
301307
}
302-
if (drip.error === 'invalid_signature') {
308+
if (errorCode === 'invalid_signature') {
303309
// Re-fetch challenge in case it rotated
304310
continue;
305311
}
306-
if (['faucet_empty', 'sbc_not_configured', 'internal_error'].includes(drip.error)) {
307-
return { success: false, network, error: drip.error };
312+
if (['faucet_empty', 'sbc_not_configured', 'internal_error'].includes(errorCode)) {
313+
return { success: false, network, error: errorCode };
308314
}
309315
}
310316

311317
return { success: false, network, error: 'max_attempts_exceeded' };
312318
}
313319

314-
// Testnet — create a throwaway wallet, no signature needed today
320+
// Testnet — create a throwaway wallet and sign the configured challenge
315321
const testnetResult = await dripWithRetry(account.address, account, 'testnet');
316322
console.log('Testnet result:', JSON.stringify(testnetResult, null, 2));
317323

318-
// Mainnet — use an existing wallet whose key is available; signature always required
324+
// Mainnet — use an existing wallet with an approved signer; signature currently required
319325
// const mainnetAccount = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
320326
// const mainnetResult = await dripWithRetry(mainnetAccount.address, mainnetAccount, 'mainnet');
321327
// console.log('Mainnet result:', JSON.stringify(mainnetResult, null, 2));
322328

323-
// If you only have an address and no key on testnet (unsigned-only):
329+
// If you only have an address and no signer on testnet (unsigned-only):
324330
// dripWithRetry(addressOnly, null, 'testnet');
325-
// NOTE: dripWithRetry(addressOnly, null, 'mainnet') will return immediately with
326-
// mainnet_signature_required_but_no_key — mainnet always requires a signature.
331+
// NOTE: the currently deployed services require a signature, so address-only
332+
// calls return immediately with signature_required_but_no_signer.
327333
```
328334

329335
## Agent-created wallet
@@ -372,13 +378,13 @@ ADDRESS="${OWNER:-$(radius-cli wallet address)}"
372378
echo "Wallet ($NETWORK): $ADDRESS"
373379

374380
# 1. Try unsigned drip first
375-
# On mainnet this will return signature_required immediately — that is expected.
381+
# Both current deployments return signature_required immediately — that is expected.
376382
DRIP=$(curl -s -X POST "$FAUCET_URL/drip" \
377383
-H "Content-Type: application/json" \
378384
-d "{\"address\": \"$ADDRESS\", \"token\": \"SBC\"}")
379385
echo "Drip response: $DRIP"
380386

381-
ERROR=$(echo "$DRIP" | jq -r '.error // empty')
387+
ERROR=$(echo "$DRIP" | jq -r 'if (.error | type) == "object" then .error.code else .error // empty end')
382388

383389
# 2. If signature required, fall back to signed flow
384390
if [ "$ERROR" = "signature_required" ]; then
@@ -414,7 +420,7 @@ fi
414420
# 3. Evaluate
415421
SUCCESS=$(echo "$DRIP" | jq -r '.success')
416422
if [ "$SUCCESS" != "true" ]; then
417-
echo "Drip failed: $(echo "$DRIP" | jq -r '.error')$(echo "$DRIP" | jq -r '.message // empty')"
423+
echo "Drip failed: $(echo "$DRIP" | jq -r 'if (.error | type) == "object" then .error.code else .error end')$(echo "$DRIP" | jq -r 'if (.error | type) == "object" then .error.message else .message // empty end')"
418424
exit 1
419425
fi
420426
echo "TX hash: $(echo "$DRIP" | jq -r '.tx_hash')"
@@ -424,12 +430,12 @@ BALANCE=$(radius-cli wallet balance --json)
424430
echo "Balance ($NETWORK): $BALANCE"
425431
```
426432

427-
## Bash Example (address-only — we do NOT own the wallet)
433+
## Bash Example (address-only — no signing access)
428434

429-
If you only have an address and no private key, you can only use the unsigned flow.
435+
If you only have an address and no approved signer, you can only probe the unsigned flow. Both deployments currently reject it with `signature_required`.
430436

431-
- On **testnet**: if the faucet requires a signature, stop and tell the user.
432-
- On **mainnet**: the faucet **always** requires a signature. Do not even attempt this flow on mainnet — direct the user to the web faucet immediately.
437+
- On **testnet**: the current deployment requires a signature, so stop and tell the user or direct them to the web faucet.
438+
- On **mainnet**: the current deployment requires a signature. Do not attempt an address-only flow; direct the user to the web faucet immediately.
433439

434440
```bash
435441
#!/usr/bin/env bash
@@ -440,7 +446,7 @@ NETWORK="${NETWORK:-testnet}"
440446

441447
if [ "$NETWORK" = "mainnet" ]; then
442448
echo "ERROR: address-only (unsigned) flow cannot be used on mainnet."
443-
echo "Mainnet always requires a signature. Provide the private key/keystore, or use:"
449+
echo "The current mainnet faucet configuration requires a signature. Use an approved signer, or visit:"
444450
echo " https://network.radiustech.xyz/wallet"
445451
exit 1
446452
fi
@@ -450,25 +456,25 @@ SBC_CONTRACT="0x33ad9e4BD16B69B5BFdED37D8B5D9fF9aba014Fb"
450456
RPC_URL="https://rpc.testnet.radiustech.xyz"
451457
ADDRESS="${1:?Usage: $0 <address>}"
452458

453-
echo "Funding (unsigned only, testnet): $ADDRESS"
459+
echo "Probing faucet configuration (unsigned only, testnet): $ADDRESS"
454460

455-
# Unsigned drip — the only option without a key
461+
# Unsigned probe — the only option without an approved signer
456462
DRIP=$(curl -s -X POST "$FAUCET_URL/drip" \
457463
-H "Content-Type: application/json" \
458464
-d "{\"address\": \"$ADDRESS\", \"token\": \"SBC\"}")
459465
echo "Drip response: $DRIP"
460466

461-
ERROR=$(echo "$DRIP" | jq -r '.error // empty')
467+
ERROR=$(echo "$DRIP" | jq -r 'if (.error | type) == "object" then .error.code else .error // empty end')
462468

463469
if [ "$ERROR" = "signature_required" ]; then
464-
echo "ERROR: Faucet requires a signature but we don't have the private key for $ADDRESS."
465-
echo "Ask the user to provide the key/keystore, or use the web faucet: https://testnet.radiustech.xyz/wallet"
470+
echo "ERROR: Faucet requires a signature but no approved signer is available for $ADDRESS."
471+
echo "Use the web faucet instead: https://testnet.radiustech.xyz/wallet"
466472
exit 1
467473
fi
468474

469475
SUCCESS=$(echo "$DRIP" | jq -r '.success')
470476
if [ "$SUCCESS" != "true" ]; then
471-
echo "Drip failed: $(echo "$DRIP" | jq -r '.error')$(echo "$DRIP" | jq -r '.message // empty')"
477+
echo "Drip failed: $(echo "$DRIP" | jq -r 'if (.error | type) == "object" then .error.code else .error end')$(echo "$DRIP" | jq -r 'if (.error | type) == "object" then .error.message else .message // empty end')"
472478
exit 1
473479
fi
474480
echo "TX hash: $(echo "$DRIP" | jq -r '.tx_hash')"
@@ -500,10 +506,10 @@ These mistakes are easy to make and have been observed in practice:
500506
| Silent curl | `curl -sf` captures to variable but agent sees `(No output)` | `curl -s` + `echo "Response: $VAR"` on the next line |
501507
| Using Foundry as the agent wallet surface | `cast wallet new` / `cast wallet sign` for a fresh agent demo | Use `RADIUS_HOME=.radius radius-cli wallet address` and `radius-cli wallet sign` |
502508
| Mixing wallet scopes | Reusing one global wallet across unrelated agent demos | Set a distinct `RADIUS_HOME` per project or agent |
503-
| Assuming signing access from an address | Treating `0x...` as enough for mainnet signed flow | Confirm `radius-cli` or another signer can sign for the address before calling the faucet |
509+
| Assuming signing access from an address | Treating `0x...` as enough for the currently configured signed flow | Confirm `radius-cli` or another approved signer can sign for the address before calling either faucet |
504510
| 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 |
505511
| 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 |
506-
| 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 |
512+
| Treating OpenAPI optionality as deployed behavior | Assuming an optional `signature` schema field means unsigned drips are accepted | Signature enforcement is configuration-driven; both services returned `signature_required` in live verification on 2026-08-21 |
507513
| 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 |
508514
| 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` |
509515
| Creating a wallet you'll forget about | Generating a fresh mainnet wallet in an unclear scope | Mainnet tokens have real value — set `RADIUS_HOME` intentionally and record which project owns it |
@@ -523,7 +529,7 @@ When an agent executes this skill, it should follow the evaluator-optimizer patt
523529
|-------|-----------|--------------|
524530
| `rate_limited` (testnet) | Too many requests from this address | Wait `retry_after_ms`, then retry |
525531
| `rate_limited` (mainnet) | Daily quota exhausted | Stop. Report to user. Retry tomorrow. Do not loop. |
526-
| `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. |
532+
| `signature_required` | Faucet has signature enforcement enabled (currently both networks) | Fall back to signed flow — but **only with an operator-approved signer**. If none is available, stop and tell the user. |
527533
| `invalid_signature` | Wrong key or stale challenge | Re-fetch challenge, re-sign, retry |
528534
| `faucet_empty` | Faucet wallet is drained | Stop. Report to user. Retry later. |
529535
| `sbc_not_configured` | Server misconfiguration | Stop. Report to user. |

0 commit comments

Comments
 (0)