@selat-ai/router-client
v0.2.0
Published
TypeScript client for interacting with SELAT Router.
Readme
@selat-ai/router-client
TypeScript SDK for selat-router that offers a fetch-like API to pay any endpoints simply with Circle Nanopayments.
Install
npm install @selat-ai/router-clientFor Circle Agent Wallet signing, install the Circle CLI in the same app so serverless
runtimes can resolve it without relying on a global circle binary:
npm install @selat-ai/router-client @circle-fin/cliQuick Start
Supported signers include Private Keys, the Circle Agent Wallet, Circle Developer-Controlled Wallets, and custom Remote Signers. The recommended approach is to use the Circle Agent Wallet, ensuring that sensitive private keys remain entirely outside the SDK environment.
Starting with Private Key
import { RouterClient, createViemSigner } from "@selat-ai/router-client";
const signer = createViemSigner(process.env.X402_CLIENT_PRIVATE_KEY as `0x${string}`);
const client = new RouterClient({
chain: "base",
signer
});
const response = await client.fetch("https://upstream.example.com/v1/data", {
method: "GET"
});Using Circle Agent Wallet (RECOMMENDED)
Before using this path, install and set up your Circle Agent Wallet here:
https://developers.circle.com/agent-stack/agent-wallets/quickstart
You should already have:
- Circle CLI installed and authenticated
- A funded agent wallet on the target chain
import { RouterClient, createCircleAgentWalletSigner } from "@selat-ai/router-client";
const signer = createCircleAgentWalletSigner({
address: process.env.SELAT_SIGNER_ADDRESS as `0x${string}`,
chain: "base"
});
const client = new RouterClient({
chain: "base",
signer
});
const response = await client.fetch(
"https://pro-api.coinmarketcap.com/x402/v3/cryptocurrency/quotes/latest?symbol=eth",
{ method: "GET" }
);When @circle-fin/cli is installed locally, the SDK automatically runs the package
entrypoint with the current Node executable. If you prefer a specific CLI binary,
pass cliCommand; relative paths such as node_modules/.bin/circle are resolved
from process.cwd().
For Next.js or Vercel serverless functions that use Circle Agent Wallet signing, include the CLI files in the function trace:
import { CIRCLE_AGENT_WALLET_NEXT_TRACE_INCLUDES } from "@selat-ai/router-client";
/** @type {import("next").NextConfig} */
const nextConfig = {
outputFileTracingIncludes: {
"/api/your-paid-route": CIRCLE_AGENT_WALLET_NEXT_TRACE_INCLUDES
}
};
export default nextConfig;Serverless signing
createCircleAgentWalletSigner shells out to the local circle CLI
(child_process.spawn). That binary — and its authenticated circle login
session — does not exist in a serverless function (Vercel, AWS Lambda, Netlify,
Google Cloud Functions, Cloud Run), where the filesystem is ephemeral and
read-only. Calling it there throws an explicit error rather than failing with an
opaque spawn circle ENOENT.
For serverless deployments, use createHttpRemoteSigner to delegate signing
to a service you host (where the Circle CLI / Agent Wallet credentials live).
Unlike the low-level createRemoteSigner, it bakes in the HTTP transport and
performs Gateway owner-address resolution, so it is a drop-in replacement for the
CLI signer — correct for smart-contract-account wallets, not just EOAs.
import { RouterClient, createHttpRemoteSigner } from "@selat-ai/router-client";
const signer = createHttpRemoteSigner({
address: process.env.SELAT_SIGNER_ADDRESS as `0x${string}`,
endpoint: process.env.SELAT_SIGNER_API_URL as string,
token: process.env.SELAT_SIGNER_API_TOKEN // optional bearer auth
});
const client = new RouterClient({ chain: "base", signer });Your signing service must accept POST { address, typedData } and respond with
{ "signature": "0x..." }, signing typedData with the key of address.
Custom Signer
Use this when signing happens outside the SDK, for example in a wallet service, HSM, or KMS.
import { RouterClient, createRemoteSigner } from "@selat-ai/router-client";
const signer = createRemoteSigner(
"0x1111111111111111111111111111111111111111",
async ({ address, typedData }) => {
// Delegate to your wallet/HSM/KMS signer service.
const response = await fetch("https://signer.example.com/sign-typed-data", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ address, typedData })
});
const data = await response.json() as { signature: `0x${string}` };
return data.signature;
}
);
const client = new RouterClient({
chain: "base",
signer
});Using Circle Developer-Controlled Wallet
Use this path when your application signs through Circle's Developer-Controlled Wallet.
import { RouterClient, createCircleDeveloperControlledWalletSigner } from "@selat-ai/router-client";
const signer = createCircleDeveloperControlledWalletSigner({
address: process.env.SELAT_SIGNER_ADDRESS as `0x${string}`,
apiKey: process.env.SELAT_CIRCLE_API_KEY!,
entitySecret: process.env.SELAT_CIRCLE_ENTITY_SECRET!,
walletId: process.env.SELAT_CIRCLE_WALLET_ID
});
const client = new RouterClient({
chain: "base",
signer
});Refunds
Refund requests and status queries automatically send an
X-SELAT-DUMMY-EIP712-SIGNATURE header. It contains a base64url-encoded JSON
payload with a zero-value Gateway owner-probe EIP-712
TransferWithAuthorization typed data object and its signature.
The owner probe uses the GatewayWalletBatched domain, a zero-value transfer to
the zero address, and the Gateway wallet as its verifying contract. This matches
the typed data used to resolve the owner for Circle Gateway payments, so services
recover the same signing owner. It is not a payment authorization. Services
should use it only to recover the signing owner and validate the refund request.
import { RouterClient, createViemSigner } from "@selat-ai/router-client";
const signer = createViemSigner(process.env.X402_CLIENT_PRIVATE_KEY as `0x${string}`);
const client = new RouterClient({
chain: "base",
signer
});
const refund = await client.refundClaim("selatx<quote-id>");
console.log(refund);
const status = await client.refundQuery("selatx<quote-id>");
console.log(status);For Circle Agent Wallet signing, replace the signer with:
const signer = createCircleAgentWalletSigner({
address: process.env.SELAT_SIGNER_ADDRESS as `0x${string}`,
chain: "base"
});Run the Example
You can run the included example script to call a real upstream endpoint through router.
- Copy
.env.exampleto.envand fill values. - Run:
With Private key:
export SELAT_CHAIN=base
export SELAT_TARGET_URL="https://pro-api.coinmarketcap.com/x402/v3/cryptocurrency/quotes/latest?symbol=eth"
export X402_CLIENT_PRIVATE_KEY=0x<your-private-key>
pnpm run example-payUsing Remote signer:
export SELAT_SIGNER_MODE=remote
export SELAT_CHAIN=base
export SELAT_TARGET_URL="https://pro-api.coinmarketcap.com/x402/v3/cryptocurrency/quotes/latest?symbol=eth"
export SELAT_SIGNER_ADDRESS=0x<your-signer-address>
export SELAT_SIGNER_API_URL="https://signer.example.com/sign-typed-data"
pnpm run example-payUsing Circle Agent Wallet:
export SELAT_SIGNER_MODE=circle-agent-wallet
export SELAT_CHAIN=base
export SELAT_TARGET_URL="https://pro-api.coinmarketcap.com/x402/v3/cryptocurrency/quotes/latest?symbol=eth"
export SELAT_SIGNER_ADDRESS=0x<your-agent-wallet-address>
pnpm run example-payUsing Circle Developer-Controlled Wallet:
export SELAT_SIGNER_MODE=circle-dev-controlled-wallet
export SELAT_CHAIN=base
export SELAT_TARGET_URL="https://pro-api.coinmarketcap.com/x402/v3/cryptocurrency/quotes/latest?symbol=eth"
export SELAT_SIGNER_ADDRESS=0x<your-wallet-address>
export SELAT_CIRCLE_API_KEY=<your-circle-api-key>
export SELAT_CIRCLE_ENTITY_SECRET=<your-entity-secret>
export SELAT_CIRCLE_WALLET_ID=<your-wallet-id>
pnpm run example-payRefund Example
Copy .env.example to .env, then set the refund quote and action:
export SELAT_CHAIN=base
export SELAT_QUOTE_ID=selatx<quote-id>
export SELAT_REFUND_ACTION=claim
export SELAT_SIGNER_MODE=private-key
export X402_CLIENT_PRIVATE_KEY=0x<your-private-key>
pnpm run example-refundTo query the refund status, set SELAT_REFUND_ACTION=query. To use Circle
Agent Wallet instead, set SELAT_SIGNER_MODE=circle-agent-wallet and provide
SELAT_SIGNER_ADDRESS.
