@linkexai/agent-sdk
v0.8.2
Published
Linkexai agent SDK — automatic USDC topup for AI agents
Maintainers
Readme
@linkexai/agent-sdk
Automatic USDC top-up for AI agents. The SDK is a small TypeScript library you import into your own agent program — it keeps your Linkexai account funded by paying x402 invoices from an agent wallet, then your normal OpenAI/Anthropic client spends that balance through the Linkexai gateway.
It is not a CLI or a daemon: nothing happens just by installing it. You call its methods from your code (or wire the auto-top-up wrapper once and forget it).
Install
npm install @linkexai/agent-sdk viem openaiConcepts
- Owner wallet — your main wallet (holds USDC on Base). Used once to derive an agent key.
- Agent key — a dedicated wallet you generate yourself and associate with the agent. It signs USDC payments. We only ever store its public address — never the key.
- API key (
sk-lxAgent-…) — identifies the account to the gateway; both the SDK and your model client use it.
Step 1 — Create the agent identity (once)
Bring your own wallet: generate a dedicated agent wallet yourself, then register it
by proving you control it. The easiest path is the Developer Portal web UI — paste
or connect the wallet, sign once, and it's associated with your account. To do it from
code instead, sign an EIP-712 AgentOwnership message with the agent key over a
single-use nonce and post it:
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"
import { encryptAgentKey } from "@linkexai/agent-sdk"
// A wallet you generated yourself (here, a fresh one). Fund it with a small amount
// of USDC on Base — that balance is the most that can ever be at risk.
const privateKey = generatePrivateKey()
const agent = privateKeyToAccount(privateKey)
// 1. Get a single-use nonce bound to the agent address (requires your account session).
const nonceRes = await fetch("https://api.linkexai.com/api/user/login/siwe/nonce", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ wallet_addr: agent.address.toLowerCase() }),
})
const { data: { nonce } } = await nonceRes.json()
// 2. Sign the ownership proof with the AGENT key (proves you control the wallet).
const issuedAt = Math.floor(Date.now() / 1000)
const ownershipSig = await agent.signTypedData({
domain: { name: "linkexai-agent-ownership", version: "1", chainId: 8453, verifyingContract: "0x0000000000000000000000000000000000000000" },
types: { AgentOwnership: [{ name: "agent", type: "address" }, { name: "nonce", type: "string" }, { name: "issuedAt", type: "uint256" }] },
primaryType: "AgentOwnership",
message: { agent: agent.address, nonce, issuedAt: BigInt(issuedAt) },
})
// 3. Register server-side → returns the one-time API key. (Needs your account session.)
const res = await fetch("https://api.linkexai.com/api/user/self/agents", {
method: "POST",
headers: { "Content-Type": "application/json" /* + account session auth */ },
body: JSON.stringify({
label: "My OpenAI agent",
agent_addr: agent.address,
ownership_sig: ownershipSig,
nonce,
issued_at: issuedAt,
}),
})
const { data } = await res.json()
console.log("API KEY (shown once):", data.api_key)
// Optional: store your agent key encrypted.
const backup = await encryptAgentKey(privateKey, process.env.BACKUP_PASSWORD!)Store the API key as a secret and keep your agent private key safe — we never receive or store it.
No-code: run the auto-funder (Docker)
Don't want to write code? Run the standalone funder — a headless daemon that watches your agent's balance and tops it up when low. It runs in your own environment; the agent private key stays in the container and is never sent to Linkexai. It's fully decoupled from your agent (any framework, any language, local or hosted) — your agent just keeps calling with its API key.
# Pull the published image (or build locally: docker build -t linkex/funder sdk/typescript)
docker pull linkex/funder
# THRESHOLD_USD: top up when balance falls below this (USD)
# TOPUP_USD: amount added each top-up (USD)
# POLL_SECONDS: balance-check interval (min 5)
docker run --rm \
-e LINKEXAI_API_KEY=sk-… \
-e AGENT_PRIVATE_KEY=0x… \
-e THRESHOLD_USD=1 \
-e TOPUP_USD=5 \
-e POLL_SECONDS=60 \
linkex/funderKeep the
#notes on their own lines: a comment after a trailing\(e.g.-e AGENT_PRIVATE_KEY=0x… # note \) swallows the line-continuation and silently truncates the command.
Or without Docker: LINKEXAI_API_KEY=… AGENT_PRIVATE_KEY=0x… npx -p @linkexai/agent-sdk linkexai-funder.
Pull failing with
403 Forbiddenfrom a*.daocloud.io/dockerproxy.comURL? Your Docker daemon has a Docker Hub registry mirror that doesn't serve user-namespace repos. Remove theregistry-mirrorsentry from~/.docker/daemon.json(or Docker Desktop → Settings → Docker Engine) and restart Docker, or pull on a network without the mirror.
Keep it running on a box that stays on (a small VPS, a server, a container host) so
top-ups happen even overnight. For embedding the funder in code you already run, use
the Funder engine or autoTopup mode below.
Funder environment variables (required: the API key + one funding credential; everything else optional):
| Var | Rail | Default | Notes |
|---|---|---|---|
| LINKEXAI_API_KEY | all | — | required — the agent's API key (sk-…) |
| AGENT_PRIVATE_KEY | EVM | — | 0x + 64 hex; required on the EVM rail |
| NETWORK | all | unset = EVM | CAIP-2 chain. solana:* selects the Solana rail; unset / eip155:* = EVM |
| AGENT_SOLANA_SECRET_KEY | Solana | — | base58 or a 32/64-byte JSON array; required when NETWORK=solana:* |
| AGENT_SOLANA_RPC_URL | Solana | public RPC | override the RPC used to build the transfer |
| CIRCLE_WALLET_ADDRESS | Circle | — | auto (discover from the login session) or a 0x address; selects the key-less Circle rail |
| CIRCLE_CLI_PATH | Circle | circle | path to the Circle CLI binary when it isn't on PATH |
| SYMBOL | all | per-network | token symbol (e.g. USDC) |
| LINKEXAI_BASE_URL | all | https://api.linkexai.com | gateway base URL |
| THRESHOLD_USD | all | 1 | top up when the balance falls below this (USD) |
| TOPUP_USD | all | 5 | amount added each top-up (USD) |
| POLL_SECONDS | all | 60 | balance-check interval (min 5) |
| FUNDER_STATE_FILE | all | ~/.linkexai-funder-state.json | where the double-spend cooldown persists across restarts; set none to disable |
Fund on Solana instead of an EVM chain by setting NETWORK=solana:* and AGENT_SOLANA_SECRET_KEY:
docker run --rm \
-e LINKEXAI_API_KEY=sk-… \
-e NETWORK=solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp \
-e AGENT_SOLANA_SECRET_KEY=… \
linkex/funderKey-less funding with a Circle Agent Wallet (Base)
Don't want a private key to exist at all? The funder can pay through a Circle Agent Wallet instead: a 2-of-2 MPC wallet controlled by your email login, where Circle refuses to sign anything beyond the spending limits you set (per-transaction / daily / weekly / monthly caps, changed only with your email OTP). The container holds no key — just a revocable login session.
The funder discovers the wallet from your login session — set
CIRCLE_WALLET_ADDRESS=auto and you never hand-copy an address.
One-time setup, in your own terminal (placeholders in CAPS are yours to fill —
they are bracket-free on purpose, since an unreplaced <…> is shell redirection):
npm install -g @circle-fin/cli # Node >= 20.18.2
export CIRCLE_ACCEPT_TERMS=1 # accept the CLI terms (the official image bakes this in)
circle wallet login YOUR-EMAIL # one command — it prompts for the OTP emailed to you
circle wallet list --chain BASE --type agent # login already provisioned it; prints the 0x address
# Fund the wallet with USDC on Base (an exchange withdrawal, or:
# circle wallet fund --address YOUR-0x-ADDRESS --chain BASE --amount 20 --method fiat)
# Set YOUR spending limits (a second email OTP confirms — never share it).
# The address is filled in from your session — nothing to copy:
circle wallet limit set --chain BASE --policy-type stablecoin \
--per-tx 5 --daily 20 --weekly 50 --monthly 100 \
--address "$(circle wallet list --chain BASE --type agent --quiet | head -n1)"Then run the funder with CIRCLE_WALLET_ADDRESS=auto instead of any key. The
Circle session lives in the CLI's home directory, so give the container a volume:
docker volume create circle-session
# One-time inside the container's volume: log in (repeat when the session expires).
# One command — it prompts for the OTP emailed to you; paste the 6-digit code.
docker run --rm -it -v circle-session:/home/funder \
--entrypoint circle linkex/funder wallet login YOUR-EMAIL
docker run --rm \
-e LINKEXAI_API_KEY=sk-… \
-e CIRCLE_WALLET_ADDRESS=auto \
-e THRESHOLD_USD=1 \
-e TOPUP_USD=5 \
-e POLL_SECONDS=60 \
-v circle-session:/home/funder \
linkex/funderGood to know:
autovs a pinned address.CIRCLE_WALLET_ADDRESS=autouses the one agent wallet in your session. If the session holds more than one, the funder refuses to guess and lists them — setCIRCLE_WALLET_ADDRESSto the exact0xaddress you want. See your wallets any time withcircle wallet list --chain BASE --type agent.- Sessions last ~28 days. When one expires the funder exits with the exact
re-login commands; run them and restart.
circle wallet logoutis your kill switch — the funder is stranded instantly, funds untouched. - Two kinds of OTP. Both are typed interactively at the prompt. A login OTP only authenticates the session; a policy-change OTP raises your spending limits, so only ever type that one yourself — never relay it — which is what keeps the limits trustworthy.
- First payment needs a deployed wallet. Circle agent wallets are smart
contract accounts: before the first signature, make any one transaction from
the wallet (e.g.
circle wallet transfer YOUR-0x-ADDRESS --amount 0.01 --token 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 --address YOUR-0x-ADDRESS --chain BASE— both addresses are your own agent wallet: it's a zero-value self-transfer that only deploys the account; gas is sponsored). - Base only for now (
NETWORKdefaults toeip155:8453); more chains as Circle's chain support and x402 facilitators expand. - SDK equivalent: pass
circle: { walletAddress: "0x…" }toLinkexaiClient(the SDK needs the literal address — get it withcircle wallet list --chain BASE --type agent;autois a funder-daemon convenience) instead ofagentPrivateKey.
Step 2 — Use it in your agent (every run)
Manual mode (you decide when to check)
import { LinkexaiClient } from "@linkexai/agent-sdk"
import OpenAI from "openai"
const linkexai = new LinkexaiClient({
apiKey: process.env.LINKEXAI_API_KEY!,
agentPrivateKey: process.env.AGENT_PRIVATE_KEY! as `0x${string}`,
thresholdUSD: 1.0, // top up when balance < $1
topupAmountUSD: 5.0, // buy $5 of quota when topping up
})
const openai = new OpenAI({
apiKey: process.env.LINKEXAI_API_KEY,
baseURL: "https://api.linkexai.com/v1",
})
await linkexai.ensureBalance() // check before calls; tops up if low
const r = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Hello" }],
})ensureBalance() reads your balance and, if it's below thresholdUSD, creates (or
resumes) an x402 order and pays it from the agent wallet. Call it wherever you want
— once at startup, before each request, or before expensive batches.
Always-funded mode (wire once, runs on every call)
Set autoTopup: true and pass wrapFetch() as the vendor client's fetch. Every
request that client makes now runs ensureBalance() first — no manual calls:
const linkexai = new LinkexaiClient({
apiKey: process.env.LINKEXAI_API_KEY!,
agentPrivateKey: process.env.AGENT_PRIVATE_KEY! as `0x${string}`,
thresholdUSD: 1.0,
topupAmountUSD: 5.0,
autoTopup: true, // enable always-funded mode
// autoTopupMinIntervalMs: 10_000, // optional: re-check at most every 10s
})
const openai = new OpenAI({
apiKey: process.env.LINKEXAI_API_KEY,
baseURL: "https://api.linkexai.com/v1",
fetch: linkexai.wrapFetch(), // ← wire once
})
// From here on, every call auto-tops-up first. No ensureBalance() in your code.
const r = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Hello" }],
})Notes:
- A burst of concurrent requests shares a single balance check / top-up (coalesced).
- The SDK's own balance/recharge calls use the raw
fetch, so wrapping causes no recursion. autoTopup: false(default) makeswrapFetch()a transparent passthrough — so you can toggle the behavior from one flag without changing wherewrapFetch()is wired.- By default the balance is checked before every request; set
autoTopupMinIntervalMsto skip checks that fall within that window and avoid an extra round-trip per call.
API
| Member | Description |
|---|---|
| new LinkexaiClient(opts) | apiKey, agentPrivateKey?, baseURL?, thresholdUSD?, topupAmountUSD?, autoTopup?, autoTopupMinIntervalMs?, network? (CAIP-2 rail), symbol? (token), agentSolanaSecretKey?, agentSolanaRpcUrl?, circle? ({ walletAddress, cliPath? } — key-less Circle rail) |
| getBalance() | Returns { quota, quota_usd, agent_spending? }. |
| recharge(amountUSD) | Create/resume an x402 order, sign, and pay it. |
| ensureBalance(needed?) | If balance < needed ?? thresholdUSD, recharge topupAmountUSD. Concurrent calls are coalesced. |
| wrapFetch(baseFetch?) | Returns a fetch that runs ensureBalance() first when autoTopup is on. |
| encryptAgentKey / decryptAgentKey | AES-256-GCM backup of the agent key. |
Run
LINKEXAI_API_KEY=sk-lxAgent-… AGENT_PRIVATE_KEY=0x… npx tsx agent.ts