Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ name = "qn"
path = "src/lib.rs"

[dependencies]
quicknode-sdk = { version = "0.8.2", features = [
quicknode-sdk = { git = "https://github.com/quicknode/sdk", rev = "0bd4f3cf13b32d75119ffe7b501929dcffa78bfd", features = [
"payments", # x402/EVM
"payments-svm", # + x402/Solana
"payments-tempo", # + MPP/Tempo
Expand Down
2 changes: 1 addition & 1 deletion IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@
transaction responses from `qn rpc x402 drip`.
**Success criteria**: Arc network and USDC resolution work, both response shapes
render correctly, and the CLI passes all verification checks against the local SDK.
**Status**: In Progress
**Status**: Complete
49 changes: 43 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,22 +262,59 @@ qn kv list get allowlist

### SQL

Discovery is always public. `qn sql clusters` and `qn sql schema` need no API
key, wallet, or flag.

```sh
qn sql clusters
qn sql schema hyperliquid-core-mainnet
```

`qn sql query` chooses who pays. An API key with no payment flag uses account
credits. `--x402-drawdown` and `--mpp-session` ignore an API key if one is also
present. Config never turns a payment path on.

```sh
# Run a query inline, from a file, or from stdin (--file -)
# Account credits (API key required)
qn sql query "SELECT action_type, user FROM hyperliquid_system_actions ORDER BY block_time DESC LIMIT 3" --cluster-id hyperliquid-core-mainnet
qn sql query --file query.sql --cluster-id hyperliquid-core-mainnet
cat query.sql | qn sql query --file - --cluster-id hyperliquid-core-mainnet

# Pipe rows into jq (stats print to stderr, so stdout stays clean)
qn sql query "SELECT 1" --cluster-id hyperliquid-core-mainnet -o json | jq '.data'

# Inspect a cluster's tables, columns, and types
qn sql schema hyperliquid-core-mainnet
```

Queries are read-only (SELECT) and capped at 1000 rows per request; page through
larger result sets with `LIMIT`/`OFFSET` in the SQL.

Get started without an account (x402 drawdown):

```sh
qn wallet generate --vm evm --name payer
qn micropayments x402 drip --payment-wallet payer --payment-network base-sepolia
qn micropayments x402 buy-credits --network base-sepolia --yes \
--payment-wallet payer --payment-network base-sepolia \
--payment-asset USDC --max-amount 10000000
qn sql query "SELECT * FROM hyperliquid_trades LIMIT 10" \
--cluster-id hyperliquid-core-mainnet \
--x402-drawdown --payment-wallet payer --payment-network base-sepolia
```

Get started without an account (MPP session). Fund pathUSD on Tempo testnet
first.

```sh
qn wallet generate --vm evm --name payer
qn micropayments mpp open --deposit 1000000 --yes \
--payment-wallet payer --payment-network tempo-testnet \
--payment-asset pathUSD --max-amount 1000000
qn sql query "SELECT * FROM hyperliquid_trades LIMIT 10" \
--cluster-id hyperliquid-core-mainnet \
--mpp-session --payment-wallet payer \
--payment-network tempo-testnet --payment-asset pathUSD --max-amount 1000000
```

`qn pay` is an alias for `qn micropayments`. `qn rpc x402` and `qn rpc mpp`
call the same funding runners.

### On-chain RPC

Make JSON-RPC calls with no endpoint to provision. `qn rpc call` mints and
Expand Down
7 changes: 6 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ pub enum Command {
/// Run SQL queries and inspect cluster schemas.
Sql(commands::sql::Args),

/// Manage crypto micropayments (x402 credits and MPP channels).
#[command(visible_alias = "pay")]
Micropayments(commands::micropayments::Args),

/// Make RPC calls.
Rpc(commands::rpc::Args),

Expand Down Expand Up @@ -255,7 +259,8 @@ impl Cli {
Command::Stream(args) => commands::stream::run(args, Ctx::from_global(global)?).await,
Command::Webhook(args) => commands::webhook::run(args, Ctx::from_global(global)?).await,
Command::Kv(args) => commands::kv::run(args, Ctx::from_global(global)?).await,
Command::Sql(args) => commands::sql::run(args, Ctx::from_global(global)?).await,
Command::Sql(args) => commands::sql::run(args, global).await,
Command::Micropayments(args) => commands::micropayments::run(args, global).await,
// RPC resolves its own context for token-cache seeding.
Command::Rpc(args) => commands::rpc::run(args, global).await,
// Wallet management is local and keyless.
Expand Down
59 changes: 52 additions & 7 deletions src/commands/agent/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ Resolution order for the API key:
1. `--api-key <KEY>` flag (highest precedence).
2. Config file: `[api] key = "..."` in `~/.config/qn/config.toml` (or the path
passed to `--config-file`).
3. If neither resolves, the command exits **4** (`no API key found`).
3. If neither resolves, the command exits **4** (`no API key found`). The one
exception is `sql query`, where a key is only one of three ways to pay: with
no key and no `--x402-drawdown`/`--mpp-session` it exits **1** and names every
option.

There is **no environment-variable fallback** by design — a key left exported in
a shell is invisible state that outlives the session.
Expand Down Expand Up @@ -69,7 +72,8 @@ gateway 5xx after the paid resend, a lost response, or an uninterpretable
post-payment response). On exit 3, check the wallet before re-running; never
blind-retry a paid call. A **drawdown** call (`--x402-drawdown`) spends prepaid
credits, not per-call funds: running out surfaces an actionable exit-2 error
pointing at `qn rpc x402 buy-credits`, and a credit is drawn only on success.
pointing at `qn micropayments x402 buy-credits` (alias `qn rpc x402 buy-credits`),
and a credit is drawn only on success. A paid SQL query uses the same 2/3 split.

## 4. Non-interactive & confirmation behavior

Expand All @@ -90,6 +94,8 @@ Gated command classes:
- `endpoint rate-limit delete-override`
- `stream delete`, `webhook delete`, `team delete`
- `kv set delete`, `kv list delete`
- `micropayments x402 buy-credits` and `rpc x402 buy-credits`
- `micropayments mpp open`/`top-up`/`close` and `rpc mpp open`/`top-up`/`close`

There is **no account-wide wipe command** — that is intentional; use the API directly
if you need it.
Expand All @@ -105,14 +111,16 @@ if you need it.
- `qn stream test-filter` evaluates a filter against historical data and changes
nothing — it is read-only and safe to retry.
- `qn sql query` is read-only but **does not auto-retry**: a query consumes credits,
so a retried query re-bills. `qn sql schema` is a cheap read and retries normally.
- A **paid** `rpc call` (`--x402`/`--mpp`/`--x402-drawdown`/`--mpp-session`)
so a retried query re-bills. `qn sql clusters` and `qn sql schema` are cheap
public-catalog reads and retry normally.
- A **paid** `rpc call` or `sql query` (`--x402`/`--mpp`/`--x402-drawdown`/`--mpp-session`)
never auto-retries — `--retries` does not apply. A per-request attempt can
move funds, and after a lost response the previous attempt may already have
settled (§3, exit 3). A drawdown call draws 1 credit per success and is
settled (§3, exit 3). A drawdown call draws prepaid credits on success and is
single-attempt (the one exception is a transparent re-auth when the session
token expired, which draws nothing). A session call signs one cumulative
voucher and is single-attempt.
voucher and is single-attempt. `sql query` has no per-request `--x402`/`--mpp`
flag; its paid lanes are `--x402-drawdown` and `--mpp-session`.

## 6. Command catalog

Expand All @@ -133,7 +141,16 @@ Top-level nouns (plurals like `endpoints`/`streams` and `ls` are accepted aliase
enabled-count
- `kv` — `set` (put, get, list, delete, bulk) and `list` (list, get, create, append,
contains, remove-item, update, delete)
- `sql` — query (inline SQL, `--file <path>`, or `--file -` for stdin), schema
- `sql` — `clusters` (alias `ls`) and `schema` always hit the public catalog
(no API key, wallet, or flag). `query` (inline SQL, `--file <path>`, or
`--file -` for stdin) chooses who pays: an API key with no payment flag uses
the account host; `--x402-drawdown` uses prepaid x402 credits; `--mpp-session`
uses an open MPP channel. Config never turns a payment path on. Neither a
key nor a payment flag is an error that names both next steps.
- `micropayments` (alias `pay`) — shared funding noun. `x402` (buy-credits,
balance, drip, supported-networks, supported-payments) and `mpp` (open,
top-up, close, status, supported-networks, supported-payments). `qn rpc x402`
and `qn rpc mpp` stay first-class and call the same runners.
- `tooling-access` — status, enable, disable (provisions the endpoint `rpc` uses)
- `rpc` — make JSON-RPC calls. `qn rpc call <method> [json-params]` calls the
account's Tooling Access endpoint (params is a JSON array or object inline, or
Expand Down Expand Up @@ -269,6 +286,34 @@ qn kv set get my-key
qn kv set list
```

**SQL without an account (x402 drawdown):**

```sh
qn wallet generate --vm evm --name payer
qn micropayments x402 drip --payment-wallet payer --payment-network base-sepolia
qn micropayments x402 buy-credits --network base-sepolia --yes \
--payment-wallet payer --payment-network base-sepolia \
--payment-asset USDC --max-amount 10000000
qn sql query "SELECT * FROM hyperliquid_trades LIMIT 10" \
--cluster-id hyperliquid-core-mainnet \
--x402-drawdown --payment-wallet payer --payment-network base-sepolia
```

**SQL without an account (MPP session).** Fund pathUSD on Tempo testnet first.

```sh
qn wallet generate --vm evm --name payer
qn micropayments mpp open --deposit 1000000 --yes \
--payment-wallet payer --payment-network tempo-testnet \
--payment-asset pathUSD --max-amount 1000000
qn sql query "SELECT * FROM hyperliquid_trades LIMIT 10" \
--cluster-id hyperliquid-core-mainnet \
--mpp-session --payment-wallet payer \
--payment-network tempo-testnet --payment-asset pathUSD --max-amount 1000000
```

`qn sql clusters` and `qn sql schema hyperliquid-core-mainnet` need no key.

**Make on-chain calls (no endpoint to provision):**

```sh
Expand Down
46 changes: 46 additions & 0 deletions src/commands/micropayments/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! Shared funding noun for x402 credits and MPP channels.
//!
//! `qn rpc x402` and `qn rpc mpp` stay first-class and call the same runners.

pub mod mpp;
pub mod x402;

use clap::{Args as ClapArgs, Subcommand};

use crate::context::GlobalArgs;
use crate::errors::CliError;

#[derive(Debug, ClapArgs)]
#[command(subcommand_required = true, arg_required_else_help = true)]
#[command(after_help = "Examples:\n \
qn micropayments x402 drip --payment-wallet payer --payment-network base-sepolia\n \
qn micropayments x402 buy-credits --network base-sepolia --yes \\\n \
--payment-wallet payer --payment-network base-sepolia \\\n \
--payment-asset USDC --max-amount 10000000\n \
qn micropayments mpp open --deposit 1000000 --yes \\\n \
--payment-wallet payer --payment-network tempo-testnet \\\n \
--payment-asset pathUSD --max-amount 1000000\n\n\
`qn pay` is an alias. `qn rpc x402` and `qn rpc mpp` call the same runners.")]
pub struct Args {
#[command(subcommand)]
pub cmd: MicropaymentsCmd,
}

#[derive(Debug, Subcommand)]
pub enum MicropaymentsCmd {
/// Manage x402 credit drawdown: buy prepaid credits, check the balance, or
/// drip testnet funds. Pair with `qn sql query --x402-drawdown` or
/// `qn rpc call --x402-drawdown`.
X402(x402::Args),

/// Manage an MPP payment channel: open, top-up, close, or check status.
/// Pair with `qn sql query --mpp-session` or `qn rpc call --mpp-session`.
Mpp(mpp::Args),
}

pub async fn run(args: Args, global: GlobalArgs) -> Result<(), CliError> {
match args.cmd {
MicropaymentsCmd::X402(a) => x402::run(a, global).await,
MicropaymentsCmd::Mpp(a) => mpp::run(a, global).await,
}
}
4 changes: 4 additions & 0 deletions src/commands/micropayments/mpp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! Re-export of the MPP lifecycle so `qn micropayments mpp` and
//! `qn rpc mpp` share one runner.

pub use crate::commands::rpc::mpp::{run, Args};
4 changes: 4 additions & 0 deletions src/commands/micropayments/x402.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! Re-export of the x402 lifecycle so `qn micropayments x402` and
//! `qn rpc x402` share one runner.

pub use crate::commands::rpc::x402::{run, Args};
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod chain;
pub mod endpoint;
pub mod kv;
pub mod metrics;
pub mod micropayments;
pub mod rpc;
pub mod sql;
pub mod stream;
Expand Down
6 changes: 3 additions & 3 deletions src/commands/rpc/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
//! RPC call and network-list commands. Default, custom-URL, and paid calls use
//! separate lanes so their auth, cache, and retry behavior cannot mix.

mod mpp;
pub(crate) mod mpp;
mod pay_asset;
mod pay_network;
mod payment;
pub(crate) mod payment;
mod supported_networks;
mod x402;
pub(crate) mod x402;

use std::io::Read;
use std::path::{Path, PathBuf};
Expand Down
8 changes: 4 additions & 4 deletions src/commands/rpc/mpp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,28 +215,28 @@ fn setup(args: &PaymentArgs, global: GlobalArgs) -> Result<(Ctx, PayScope), CliE
}

// Pay scope carried to channel-cache keying; the payer address is added later.
pub(super) struct PayScope {
pub(crate) struct PayScope {
pay_network: String,
pay_asset: String,
}

impl PayScope {
pub(super) fn from_config(payment: &quicknode_sdk::PaymentConfig) -> Self {
pub(crate) fn from_config(payment: &quicknode_sdk::PaymentConfig) -> Self {
PayScope {
pay_network: payment.pay_network.clone(),
pay_asset: payment.asset.clone(),
}
}

pub(super) fn with_address(&self, address: String) -> config::ChannelScope {
pub(crate) fn with_address(&self, address: String) -> config::ChannelScope {
config::ChannelScope {
address,
pay_network: self.pay_network.clone(),
pay_asset: self.pay_asset.clone(),
}
}

pub(super) fn describe(&self) -> String {
pub(crate) fn describe(&self) -> String {
format!("{} on {}", self.pay_asset, self.pay_network)
}
}
Expand Down
17 changes: 10 additions & 7 deletions src/commands/rpc/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ pub(super) async fn run_drawdown_call(args: CallArgs, global: GlobalArgs) -> Res
}

/// Load a fresh cached gateway session or authenticate one.
pub(super) async fn ensure_gateway_session(
pub(crate) async fn ensure_gateway_session(
ctx: &Ctx,
global: &GlobalArgs,
) -> Result<GatewaySession, CliError> {
Expand All @@ -163,7 +163,10 @@ pub(super) async fn ensure_gateway_session(
}

/// Authenticate and replace the cached session.
async fn reauthenticate(ctx: &Ctx, global: &GlobalArgs) -> Result<GatewaySession, CliError> {
pub(crate) async fn reauthenticate(
ctx: &Ctx,
global: &GlobalArgs,
) -> Result<GatewaySession, CliError> {
let sessions_path = config::sessions_cache_path(global.resolve_config_path().as_deref());
let address = ctx.sdk.rpc.payment_address()?;
let session = ctx.sdk.rpc.gateway_authenticate().await?;
Expand All @@ -174,7 +177,7 @@ async fn reauthenticate(ctx: &Ctx, global: &GlobalArgs) -> Result<GatewaySession
}

/// Check for an expired gateway session.
fn is_token_expired(e: &SdkError) -> bool {
pub(crate) fn is_token_expired(e: &SdkError) -> bool {
matches!(
e,
SdkError::Api { status, body }
Expand Down Expand Up @@ -328,7 +331,7 @@ fn load_payment_section(global: &GlobalArgs) -> Result<PaymentSection, CliError>
}

/// Payment parameters shared by paid call and lifecycle commands.
pub(super) struct PaymentParams<'a> {
pub(crate) struct PaymentParams<'a> {
pub key_file: Option<&'a Path>,
pub wallet: Option<&'a str>,
pub max_amount: Option<&'a str>,
Expand All @@ -338,7 +341,7 @@ pub(super) struct PaymentParams<'a> {
}

/// Parameters needed by keyless session commands.
pub(super) struct SessionParams<'a> {
pub(crate) struct SessionParams<'a> {
pub key_file: Option<&'a Path>,
pub wallet: Option<&'a str>,
pub payment_network: Option<&'a str>,
Expand Down Expand Up @@ -439,7 +442,7 @@ fn resolve_drawdown_config(
}

// Resolve the wallet and pay network for balance/drip.
pub(super) fn resolve_session_params(
pub(crate) fn resolve_session_params(
params: &SessionParams<'_>,
section: &PaymentSection,
wallets_dir: Option<&Path>,
Expand Down Expand Up @@ -499,7 +502,7 @@ pub(super) fn resolve_session_params(
}

/// Resolve payment parameters before any network I/O.
pub(super) fn resolve_payment_params(
pub(crate) fn resolve_payment_params(
scheme: &str,
params: &PaymentParams<'_>,
section: &PaymentSection,
Expand Down
Loading
Loading