Skip to content

New Wallet Providers, x402 Improvements, agent ergonomics, and tests#14

Open
AkasshP wants to merge 21 commits into
mainfrom
eng-1976/para-wallet-provider
Open

New Wallet Providers, x402 Improvements, agent ergonomics, and tests#14
AkasshP wants to merge 21 commits into
mainfrom
eng-1976/para-wallet-provider

Conversation

@AkasshP

@AkasshP AkasshP commented Jun 3, 2026

Copy link
Copy Markdown

What this PR does

This was a collaborative PR — Akassh, Eriks, Tyler used it to get hands-on with the radius-cli codebase, while also shipping real improvements. The result: four new wallet providers, a cleaner provider architecture, x402 protocol fixes, and better agent/script ergonomics.


1. Wallet provider abstraction

The keystore is now one of five providers rather than the only option. A new --wallet flag (or RADIUS_WALLET env var) selects which one to use. The wallet login, wallet logout, and wallet status commands all work regardless of which provider is active.

radius-cli --wallet para wallet login
radius-cli --wallet cdp  wallet address
radius-cli --wallet proxy wallet x402 get https://example.com/resource

Provider config lives in ~/.radius/config.json under a namespaced key (providers.para, providers.cdp, etc.) so adding providers doesn't touch the core config logic.


2. New wallet providers

Para (--wallet para)

Logs in with email. Uses Para's server SDK to generate a pregenerated (unclaimed) MPC wallet. Session saved to ~/.radius/para-session.json. Signs with createParaViemAccount.

Coinbase CDP (--wallet cdp)

Logs in with a CDP API Key ID, API Key Secret, and Wallet Secret. Manages server-side EVM accounts — wallets live in CDP's infrastructure. Legacy transaction signing works via a signHash workaround (CDP's native signTransaction doesn't support legacy txs, which is what Radius uses).

Privy (--wallet privy)

Logs in with a Privy App ID and App Secret, plus a wallet ID. Signs via Privy's REST API (personal_sign, eth_signTypedData_v4, secp256k1_sign). Same legacy tx workaround as CDP.

Proxy (--wallet proxy)

Config-only provider with no login/logout. Delegates all signing to a remote HTTP wallet-proxy service (e.g. a Cloudflare Worker). The private key never leaves the proxy — the CLI just sends signing requests.

Required config:

RADIUS_WALLET_PROXY_URL=https://your-proxy.example.com
RADIUS_WALLET_ALIAS=my-wallet

Optional auth: bearer token (RADIUS_WALLET_PROXY_TOKEN) and/or Cloudflare Access service tokens (CF_ACCESS_CLIENT_ID / CF_ACCESS_CLIENT_SECRET).


3. x402 protocol improvements

  • Permit2 flow: added buildPermit2PaymentPayload and signPermit2WitnessTransfer — supports the new Radius permit2-based settlement path
  • EIP-2612 permit flow: added signEip2612Permit and readPermitNonce for ERC-20 permit-based settlement
  • Payment header fix: payment-signature is now stripped from user-supplied headers on the initial (pre-402) request, preventing stale payment data from leaking
  • Tx hash normalization: decodePaymentResponse now accepts transaction, txHash, transactionHash, or hash from the server response

4. Agent/script ergonomics

Structured exit codes

Commands now exit with a meaningful code so agents and scripts can branch on failure category without parsing error text:

Code Meaning
0 Success
1 General error
2 Usage error (bad arguments)
3 Config error (missing env var, bad provider setup)
4 Auth error (not logged in, invalid key)
5 Network/RPC error
6 Insufficient balance
7 x402 payment declined or failed

The table is also printed at the bottom of radius-cli --help and radius-cli wallet x402 --help.

Other UX improvements

  • wallet status reports provider, login state, and address clearly for all five providers
  • Error messages now say which provider to use in the fix hint (e.g. run wallet login --wallet cdp)
  • wallet export hard-errors on remote providers (key material is intentionally not exportable)
  • wallet balance fixed after an eth_getBalance semantics change was double-counting SBC

5. Test coverage

New test files added: providers.test.ts, x402-eip2612.test.ts, x402-permit2.test.ts, x402-protocol.test.ts, balance.test.ts.

E2E results

Command keystore para cdp privy proxy
wallet status
wallet login n/a n/a
wallet address
wallet sign "hello"

AkasshP and others added 9 commits May 29, 2026 12:53
Also fix x402 challenge parsing to accept `amount` field (v2 compat).
- tests/providers.test.ts: stub providers (cdp/para/privy) reject with
  "not yet implemented" and expose no exportPrivateKey; keystore provider
  auto-creates on first use, round-trips export, honors explicit password,
  rejects wrong password; requireAccount/getOwnAddress dispatch to the
  selected provider and --private-key overrides it
- tests/x402-protocol.test.ts: lock in the `amount` fallback (x402 v2),
  precedence of maxAmountRequired, and rejection when both are absent

Covers the test gap flagged in the PR #10 review. 50/50 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Exercises the branch added in fc1173d: an invalid `amount` with no
`maxAmountRequired` reports accepts[i].amount in the error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… change

The Radius RPC change (2026-06) made eth_getBalance return
token_balance × per_unit_exchange_rate + raw_native, so the SBC token
balance is already included in the native balance. The balance command
was summing eth_getBalance + SBC balanceOf on top, double-counting SBC
(e.g. reporting $0.20 for a wallet holding only 0.1 SBC).

eth_getBalance is now treated as the total; the raw-native (RUSD) line
is derived as the remainder via splitAggregateBalance (clamped at zero),
assuming the 1 SBC = 1 native unit peg. JSON output gains totalWei and
rusdWei now reports the derived native remainder.

Note: per the same RPC change, the returned balance may exceed what the
network will provision for a single transaction — the CLI does not gate
sends on balance (viem's eth_estimateGas path validates instead).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two interop fixes verified end-to-end against a live Radius x402 server
(rad-read.replit.app — paid 0.001 SBC on mainnet, content delivered):

1. v2 payment header envelope. The CLI emitted the v1 shape (flat
   scheme/network, numeric validity bounds) regardless of challenge
   version. Per coinbase/x402 specs/schemes/exact/scheme_exact_evm.md,
   v2 echoes the chosen accepts entry verbatim as `accepted`, carries
   the challenge's `resource`, and stringifies validAfter/validBefore.
   parseChallenge now retains the raw accepts entry and resource for
   the echo. v1 output is unchanged.

2. EIP-2612 permit settlement (Radius flavor). SBC does not implement
   EIP-3009 transferWithAuthorization, so signTransferAuthorization
   payloads can never settle against it. Radius servers advertise
   `extra.settlementMethod: "permit-transferFrom"` plus
   `extra.settlementSpender` in the challenge; the CLI now detects
   that, reads the owner's permit nonce on-chain, signs an EIP-2612
   Permit for the settlement spender (deadline = now +
   maxTimeoutSeconds), and sends the documented flat envelope with a
   `kind: "permit-eip2612"` payload (v/r/s split signature). Servers
   without the extra keep the EIP-3009 path.

Tests: v2 envelope round-trip + v1 stability, permit signature
recovery to owner, permit header shape. 60/60 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per docs.radiustech.xyz/skills/x402.md and the reference client in
radiustechsystems/skills (plugins/radius/skills/x402):

- Permit2 dual-signature settlement when the challenge advertises
  extra.assetTransferMethod: "permit2": an EIP-2612 Permit for the
  canonical Permit2 contract (sequential nonce from token.nonces, goes
  in extensions.eip2612GasSponsoring for gas-free allowance setup) plus
  a Permit2 PermitWitnessTransferFrom for the x402 proxy (random
  128-bit nonce, witness binds payTo), sharing one deadline. Typed data
  taken verbatim from references/permit2-typed-data.template.json.
- Challenge detection: official v2 servers carry the challenge in a
  base64 PAYMENT-REQUIRED response header; body JSON remains the
  fallback for older servers.
- Payment is sent in both PAYMENT-SIGNATURE (official v2) and X-Payment
  (legacy) headers; receipts read from PAYMENT-RESPONSE before
  x-payment-response.
- v2 envelope keeps flat scheme/network alongside the accepted echo,
  matching the skill's payload example.

Settlement dispatch is now: permit2 (assetTransferMethod) →
permit-eip2612 (settlementMethod, verified live against rad-read) →
EIP-3009 (standard x402 default).

Tests: permit2 signature recovery under the skill typed data, payload
structure field-for-field vs the skill example, nonce randomness,
canonical addresses. 64/64 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement Para MPC wallet provider using @getpara/server-sdk and
@getpara/viem-v2-integration with pregenerated wallets for CLI use.
@AkasshP AkasshP changed the base branch from eng-1974/wallet-provider-abstraction to main June 3, 2026 22:38
@AkasshP AkasshP requested a review from erikzrekz June 3, 2026 22:45
erikzrekz

This comment was marked as outdated.

AkasshP added 2 commits June 3, 2026 22:38
- Pass session address to createParaViemAccount (fixes wrong-wallet bug)
- Use non-deprecated object overload with scoped cast
- Namespace provider config: providers.para.{apiKey,env}
- Wire resolveEnvironment to read from config (fix dead paraEnv bug)
- Merge dual resolveApiKey into single function with interactive flag
- Move Para SDK to optional peerDependencies with dynamic import
- Add mocked getAccount happy-path test
- Add UX note: pregenerated wallet not yet claimed
Implement CDP wallet provider using @coinbase/cdp-sdk with server-managed
EVM accounts. Legacy tx signing via signHash workaround for Radius chain.
Optional peer dep with dynamic import, same pattern as Para.
Comment thread src/lib/providers/cdp.ts Outdated
Comment thread src/lib/providers/cdp.ts Outdated
Comment thread src/lib/providers/cdp.ts Outdated
Comment thread src/lib/providers/cdp.ts Outdated
@erikzrekz

This comment was marked as resolved.

@AkasshP

This comment was marked as resolved.

AkasshP and others added 4 commits June 4, 2026 15:53
- Auto-generate account name when blank (radius-cli-<timestamp>)
- Always use name-based lookup, remove address-based fallback
- Fix status hint to include --wallet cdp
- Remove as any from signTypedData (types match directly)

This comment was marked as outdated.

AkasshP and others added 3 commits June 5, 2026 16:04
Implements the Privy server wallet provider using the Privy REST API
(secp256k1_sign, personal_sign, eth_signTypedData_v4). Session stored
in ~/.radius/privy-session.json with 0o600 permissions.
Show balances after provider login/status, make Para archive/restore explicit, enumerate CDP and Privy accounts where possible, and disable provider telemetry defaults.
Share provider session file handling and remote legacy transaction signing between CDP, Para, and Privy without changing provider behavior.
@TJ-Frederick

This comment was marked as resolved.

TJ-Frederick

This comment was marked as resolved.

@AkasshP

This comment was marked as outdated.

- Para login hang: call para.disconnect() after session save
- Strip payment-signature from user-supplied headers on initial request
- Normalize tx hash fields in decodePaymentResponse (txHash, transactionHash, hash)
Exit codes: 0=success, 1=general, 2=usage, 3=config, 4=auth, 5=network, 6=balance, 7=payment.
Agents can switch on exit code instead of parsing error text.
@erikzrekz erikzrekz changed the title Add Para as a WalletProvider (ENG-1976) New Wallet Providers, x402 Improvements, agent ergonomics, and tests Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants