Reference
Agent Keys
Mint scoped agent session keys — give your agent a budget, not your keys.
An agent key is a real on-chain signer on the user's smart wallet, restricted
so it can only move specific tokens, and only when the policy contracts you
attach co-sign inside the wallet's __check_auth. The chain enforces the
result: an autonomous agent holding the key can pay for things on its own, but
cannot exceed its budget or step outside its policies — even if the key is
fully compromised.
Requires
vellar-sdk≥ 0.5.0. The SDK exposes this aswallet.agents.
Why this is different from a software budget
Most agent-payment tools enforce spending limits in code the agent runs, or in
a gateway it calls — a rule the agent could bypass if compromised. A Vellar
agent key's limits live in the smart wallet's __check_auth, so they are
checked by consensus every time the agent signs. Stack two policies and
both are enforced on-chain:
- a spending-limit policy — how much (a cumulative rolling-window cap),
- a verified-only policy — through what (only contracts whose source is attested as verified; see Policies).
A key restricted by both can pay for API calls all day, but cannot overspend its cap and cannot be tricked into paying through unverified code. Nobody — not your server, not the SDK, not a hijacked agent process — can override that.
The flow
1. Deploy the policies (wallet.policies) — e.g. a spending-limit instance and
a verified-only instance, each attached to the wallet.
2. Generate an agent keypair. YOU hold the secret; the SDK never sees it.
3. wallet.agents.mint(...) adds the agent's public key as a signer whose
SignerLimits require those policies to co-sign the granted tokens.
← the ONE passkey prompt
4. Hand the agent its secret + the wallet's C-address. It pays via
wallet.x402 under the on-chain budget — no passkey, no admin keys.
5. wallet.agents.revoke(...) removes the key on-chain — the remote kill.
Minting a key
import { Keypair } from "@stellar/stellar-sdk";
// You generate the agent's keypair and keep the secret. The SDK never
// receives it — only the public key goes on-chain as a signer.
const agentKey = Keypair.random();
const { hash, expiresAt } = await vellar.agents.mint({
publicKey: agentKey.publicKey(),
grants: [
{
token: usdcSac, // the SEP-41 token the agent may move
policies: [spendingLimitId, verifiedOnlyId], // both must co-sign
},
],
expiresAt: new Date(Date.now() + 7 * 864e5), // optional on-chain expiry
// store: "persistent" (default) | "temporary"
});
grants is a list of { token, policies } — one per token the agent may
transact through. Each grant must name at least one policy; an
unrestricted grant is deliberately not mintable through wallet.agents
(mint one through your own kit wiring if you truly intend an unlimited signer).
For a budget in more than one token, attach a separate token-scoped policy per
token and add a grant for each.
expiresAt accepts a Date or unix seconds; an expired key stops signing
with no revoke needed. mint returns the transaction hash and, if set, the
ISO expiry.
Revoking a key
await vellar.agents.revoke(agentKey.publicKey());
On-chain removal is immediate — the key stops signing the moment the transaction confirms. This is the remote kill for a lost or misbehaving agent.
Configuration
mint and revoke are wallet-admin actions, so they are passkey-signed
through an agentKeys runtime you wire to your PasskeyKit — exactly like
policyAttach. Without it, wallet.agents calls throw a
clear error; the rest of the wallet still works.
const vellar = createVellarWallet({
network: "testnet",
appName: "My App",
kit,
sac,
backend,
isValidAddress,
agentKeys: {
// Resume the connected passkey without a prompt, when possible.
resume: (keyId) => resumeKitConnection(kit, keyId),
// Add the agent key as a policy-limited signer. Build the SignerLimits
// from the grants, passkey-sign, submit via your backend.
async addAgentKey({ publicKey, grants, expirationSeconds, store }) {
const { SignerStore, SignerKey } = await import("passkey-kit");
const limits = new Map(
grants.map((g) => [g.token, g.policies.map((p) => SignerKey.Policy(p))]),
);
const tx = await kit.addEd25519(
publicKey,
limits,
store === "temporary" ? SignerStore.Temporary : SignerStore.Persistent,
expirationSeconds,
);
const signed = (await kit.sign(tx)) ?? tx;
const { hash } = await backend.submitTransaction({
signedXdr: typeof signed === "string" ? signed : signed.toXDR(),
network: "testnet",
});
return { hash };
},
async removeAgentKey(publicKey) {
const { SignerKey } = await import("passkey-kit");
const tx = await kit.remove(SignerKey.Ed25519(publicKey));
const signed = (await kit.sign(tx)) ?? tx;
const { hash } = await backend.submitTransaction({
signedXdr: typeof signed === "string" ? signed : signed.toXDR(),
network: "testnet",
});
return { hash };
},
},
});
The SDK stays free of a passkey-kit dependency by keeping this a seam — the
same reason policyAttach is wired the host side.
Honesty
- The agent secret is yours to hold. The SDK never sees or stores it. Keep it in the agent's own secure storage (a non-extractable key where the runtime supports it); never in the browser or the wallet app.
- Verified ≠ safe. A verified-only grant restricts the agent to contracts with reproducible, attributable source — provenance, not an audit or a safety proof. See Policies.
- Expiry + revoke bound the blast radius. A compromised agent key can, at worst, spend up to its remaining budget through verified contracts until it expires or you revoke it — never the account's full balance, never admin actions, never unverified code.
Once minted, the agent pays with x402 using
createSessionKeySigner. Together that is the full loop: give your agent a
budget, not your keys.