Skip to content

The full client ​

Every option of every call, in base units. Start with Build in TypeScript: this page continues its program.

Connect ​

solive.client is the full client underneath connect. createClient makes one on its own.

ts
import { createClient } from "@intent-engine/client";

const client = createClient({ url: "https://api.devnet.solana.com" });

Create a swap intent ​

Sell 0.05 SOL for at least 50 test tokens. The devnet filler prices test tokens at 200 per wrapped SOL and pays exactly the minimum asked; 50 is more than 0.05 SOL buys at that price, so the intent stays open until you cancel it.

createIntent sets up what it needs in the same transaction: it registers the wallet, wraps the SOL and creates the receiving token account. It picks the next free nonce and expires the intent in an hour, unless you pass nonce, expiresIn or expiresAt.

ts
import { DEVNET_TEST_MINT, Kind, NATIVE_MINT, parseAmount } from "@intent-engine/core";

const sell = await client.mint(NATIVE_MINT);
const buy = await client.mint(DEVNET_TEST_MINT);
const order = { signer, inputMint: NATIVE_MINT, outputMint: DEVNET_TEST_MINT };
const swap = { kind: Kind.SwapExactIn, amountIn: parseAmount("0.05", sell.decimals), minOut: parseAmount("50", buy.decimals) } as const;
const { nonce } = await client.createIntent({ ...order, params: swap });

Read ​

  • intents({ owner }) returns a wallet's intents; intent(owner, nonce) returns one, or null once it is closed.
  • configSummary spells out the engine's settings: pauses, which kinds are on, and their fees.
  • kindName, statusName and termsOf name an intent's kind and status and decode its terms.

This prints SwapExactIn Open and then the terms in tokens.

ts
import { configSummary, formatAmount, IntentStatus, kindName, statusName, termsOf } from "@intent-engine/core";

const { createPaused, fillPaused, kinds } = configSummary(await client.config());
console.log(createPaused, fillPaused, kinds.map(({ name, create, fill, feeBps }) => `${name} ${create} ${fill} ${feeBps}`));
const mine = await client.intents({ owner: signer.address });
for (const { intent } of mine.filter(({ intent }) => intent.nonce === nonce)) {
  const { sells, receivesAtLeast } = termsOf(intent);
  console.log(kindName(intent.kind), statusName(intent.status));
  console.log(formatAmount(sells, sell.decimals), formatAmount(receivesAtLeast ?? 0n, buy.decimals));
}

Watch ​

watch calls back with the current intents, then with every intent that changes, until its signal aborts. It rereads the chain every resyncSeconds; pass onError to hear about failed reads. A filled intent's account closes soon after the fill, so intent then returns null. This prints Open now and Cancelled once the cancel below lands.

ts
const controller = new AbortController();
const watched = client.watch(
  (changed) => {
    for (const { intent } of changed.filter(({ intent }) => intent.nonce === nonce)) {
      console.log(statusName(intent.status));
      if (intent.status !== IntentStatus.Open) {
        controller.abort();
      }
    }
  },
  controller.signal,
  { owner: signer.address },
);

Errors ​

A failed call throws. explain names the engine error behind it, or returns null if the engine raised none; engine errors lists each one and what it means for your call. An expiry in the past prints ExpiryNotInFuture.

ts
const failure = await client.createIntent({ ...order, params: swap, expiresAt: 0n }).catch((error: unknown) => error);
console.log(client.explain(failure)?.name);

Cancel ​

cancel returns the escrow to your wrapped SOL token account, where refunds and payouts in SOL arrive. unwrap closes that account so its balance returns to the wallet as SOL, and resolves to null when the wallet holds none.

ts
await client.cancel(signer, nonce);
await watched;
await client.unwrap(signer);

Limit orders and Dutch auctions ​

A LimitOrder sells makeAmount for at least takeAmount. A DutchAuction sells amountIn for at least startOut until startTs, after which its minimum falls in a straight line to endOut at endTs. A fill takes the whole escrow. termsOf prices an intent at the time you pass it; pass the chain's time from chainTime, since the chain's clock and yours can differ. tokenName returns SOL for NATIVE_MINT, TEST for DEVNET_TEST_MINT and undefined for any other mint.

fillOf reads how an intent was filled, also after its account closed, or returns null before a fill. It returns the steps, the solver, what it delivered, the minimum required, the protocol fee and the solver's net, with the signature, slot and blockTime. An auction's minimum keeps falling, so it can end below what the filler delivered.

Each order below sells 0.01 SOL, and the devnet filler fills both. The code waits for each fill, so run it while the filler is online. This prints each order's kind, the least it receives now and its token, then the steps of its fill, what it delivered and the minimum at the fill; for the limit order these are 2, 1.5 and 1.5.

ts
import { setTimeout as sleep } from "node:timers/promises";
import { eventNamed, tokenName } from "@intent-engine/core";

const start = await client.chainTime();
const limit = { kind: Kind.LimitOrder, makeAmount: parseAmount("0.01", sell.decimals), takeAmount: parseAmount("1.5", buy.decimals) } as const;
const auction = { kind: Kind.DutchAuction, amountIn: limit.makeAmount, startOut: parseAmount("1.9", buy.decimals), endOut: parseAmount("1", buy.decimals), startTs: start, endTs: start + 600n } as const;
for (const params of [limit, auction]) {
  const placed = await client.createIntent({ ...order, params });
  const { intent } = (await client.intent(signer.address, placed.nonce))!;
  console.log(kindName(intent.kind), formatAmount(termsOf(intent, await client.chainTime()).receivesAtLeast ?? 0n, buy.decimals), tokenName(intent.outputMint));
  let fill = await client.fillOf(placed.intent);
  while (fill === null) {
    await sleep(2_000);
    fill = await client.fillOf(placed.intent);
  }
  console.log(fill.steps, formatAmount(fill.delivered, buy.decimals), formatAmount(fill.required, buy.decimals));
}

Escrow work behind a proof ​

An AttestedResult intent holds payment until its judge signs a result. specHash pins the spec the judge checks, and judgeFee pays the judge out of the escrow.

settleProof is the judge's call: it pays the worker's wallet payTo. Before sending, it refuses an expired escrow (EscrowExpired), a payTo owned by a program (PayToNotWallet), a payTo whose token account moved (PayeeMoved) and an escrow another worker has claimed (NotSettleable). Here your wallet pays the fee, so the new judge key needs no SOL. The judge keeps its 64 key bytes, because it opens what workers seal to it.

history returns an intent's events, newest first, also after it closes. An IntentAttested event's net is what the payee received. This prints Filled, then IntentAttested and the worker's pay in SOL.

ts
import { generateKeyPairSync } from "node:crypto";

const judgeJwk = generateKeyPairSync("ed25519").privateKey.export({ format: "jwk" });
const judgeKeypair = Uint8Array.from([...Buffer.from(judgeJwk.d ?? "", "base64url"), ...Buffer.from(judgeJwk.x ?? "", "base64url")]);
const judge = await createKeyPairSignerFromBytes(judgeKeypair);
const worker = await generateKeyPairSigner();
const specHash = new Uint8Array(createHash("sha256").update("the acceptance spec the work must pass").digest());
const pay = parseAmount("0.01", sell.decimals);
const work = { kind: Kind.AttestedResult, amountIn: pay, minResult: 1n, maxResult: 1n, attestor: judge.address, specHash, judgeFee: pay / 10n } as const;
const job = await client.createIntent({ ...order, params: work });
await client.settleProof({ judge, payer: signer, owner: signer.address, nonce: job.nonce, payTo: worker.address, result: 1n });
console.log(statusName((await client.intent(signer.address, job.nonce))?.intent.status ?? -1));
const paid = eventNamed(await client.history(job.intent), "IntentAttested");
console.log(paid?.name);
console.log(formatAmount(paid?.fields.net ?? 0n, sell.decimals));

Fees ​

  • Each kind has a protocol fee, feeBps in configSummary, taken from the escrow at the fill.
  • An intent records its kind's fee and the partner cut, feeBps and partnerCutBps, when it is created, and every fill or release pays those. A later config change applies only to intents created after it.
  • The engine caps it at MAX_FEE_BPS (100) for swap, limit and dutch orders and at MAX_ATTESTED_FEE_BPS (300) for attested ones; maxFeeBpsFor gives a kind's cap.
  • A proof escrow pays the judge's judgeFee first. The protocol fee and any partner fee come out of the rest, and the worker gets what remains.
  • The protocol keeps the intent's partnerCutBps of every partner fee.
  • splitFees and feeBpsFor from @intent-engine/core compute the same split.

This prints what the worker, the judge and the protocol received, in SOL; together they make up the escrow.

ts
const received = [paid?.fields.net ?? 0n, work.judgeFee, paid?.fields.fee ?? 0n];
console.log(received.map((amount) => formatAmount(amount, sell.decimals)).join(" "));

When the judge refuses ​

The engine has no refusal call: a judge that rejects the work sends nothing, and the escrow stays open. The owner cancels it. If the owner does not, anyone can expire the intent once its expiry passes, which returns the escrow to the owner's token account. After the cancel, settleProof throws NotSettleable. This prints Cancelled NotSettleable.

ts
const refused = await client.createIntent({ ...order, params: work });
await client.cancel(signer, refused.nonce);
const late = await client.settleProof({ judge, payer: signer, owner: signer.address, nonce: refused.nonce, payTo: worker.address, result: 1n }).catch((error: unknown) => error);
console.log(statusName((await client.intent(signer.address, refused.nonce))?.intent.status ?? -1), (late as Error).name);
await client.unwrap(signer);

Expire and close ​

expire refunds an open intent once it has expired, and close returns the rent of a finished intent. Both take the signer, then the owner and nonce. The devnet filler also expires and closes overdue intents, in passes with a pause between them, so it may get there first; the code below allows for that. This prints null.

ts
const due = (await client.chainTime()) + 10n;
const brief = await client.createIntent({ ...order, params: swap, expiresAt: due });
while ((await client.chainTime()) < due) {
  await sleep(1_000);
}
try {
  await client.expire(signer, signer.address, brief.nonce);
} catch (error) {
  if ((await client.intent(signer.address, brief.nonce))?.intent.status === IntentStatus.Open) {
    throw error;
  }
}
try {
  await client.close(signer, signer.address, brief.nonce);
} catch (error) {
  if ((await client.intent(signer.address, brief.nonce)) !== null) {
    throw error;
  }
}
console.log(await client.intent(signer.address, brief.nonce));
await client.unwrap(signer);

Find work and deliver it ​

  • specUrl on createIntent publishes where the spec lives, in a memo the owner signs.
  • jobs lists up to 50 open, unexpired proof escrows, those with the most time left first, each with its specUrl.
  • spec fetches that url, following no redirect and reading at most 1048576 bytes, and says whether it matches the committed hash. It never runs the text.
  • deliver announces a worker's work: a memo with the intent, a commitment that binds the work's sha256 to the worker's wallet and a salt, a check of that salt, and the salt and url sealed to the judge or to each committee member, plus a 0-lamport transfer to the judge.
  • The salt comes from the worker's own signature over the intent and the sha256, so only its key derives it. deliver refuses a signer that cannot sign messages, such as a Ledger, with NoMessageSigner; saltSignature passes a signature made elsewhere, which must verify for the worker (BadSaltSignature).
  • deliveryRecord lists the announcements to a judge, newest first, reading at most 100 pages of 1000 transactions. With the judge's 64-byte keypair it opens each sealed salt and url. Decide whom to pay only when its complete is true.
  • A transport you pass to createClient needs signaturesOf, transactionsOf and firstAvailableSlot for a complete record.

Here your wallet plays the worker, and the judge pays the earliest wallet that announced the hash it checked. This prints true true twice.

ts
import { firstDelivery } from "@intent-engine/client";

const specUrl = "https://raw.githubusercontent.com/MerlijnW70/solive/3aad6ee86067e10695d44e47fbc5ed67535a5cfb/SECURITY.md";
const spec = new Uint8Array(createHash("sha256").update(new Uint8Array(await (await fetch(specUrl)).arrayBuffer())).digest());
const posted = await client.createIntent({ ...order, params: { ...work, specHash: spec }, specUrl });
const listed = (await client.jobs({ attestor: judge.address })).find(({ intent }) => intent === posted.intent);
console.log(listed?.specUrl === specUrl, (await client.spec(posted.intent))?.matches);
const sha256 = createHash("sha256").update("the delivered work").digest("hex");
await client.deliver({ worker: signer, intent: posted.intent, sha256, url: "https://example.org/work.tar" });
const record = await client.deliveryRecord({ judge: judge.address, intent: posted.intent, keypair: judgeKeypair });
if (!record.complete) {
  throw new Error(`read ${record.pages} pages without reaching the escrow's creation; pass a higher pages`);
}
const delivery = firstDelivery(record.deliveries, sha256);
console.log(delivery?.worker === signer.address, delivery?.sha256 === sha256);
await client.settleProof({ judge, payer: signer, owner: signer.address, nonce: posted.nonce, payTo: delivery!.worker, result: 1n, deliveredSha256: sha256, deliveredSalt: delivery!.salt });
await client.unwrap(signer);

firstDelivery(record.deliveries, sha256) returns the earliest announcement whose opened salt opens its commitment to the checked hash, or null; the judge pays its announcer. settleProof takes deliveredSha256 and deliveredSalt to record the paid work's hash and the payee's salt in the release, in the memo intent-engine receipt and solive_wallet_receipt read. Judge work does all of this from the command line.

judgeOpenings(record.deliveries, delivery) lists the salts of the announcements before the payee's, in order, split into as many memos as needed, and openingInstructions publishes each. openDelivery publishes one announcer's own salt. recordedStanding reads a release as valid when the recorded salt opens an announcement by a wallet the release paid, and gives refutedBy when a published salt shows an earlier announcement of the same hash.

Who can do what ​

  • Any wallet other than the owner and the receiver can fill an open swap intent before it expires.
  • Only a verdict signed by the judge the intent names settles a proof escrow; any wallet can send that signed verdict and pay for it.
  • A fill or settle at or after the expiry fails with IntentExpired; settleProof throws EscrowExpired before it sends.
  • Only the owner can cancel; another wallet's cancel fails with Unauthorized.
  • The owner can cancel an open proof escrow until the judge settles it, also after it expires as long as nobody has expired it.
  • After a settle, the owner's cancel fails with InvalidTransition.
  • After a cancel, the judge's settle fails: settleProof throws NotSettleable before it sends, and the engine refuses the same transaction with InvalidTransition.
  • Anyone can expire an open intent at or after its expiry, which returns the escrow to the owner's token account and pays the expirer the crank bounty; before the expiry it fails with NotExpired.
  • Anyone can close a filled, cancelled or expired intent, which returns the rent of the intent and its escrow account to the wallet that paid it, the owner for createIntent; the closer earns the crank bounty the intent still holds, except on a cancelled intent, where it goes to that wallet too.
  • Closing an open intent fails with InvalidTransition.

Costs ​

createIntent pays from the signer's wallet:

  • the rent of its user account, once, which stays open;
  • the rent of the intent and its escrow account, and of any token account it creates;
  • the escrow itself;
  • the create fee, which does not come back;
  • the crank bounty, which the intent holds until someone expires or closes it.

cancel returns the escrow, close returns the rent, and unwrap turns wrapped SOL back into SOL. This prints the create fee and the crank bounty in SOL.

ts
const { createFee, crankBounty } = configSummary(await client.config());
console.log(formatAmount(createFee, sell.decimals), formatAmount(crankBounty, sell.decimals));

Test offline ​

svmTransport(svm) from @intent-engine/client/svm runs the client against a local LiteSVM instance instead of a cluster: pass it as createClient({ url, transport }). It needs the litesvm package, which ships for Linux and macOS only; on Windows, run it in WSL.

Next ​