@xilo-org/sdk
v0.1.0
Published
Pay for and read Xilo intelligence from an AI agent: quotes checked against your spending policy before anything is signed.
Maintainers
Readme
@xilo-org/sdk
Pay for and read Xilo intelligence from an agent. The safe way is the default:
- Keys stay out of the model. The signer lives inside the SDK and is read from
XILO_AGENT_PRIVATE_KEY. Your model only sees method names and results. - Spending rules run outside the model. Every price is quoted first and checked against your policy before anything is signed. These limits apply on top of the owner limits Xilo enforces server-side.
- Untrusted text never reaches the acting model unless you choose to pass it through a tool-less reader.
npm install @xilo-org/sdk
export XILO_AGENT_PRIVATE_KEY=0x… # the agent's own wallet, funded with a little USDGnetwork picks Xilo's public endpoint: "testnet" (the default) uses the public sandbox at https://xilo-staging.vercel.app, and "mainnet" uses https://www.xilomarkets.xyz (from launch). To point somewhere else, set XILO_API_URL or pass apiUrl.
Example: a sniper that checks before every buy
import { createXiloClient, stripUntrusted, XiloPolicyError } from "@xilo-org/sdk";
const xilo = createXiloClient({
network: "testnet",
policy: {
maxPerCallUsdg: 0.25,
maxPerDayUsdg: 20,
maxPerTaskUsdg: 5,
allowedPayTo: ["0x…xilo-collector"], // only Xilo's collector
},
});
// Pick the most accurate contract-safety seller once.
const { results } = await xilo.search({ category: "contract_safety", type: "lookup" });
const safety = results[0] as { id: string; versionHash: string };
async function shouldBuy(token: `0x${string}`) {
try {
const r = await xilo.lookup({ listingId: safety.id, subject: token });
const out = stripUntrusted(r.result) as { verdict: "SAFE" | "RISKY" | "RUG"; honeypot?: boolean };
return out.verdict === "SAFE" && out.honeypot !== true;
} catch (e) {
if (e instanceof XiloPolicyError) return false; // over budget or a suspicious quote: skip, don't pay
throw e;
}
}Paying
| Mode | How | When |
| --- | --- | --- |
| payment: "x402" (default) | A USDG authorization (EIP-3009), signed per call and settled only after the seller delivers | Simple setups; no balance to manage |
| payment: "balance" | Your prepaid Xilo balance, via a signed agent key | Many small calls, alerts (watch), MCP. Top up with xilo.topup(10) |
In both modes the SDK asks for a quote first and refuses before signing anything if:
- the payee isn't in
allowedPayTo; - the network, asset or scheme is wrong;
- the price is over
maxPerCallUsdg; - the day's spend would go over
maxPerDayUsdg. Parallel calls reserve their spend first, so they can't overshoot.
Calls that fail after payment aren't settled by Xilo, and the SDK doesn't count them. Free trial calls count as 0.
Daily spend is kept in memory. Long-running agents should pass spendStore (get(day) and add(day, micro)) backed by a file or a database.
Methods
| Method | Paid | Notes |
| --- | --- | --- |
| search(q) | free | Ranked by measured accuracy |
| lookup({ listingId, subject }) | ✅ | One token or wallet |
| getList({ listingId }) | ✅ | |
| batch({ lookups }) | ✅ | Up to 50, all or nothing |
| hire({ listingId, input, capUsdg? }) | ✅ | capUsdg defaults to, and can't exceed, maxPerTaskUsdg |
| task(taskId, readToken?) | free | Status, plus the result for the buyer |
| watch({ listingId, subjects }) / unwatch(id) | balance | Live alerts |
| balance() / topup(usdg) | — / ✅ | |
| spentToday() | — | USDG spent through this client today |
| prepareSwap({ tokenIn, tokenOut, amountIn, slippageBps?, acceptRisk? }) | free | Unsigned swap for this wallet; see below |
Version pinning: set policy.pinnedVersions (listingId → versionHash). Calls then fail with a 409 if the seller changed the listing, until the owner approves the new version in the dashboard.
Acting on it: non-custodial swaps
prepareSwap() returns an unsigned swap for the agent's own wallet. Xilo never signs, never holds funds and adds no fee. The rails are enforced by Xilo's API, not by your prompt:
- Buying any token except native ETH (
0xeeee…eeee) or USDG needs acontract_safetylookup on that token by this agent in the last 24 hours.- RUG, honeypot, or a deployer who rugged before (
dev_history) refuses the swap. - RISKY, or a sell tax over 10%, needs
acceptRisk: true.
- RUG, honeypot, or a deployer who rugged before (
- The output always goes to the calling wallet. Slippage is capped at 5%. Only known aggregator routers are used. Approvals are for the exact amount.
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const r = await xilo.lookup({ listingId: safety.id, subject: token }); // the check
const swap = await xilo.prepareSwap({
tokenIn: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
tokenOut: token,
amountIn: "10000000000000000", // 0.01 ETH, in wei
slippageBps: 100,
});
// Check swap.quote, then send from your own wallet:
const wallet = createWalletClient({ account: privateKeyToAccount(process.env.XILO_AGENT_PRIVATE_KEY as `0x${string}`), transport: http(RPC_URL) });
if (swap.approval) await wallet.sendTransaction({ to: swap.approval.to, data: swap.approval.data, chain: null });
await wallet.sendTransaction({ to: swap.tx.to, data: swap.tx.data, value: BigInt(swap.tx.value), chain: null });Swap routing is available on mainnet (KyberSwap's aggregator); on testnet the endpoint answers 503.
Seller rewards
Staked sellers whose calls were right share 20% of the pool's trading fees, in ETH, through the daily Pulse report (see /pulse on the site). They're payment for accurate work, not for holding.
const rewards = await xilo.rewards(); // every day you earned, newest first
for (const c of await xilo.prepareRewardClaims()) {
// Unsigned, like prepareSwap: send each from the seller wallet. Only final days are included.
await wallet.sendTransaction({ to: c.tx.to, data: c.tx.data, value: 0n });
}A claim needs an active stake, reverts if that day was already claimed, and stays open for 90 days.
Untrusted text: two-step reading
Outputs are structured. Free text only arrives as { untrusted: true, text }. Either strip it:
const safe = stripUntrusted(r.result); // every untrusted text becomes "[untrusted text removed]"or read it with a model that has no tools and no wallet. Anything outside your schema is dropped:
import { readUntrusted } from "@xilo-org/sdk";
const { risk } = await readUntrusted(r.result.note, z.object({ risk: z.enum(["low", "high"]) }), myToolessLLM);Registering the agent with its owner
The owner starts registration on xilo's /agents page, which shows a one-time message. The agent signs it with its own key:
XILO_AGENT_PRIVATE_KEY=0x… npx @xilo-org/sdk sign "Xilo: register agent 0x… to owner 0x…
Nonce: …"Paste the signature back on the page. sign only signs Xilo's register-agent message for this key's own address, so it can't be used to sign anything else (a token permit, for example).
MCP (Claude Code, Cursor, …)
XILO_NETWORK=mainnet npx @xilo-org/sdk mcp-config # or leave XILO_NETWORK unset for the testnet sandboxThis prints the mcpServers block. Its Authorization header is your agent key, a signature rather than the private key, but it can spend your prepaid balance, so keep it out of shared files.
For Claude Code, print the install command instead and run it:
npx @xilo-org/sdk mcp-config --claude-code
# claude mcp add --scope user --transport http xilo https://…/mcp --header 'Authorization: XiloKey …'--key prints only the agent key (wallet:signature), for the Xilo Claude Code plugin's XILO_AGENT_KEY.
Try a listing from the terminal
XILO_AGENT_PRIVATE_KEY=0x… npx @xilo-org/sdk try <listingId> <0x…token> --max 0.1One real lookup. It uses a free trial call if the wallet has one, otherwise the prepaid balance, and refuses before signing anything if the price is over --max (0.1 USDG by default). Pin a version with --version 0x…. The same flow is tryLookup() in code.
Publishing
The package is ready to publish:
@xilo/sharedis bundled in: tsup bundles the code, andscripts/bundle-types.mjsships the types underdist/shared.viemandzodare its only runtime dependencies.
Checked from outside the monorepo: npm pack gives a 40 KB tarball. Installed into an empty project, it imports, type-checks under strict with skipLibCheck off, runs the xilo-sdk CLI, and does a live search.
To publish:
- The npm organization is
xilo-org(free for public packages); your npm user must be a member. - Run
npm login. - From
packages/sdk:pnpm build && npm publish.publishConfig.accessis alreadypublic.
Bump version for every release.
