@paybox-sh/sdk
v2.8.0
Published
Typed Node SDK + `paybox` CLI for the PayBox agent API, with REST core operations and runtime MCP plugin discovery and invocation.
Readme
@paybox-sh/sdk
Typed Node SDK and paybox CLI for the PayBox agent API. It mirrors the MCP tool
surface — list_credentials, request_payment, claim_payment_credentials,
request_wallet_sign, request_secret, request_swap, request_transfer,
discover_services,
pay_x402, use_service, get_portfolio, get_buy_link, get_deposit_link,
request_account_change, get_request, and the generic plugin discovery,
contract, and invocation flow. Core operations use the REST API; plugins use
the same MCP tools as connector hosts, with no hardcoded per-plugin paths.
Wallet signing is non-custodial and runs in-process: the SDK reuses the exact
signing core from the wallet-sign iframe (@paybox/mcp-app), builds the chain
digest, and ed25519-signs the MoonX envelope with your pbxk1. key. The MoonX
secret never reaches the client — the secret-gated /sign happens server-side.
Install
pnpm add @paybox-sh/sdk # library
npx @paybox-sh/sdk login # or use the CLI directlyDriving this CLI from an agent? SKILL.md ships in the package and
routes wallet tasks through existing access or the required setup steps. It
preserves an explicit wallet choice and resumes the requested task after setup.
Use the CLI on Muse, Grok Bot, Grok Computer, and any agent on a
computer with no connector screen — not the grok.com MCP connector path.
Install it where the agent runs, including a hosted VM or sandbox.
CLI
# Setup is two commands. `login` authenticates; `connect` pairs a signing key.
# Pairing prints a two-digit number and a link; you open the link and pick that
# same number in PayBox. The key is generated locally and never leaves this
# machine, so there is nothing to copy. Neither command needs a browser on the
# machine running the CLI — approve both from a phone if you like. Add --no-open
# to either command to print the link without launching a browser.
paybox login # device-code OAuth (browser + passkey)
paybox login --key pbx_live_… # personal API key instead (no OAuth)
paybox connect # pair a signing key
PAYBOX_SIGNING_KEY=pbxk1.… paybox … # headless: reuse an already-paired key
paybox credentials # list usable credentials
# `--credential <id>` below is the `credential.id` from that list, not the wallet name.
paybox pay --credential <id> --merchant Acme --url https://acme.com --amount 1999
paybox secret --credential <id> --purpose "deploy" # one-time token
paybox secret --credential <id> --raw # raw value (grant-gated)
paybox sign --credential <id> --intent '{"op":"message","message":"gm"}'
paybox swap --credential <id> --src-chain eip155:8453 \
--src-token native --dst-token 0x… --amount 1000000000000000
paybox transfer --credential <id> --chain eip155:8453 \
--to 0x… --token 0x… --amount 1000000 # omit --token for ETH or SOL
paybox portfolio --address 0x… --networks 1,8453
paybox discover "weather api" # browse curated paid x402 services
paybox discover --raw "weather api" # debug: uncurated compact summaries
paybox pay-x402 --credential <id> --url https://svc/api --accepts @accepts.json
paybox use-service --credential <id> --url https://svc/api # paybox pays + fetches
paybox claim <request_id> # claim an approved payment's card (one-time)
paybox buy-link --credential <id> --amount-usd 50 # defaults EVM funding to Base
paybox buy-link --credential <id> --destination eip155:1 --currency eth
paybox account-change --note "need a wallet grant" # manage_url for the user to open
paybox account-change --credential <id> --mode always_approve # user confirms the new approval mode
paybox request <request_id> --wait # poll a pending request
paybox request <request_id> --complete-signature # sign that parked wallet-sign request locally
# Plugins (enable them in the paybox app first)
paybox plugins list --status enabled
paybox plugins contract paybox://plugins/world
paybox plugins use world world_get_events --input '{"status":"active"}'
paybox version # installed version (+ update check)
paybox update # update the global install to latest
paybox uninstall # clear stored config (prints npm rm)
paybox --json credentials # machine-readable output on any commandversion/update/uninstall are conveniences over npm: update runs
npm i -g @paybox-sh/sdk@latest against the same registry version checks
(use --print to just show it); uninstall
clears ~/.config/paybox and prints the npm rm -g line (a package can't remove
its own binary). For project installs use pnpm up/pnpm rm, or npx
@paybox-sh/sdk@latest to always run the latest without installing.
A global install also updates itself: at most once a day, before a command, the
CLI checks npm. When a newer version in the same major exists, it installs it,
prints one line to stderr, and runs the same command again with the new version.
Stdout does not change. Project installs, npx, and new major versions get a
stderr notice instead. Set PAYBOX_NO_AUTO_UPDATE=1 to turn it off.
sign, swap, transfer, pay-x402, and use-service can complete in-process with a
configured pbxk1. key. Plugin calls return the raw MCP tool result.
paybox plugins use declares the browser signing surface, so its output has
no in-chat signing-window fields. A parked plugin trade finishes from the CLI
through the completion_url returned when the wallet's access is "Complete
transactions in PayBox". usePlugin(args, "browser") declares the same surface
from the library.
An MCP host can declare its signing surface on each HTTP request with
X-Paybox-Signing-Surface: in-process, browser, or none. Omit the header to
keep the existing MCP Apps behavior. The value selects one advisory
next_action; it does not grant signing access. A host must use in-process
only when it has an eligible local callback for that operation. For a parked
wallet-sign request, call paybox.completeWalletSignRequest(requestId) from
that callback. The SDK reads the stored intent from PayBox and signs the same
request. It does not create another request. Do not put the signing key in an
MCP tool argument or model context. pending_approval still needs a person;
denied and error remain terminal request states.
For a Solana swap or transfer the SDK re-pins a fresh recent blockhash immediately before
signing (parity with the signing window — the quote-time blockhash expires
within ~60–90s, often before signing), so a swap that dwells for approval no
longer broadcasts as BlockhashNotFound. An EVM swap that needs a new token
approval signs the approval, waits for it to confirm, and then signs the swap
step of the same request.
pay-x402 is header mode — on success output.value.x_payment is the X-PAYMENT
header to replay on the original request. use-service is gateway mode — PayBox
makes the paid call for you and returns the resource's reply on
output.value.response. Pass --accepts from the resource's own 402.
A paybox discover result with accepts: [] has no catalog price: the provider
sets it in the live 402, or the route is free. Read it with paybox plugins
contract <contract_uri> and call it with use-service and that contract's
--method and --body; PayBox reads the live 402 and pays within the grant. If
use-service reports HTTP 200 instead of 402, the route is free: call it
directly with the same method and body.
Library
Autonomous budget payments return success with output.value.redacted: true.
Keep request_id, then call claimPaymentCredentials(request_id) to issue and
receive the card once. If claim delivery is uncertain, do not create a replacement
payment: the budget may already have been charged and the card cannot be recovered.
The merchant is paid only when you use the card at checkout.
import { PayboxClient } from "@paybox-sh/sdk";
const paybox = PayboxClient.fromConfig(); // reads ~/.config/paybox + env
// or: new PayboxClient({ apiKey: "pbx_live_…", signingKey: "pbxk1.…" });
const { credentials, ungranted } = await paybox.listCredentials();
const pay = await paybox.requestPayment({
credentialId: credentials[0].credential.id,
merchant: "Acme",
merchantUrl: "https://acme.com",
amountCents: 1999,
});
// Ungranted credentials the user owns but hasn't granted this connector — pass
// an id to request_account_change's `add` to ask for access.
console.log(ungranted.map((c) => `${c.name} (${c.kind})`));
const sig = await paybox.requestWalletSign({
credentialId: walletId,
chain: "eip155:8453",
intent: { op: "message", message: "gm" },
}); // signs in-process if a key is set
// x402: discover a paid endpoint, then pay it — header mode (you replay the
// X-PAYMENT header) or gateway mode (paybox fetches the paid content for you).
const services = await paybox.discoverServices("weather api");
const rawServices = await paybox.discoverServices("weather api", { raw: true }); // debug only
const paid = await paybox.useService({ credentialId: walletId, url: services[0].resource });
// paid.response.output.value.response holds the resource's reply
const plugins = await paybox.discoverPlugins({ status: "enabled" });
const selected = plugins.plugins.find((plugin) => plugin.id === "aave");
const contract = await paybox.getPluginContract(selected!.contract_uri!);
const result = await paybox.usePlugin({
pluginId: contract.contract.id,
toolId: contract.contract.tools[0].id,
input: {},
});Run and resume a plugin request
runPlugin invokes a plugin once and drives its stored request. resumeRequest
reads an existing request and does not invoke the plugin again. Signing follows
validated Solana/EVM transaction capabilities and server-owned refresh_before_sign,
not plugin names. resumeRequest also consumes stored wallet_sign transaction
intents through the existing signer. Unknown formats, versioned plans and gasless
prepared plans stop with outcome: "unsupported". Public plugins also use
use_plugin, with their installation ID as plugin_id; request completion does
not by itself prove a purchase or broadcast. Use the plugin contract to select an
enabled tool and construct its input.
import type { UsePluginArgs } from "@paybox-sh/sdk";
async function runApprovedOperation(operation: UsePluginArgs) {
const execution = await paybox.runPlugin(operation, { timeoutMs: 60_000 });
if (execution.requestId) {
await saveRequestId(execution.requestId);
}
return execution;
}
const execution = await paybox.resumeRequest(savedRequestId, {
waitForApproval: true,
timeoutMs: 60_000,
});The example assumes application-owned storage through saveRequestId and
savedRequestId. Keep the request ID so another process can resume after a pause.
A paired signing key enables automatic signing. Set autoSign: false to stop
before signing. The server still enforces wallet grants and approval requirements.
| Outcome | Meaning |
| --- | --- |
| terminal | Check response.status: success, denied, or error. A signature alone is not success. |
| approval_required | The user must approve through Paybox. Approval waiting is disabled by default. |
| signing_required | Automatic signing is disabled or a signing key is unavailable. |
| unsupported | The result or signing plan is outside the supported contract. |
| reconcile_required | The client cannot confirm completion. Query the same request before further action. |
| deadline_exceeded / cancelled | The client stopped. The server operation can still finish. |
The default deadline is 300 seconds and includes network calls. Polling starts at
2 seconds, doubles when the status is unchanged, and stops increasing at 30 seconds.
A status change resets the interval. intervalMs sets the starting interval from
1 to 30,000 milliseconds. signal accepts an AbortSignal for cancellation.
Do not start concurrent drivers for the same request. After an uncertain result,
use getRequest before an explicit resume. The driver does not automatically retry
a signature submission or create a replacement request. If invocation stops before
a request ID arrives, reconcile outside this driver before invoking the plugin again.
usePlugin still returns the raw MCP result, including metadata. Use it for data
tools and unsupported flows. The new methods do not change existing CLI commands.
Funding requests stop with outcome: "unsupported", reason: "funding_required",
and a non-signing pending_funding response. They never reopen or submit a plan.
Configuration
Resolution order: explicit constructor options → environment → stored config.
| Env | Meaning |
|-----|---------|
| PAYBOX_API_URL | API origin (default https://api.paybox.sh). |
| PAYBOX_API_KEY | A pbx_live_* personal API key. |
| PAYBOX_ACCESS_TOKEN | A raw OAuth access token. |
| PAYBOX_SIGNING_KEY | A pbxk1. wallet signing key (enables sign/swap). |
| PAYBOX_CONFIG_DIR | Override the config dir (default ~/.config/paybox). |
| PAYBOX_NO_AUTO_UPDATE | 1 turns off the daily self-update of a global install. |
| PAYBOX_UPDATE_REGISTRY | Registry the self-update checks and installs from (default https://registry.npmjs.org). Project .npmrc and npm_config_registry are ignored. |
paybox login writes ~/.config/paybox/config.json (mode 0600 on
macOS/Linux). On Windows the POSIX mode bits don't restrict access — the file
is not access-protected there, so prefer PAYBOX_API_KEY / PAYBOX_SIGNING_KEY
env vars on shared Windows machines.
Headless and CI
Pairing needs a human to confirm the code, so CI cannot pair at run time. Pair once ahead of time as the identity CI will use, then carry both secrets:
# On a laptop. The pbx_live_ key comes from the PayBox app (Agents → create an
# agent, pick its wallets); it is shown once.
paybox login --key pbx_live_… # authenticate as the client CI will use
paybox connect # pick the number; approve on any device
grep signingKey ~/.config/paybox/config.jsonenv:
PAYBOX_API_KEY: ${{ secrets.PAYBOX_API_KEY }} # pbx_live_…
PAYBOX_SIGNING_KEY: ${{ secrets.PAYBOX_SIGNING_KEY }} # pbxk1.…PayboxClient.fromConfig() reads both from the environment, so the runner needs
no config file, no browser, and no TTY.
Those two secrets are a matched pair: a signer is bound to the client that
paired it. Present a pbxk1. to a different client — including the throwaway
client that a plain paybox login registers — and signing fails with agent
signer belongs to another client. It reads like a revocation but means you
paired under a different identity. Rotating the API key mints a new client, so
re-pair after rotation — paybox login drops the stored key when it does this
and tells you to run paybox connect, rather than leaving one that cannot
sign.
Develop
pnpm-lock.yaml also contains the ../mcp-app importer because this SDK
consumes the iframe's signing core through file:../mcp-app. The iframe build
shares this dependency resolution but keeps its installed modules separate.
pnpm install
pnpm typecheck
pnpm build # tsup → dist/ (ESM + types + paybox bin)
# Refresh the generated tool types from the MCP tool contract:
pnpm genThe argument types in src/types.generated.ts come from
artifacts/contracts/mcp-tools-v1.json, the committed list of core MCP tools and
their input schemas. CI fails when the tools change and the file does not, or
when this file is not regenerated from it. Refresh it with
PAYBOX_UPDATE_CONTRACTS=1 cargo test --lib mcp_tool_contract_matches_core_tools
in server/. Response types in src/types.ts track server/src/models.rs.
