@delicti-protocol/sdk
v0.16.1
Published
Spending limits for AI agents that hold across calls and chains, proven by Flare's FDC and enforced by a bond: TypeScript SDK, watchers and sentinel for DELICTI. x402 · stablecoins · XRPL · Flare.
Maintainers
Readme
@delicti-protocol/sdk
Spending limits for AI agents that hold across a whole sequence of payments. The proof comes from consensus, and a bond enforces them. An agent told "spend at most 4" that spends 1 five times passes every per-call check. DELICTI judges the sum. The Flare Data Connector witnesses what the agent really did on Flare, Ethereum or the XRP Ledger, and the agent's bond pays for the breach in proportion. There is no court and no admin key.
Works with x402 (EIP-3009) payments, stablecoins (USD₮0, USDC.e, FXRP), MCP tool servers and receipt-less agents.
TypeScript for DELICTI, built on viem. It has three parts:
Delicti, the calls an integrator makes. A principal commits a mandate and bonds it, an agent accepts it or declares it exclusive, and anyone reads where a mandate stands.Erc20OutflowWatcherand thedelicti-watchCLI, the party the protocol's economics were written for. It watches an exclusive stablecoin mandate and keeps its §6.11 docket current. When the agent's outflow crosses the budget, it commits, waits out the lead, proves the deeds through the FDC and files the conviction. It needs no receipts and no cooperation from the agent or the facilitator.XrplOutflowWatcherdoes the same for §6.10 on XRPL.Sentinel(delicti-watch sentinel) runs across the whole protocol. It discovers every mandate, watches every one a third party can (§6.11 stablecoins on Flare, §6.10 XRP outflow on XRPL), prices each piece of work before buying a single attestation, acts according to its policy, and publishes a per-agent public score (SPEC §11.2).
v0.16, Coston2 and XRPL testnet only. Not audited. Whitehat use on testnets.
Install
npm i @delicti-protocol/sdk viem # the library (ESM, Node ≥ 22, types included)
npx -p @delicti-protocol/sdk delicti sentinel # the command line, without installing anythingOnce installed, the CLI is delicti (alias delicti-watch).
SUMMA: one dollar budget across chains (amendment v1.1, live on Coston2)
import { coston2, judgeSummaAbi, summaMeterAbi, mandateFacilitatorAbi } from "@delicti-protocol/sdk";
coston2.summa; // { judge, vault, meter, facilitator }An umbrella mandate states the budget in µUSD. Its agent links rail mandates to it: XRP outflow on the XRP Ledger, and USD₮0 or any other mapped ERC-20 on Flare. Every deed is priced at the FTSO anchor value of the round it happened in, and that price is proven on-chain. SummaMeter refuses the slice that would cross the budget, whichever chain it is on. MandateFacilitator settles x402 (EIP-3009 receiveWithAuthorization) only through that brake, and the brake, the settlement and the receipt happen in one transaction. Live runs are in docs/DEPLOYMENTS.md.
An agent under a mandate, in a few lines
import { Delicti, coston2 } from "@delicti-protocol/sdk";
const delicti = new Delicti(coston2, publicClient);
const { id } = await delicti.commitMandate(principal, {
agent: agent.account.address,
terms: "may pay up to 4 USDT0 for data, via x402",
budget: 4_000_000n, // token units (6 decimals)
validFrom: now, validUntil: now + 86_400n,
token: USDT0, // omit for the native asset
});
await delicti.declareExclusive(agent, id); // "everything my address does in the window is this mandate's"
await delicti.post(principal, id, parseEther("100"));
// …the agent pays by x402 as usual (it signs, a facilitator sends). Nothing else changes for it.
console.log(await delicti.status(id)); // live, bond, severity, docketsThe watcher
export PRIVATE_KEY=0x… # the watcher's own key: pays attestation fees, earns the reward
export VERIFIER_URL=… VERIFIER_API_KEY=… DA_URL=…
npx delicti erc20 12 # every 60 s
npx delicti erc20 12 --once # one cycle
npx delicti status 12Each cycle goes through these steps:
- It reads the mandate. It must name this deployment's Vault, have an ERC-20 as its asset, be exclusive, and still have a bond.
- It finds every
Transfer(from = agent)of that token sincevalidFrom. It uses the explorer's log index for this, because Flare RPCs serve 30 blocks pereth_getLogs. The explorer only finds candidates and is never a witness: nothing reaches the judge except inside an FDC proof. - It drops logs already on the docket (
eventFiled) and logs outside the window. It groups the rest by transaction, in ascending hash order, with at most 50 logs per request (the FDC's cap). What is left over waits for the next cycle. - Below the budget: it requests one
EVMTransactionattestation per transaction, listing exactly the logs it means to file, and files them. No commitment, no reward. This keeps the docket current. - Past the budget: it commits first (kind 8, 256-bit random salt), before any attestation makes the case public. It waits for the first voting round that starts
commitLeadafter the commitment. Then it attests, files, and the Vault slashes.
The planner (planErc20) is pure and tested on its own (test/planner.test.ts).
The XRPL watcher: reading history the way the FDC will
A mandate names its XRPL account only by hash (agentRef). The watcher finds the account from the mandate's exclusivity statement: the ExclusiveProven event holds the statement's XRPL transaction id, the FDC verifier's index says who signed it, and keccak256(signer) must equal agentRef.
Reading the account's history takes more care than it seems. account_tx over a real window needs a full-history XRPL server; the public testnet endpoint reachable here keeps about 1,300 ledgers, under an hour and a half. But every transaction that moves an account's XRP modifies its AccountRoot, and each modification records the previous transaction that did (PreviousTxnID). So the balance history is a linked list, anchored at account_info. XrplHistory.walk follows it backwards through the FDC verifier's own index, about 15 days of full transactions with metadata. It finds exactly what can still be proven, offers taken in other accounts' transactions included, and a balance change that is not on the list did not happen.
npx delicti xrpl 13 --once # one mandateThe sentinel
npx delicti sentinel # observe everything, act on nothing, no key needed
npx delicti sentinel --policy profit --interval 300 # act where stipends + reward cover the cost
npx delicti sentinel --policy altruist --html report.html --out report.json --state sentinel.jsonEach round goes through five steps:
- Discover every mandate from state (
nextId,get), not from logs. - Classify each one:
- §6.11 and §6.10 are watchable by anyone.
- Receipted cases (§6.2, §6.3, §6.8) are listed. Judging them needs the agent's leaves.
- Unacknowledged mandates are ignored, as SPEC §11.1 requires.
- A mandate bonded in an older Vault is watched by that Vault's judges, as far as they can go (
network.history).
- Observe each watchable mandate against its source chain.
- Price each plan. The cost is attestation fees plus gas. The income is the watch pool's stipends (§8.4, v0.14) plus the reward from
BondLens.penaltyFor. - Act according to the policy:
observe: act on nothing.profit: act only where the income covers the cost.altruist: act on everything. Somebody has to, and the protocol's own sentinel is altruist (docs/research/watchers.md).
The score it publishes is per agent and deliberately not a single number. The facets are:
- standing, where
breach-unjudgedis the alarm: the chain shows more outflow than the budget, and no verdict exists yet; - verdicts and value taken, across every Vault;
- bond at stake;
- worst budget use;
- watched and self-watched mandates;
- unfiled and lost deeds.
The watch pool (v0.14; watch pool v2 since v0.16)
await delicti.setWatchTerms(principal, id, parseEther("0.05"), 100_000n); // per new deed moving ≥ 0.1 XRP
await delicti.fundWatch(principal, id, parseEther("0.5")); // the principal onlyA stipend is paid to whoever sealed the deed's exact attestation request, then paid for it through the Vault, not to whoever files it.
The watchers do this automatically (seal.ts):
- On v0.16 mandates they seal every request, wait
commitLead(10 minutes) and pay with the salt. - Before a conviction the seals go in before the case's own commitment, so one wait covers both.
- A request someone else already holds goes straight to FdcHub.
- On v0.15 mandates they pay unsealed, as those Vaults require.
A copier who files someone else's proofs, or lifts a request from the mempool, holds nothing.
import { requestAttestations } from "@delicti-protocol/sdk";
const { plan, rounds } = await requestAttestations({ fdc, publicClient, wallet, dep, requests });examples/seal-live.ts runs it live on Coston2:
- a watcher seals and is paid;
- an attacker's made-up MIC holds a key no proof names;
- a mempool copier is refused on-chain, with and without a fresh seal.
SUMMA: one dollar budget, the tripwire, the attempt register (amendments v1.1, v1.2)
import { Summa, signPayment, USD6_ASSET } from "@delicti-protocol/sdk";
const summa = Summa.of(coston2, publicClient); // the v0.16 stack; Summa.of(net, pc, vaultSumma) for an older one
const { id: umbrella } = await delicti.commitMandate(principal, { agent, terms: "at most $40", budget: 40_000_000n,
validFrom, validUntil, source: "SUMMA", assetKey: USD6_ASSET, bond: coston2.summa!.vault });
await summa.link(agent, umbrella, member); // the umbrella's agent puts a rail under it
await summa.declareEffector(principal, umbrella, guardOrFacilitator);
await summa.setTripwire(principal, umbrella, 1n); // one recorded attempt stops every rail
const auth = await signPayment(agent, { token, domain: { name: "Mock USDT0", version: "1" }, facilitator: coston2.summa!.facilitator,
seller, umbrellaId: umbrella, memberId: member, value, validBefore });
await summa.settle(anyone, { umbrellaId: umbrella, memberId: member, seller, auth }); // or, refused by the brake:
await summa.recordAttempt(anyone, { umbrellaId: umbrella, memberId: member, seller, auth });
await summa.state(umbrella); // { tallyUsd6, tripwire, strikes, tripped }
await summa.rearm(principal, umbrella);delicti status <id> reads any mandate from the Vault it names, whichever version that is. For an umbrella, it also reads its meter.
Tests
npm test # offline: both planners, the XRPL history reader on a real taken offer,
# the score, commitment encoding, decoding a real FDC proof
DELICTI_ONLINE=1 npm test # + a round trip through the live JudgeEvm on Coston2The commitment encoding is pinned to a value the v0.13 Vault computed on-chain. The online test sends a real proof from mandate #12 back to its live v0.13 judge. The judge passes it through FdcVerification and every check, then refuses it with NothingNew because it is already filed. That round trip is the proof that the SDK's encoding is the contract's.
Regenerating the ABIs
forge build at the repo root, then npm run abi. src/abi.ts is generated from the Foundry build and never edited by hand.
