Client developers

You are building something that talks to dClutch — a bot, a dashboard, an integration, a tool of your own. This guide shows you the pieces you build with and one working example of each core flow. The exact tables — every account layout, every instruction, every error code — live in the generated reference; this page shows you the code.

Two packages in this repository are yours:

Neither is on npm yet. Inside the repository, depend on the SDK with "@dclutch/sdk": "file:../../packages/dclutch-sdk" and import modules directly: @dclutch/sdk/rpc, @dclutch/sdk/marketDiscovery, and so on.

Reading markets

Everything starts with a SolanaRpcClient and a set of program ids. Program ids come from whatever run you are targeting — a local run spec, or the session file dclutch found writes.

import { SolanaRpcClient } from '@dclutch/sdk/rpc';
import {
  enumerateCoreMarketAddressesV1,
  inspectMarketDiscoveryV1,
} from '@dclutch/sdk/marketDiscovery';

const client = new SolanaRpcClient('http://127.0.0.1:19790/');
const found = await enumerateCoreMarketAddressesV1(client, CORE_PROGRAM_ID);
const discovery = await inspectMarketDiscoveryV1(client, {
  coreProgramId: CORE_PROGRAM_ID,
  addresses: found.addresses,
});
for (const card of discovery.cards) {
  console.log(card.address, card.status === 'decoded' ? card.phase : card.refusal);
}

A card decodes fully or comes back as refused with a reason. A refusal here means the account did not match the layout exactly — treat it as "not a market I can use", not as an error to retry.

All reads happen at one finalized slot, so the numbers you show a user are from a single consistent snapshot. inspectMarketDetailV1 (@dclutch/sdk/marketDetail) gives you one market in full; inspectPortfolioV1 (@dclutch/sdk/portfolio) derives a user's positions directly from market addresses — no indexer involved.

Buying and selling

A Direct trade settles between two signed intents: one seller, one buyer, crossed at one execution price. Your client plays one side and needs the other side's signed intent (or plays both sides, in a bench).

import {
  compileDirectInlineTransactionV3,
  encodeCompactIntentSigningMessageV2,
} from '@dclutch/sdk/directInlineV3';
import { inspectDirectHotRouteV3 } from '@dclutch/sdk/directHotChain';
import nacl from 'tweetnacl';

// 1. Acquire the route: the market's fixed trading accounts, read back and
//    checked at one finalized slot.
const inspection = await inspectDirectHotRouteV3(client, routeManifest);

// 2. Build and sign your intent (here: buying outcome 1, up to 5 units,
//    limit price in the market's own scale).
const intent = {
  side: 1, lifecycle: 0, outcome: 1,
  market: inspection.route.market,
  generation: inspection.route.generation,
  nonce: 1n,
  validFrom: BigInt(inspection.observedSlot),
  validThrough: BigInt(inspection.observedSlot) + 150n,
  maximumFill: 5n, limitPrice: 400_000n,
  feeBasisPoints: inspection.route.feeBasisPoints,
  collateralAccount: MY_COLLATERAL_ACCOUNT,
} as const;
const signature = nacl.sign.detached(
  encodeCompactIntentSigningMessageV2(intent), myKeypair.secretKey);

// 3. Cross it with the counterparty's signed intent and submit.
const plan = compileDirectInlineTransactionV3({
  route: inspection.route,
  seller: theirSignedIntent,
  buyer: { maker: myKeypair.publicKey.toBase58(), signature, intent },
  fill: 5n, executionPrice: 400_000n,
  clockSlot: BigInt(inspection.observedSlot),
});
plan.transaction.sign([payerKeypair]);
await client.sendRawTransaction(plan.transaction.serialize());

plan.preview tells you the exact collateral debit, credit, and fees before anything is signed — show it to your user. The compiler refuses anything the chain would refuse (an intent that does not admit the fill, a price outside either limit, a stale slot), so a transaction that compiles is one the program will actually consider.

The CLI's intent / buy / sell commands are this exact code plus a JSON file format for handing intents between machines.

Redeeming

Redemption has two steps, and today your client can perform one of them. The step you can build: create the market's Claims-role Custody replay, which every payout needs to exist first.

import { inspectClaimsCustodyReplayV1 } from '@dclutch/sdk/claimsCustodyReplay';

const state = await inspectClaimsCustodyReplayV1(client, {
  marketAddress, claimsProgramId, custodyProgramId, registryProgramId,
  payer: myKeypair.publicKey.toBase58(),
});
if (state.status === 'creatable') {
  state.plan.transaction.sign([myKeypair]);
  await client.sendRawTransaction(state.plan.transaction.serialize());
}

The payout instruction itself is not yet callable from a wallet: the program only accepts it from Core or Trading (decision 0008). Say that to your user rather than looping on a transaction that cannot land; PLAIN_POSITION_PAYOUT_BLOCK_V1 in the same module is ready-made copy.

Founding a market

Founding is driven by a run spec — a JSON file naming the programs, the market recipe, and where the evidence goes. The producer binary does the work; wrap it the way dclutch found does (packages/dclutch-cli/src/commands/found.ts), or start from tools/gauntlet/run.sh, which assembles a spec end to end. What you get back is a running local validator with an open market and an evidence file whose accounts map names everything the market is made of.

When a transaction fails

A failed dClutch transaction carries a custom program error code, and the code alone tells you which program refused: code >> 12 is the program's band (codes below 0x1000 are some other program's — SPL Token, the loader). The SDK turns any code into a name and a sentence:

import { renderRefusal, customCodeFromTransactionError } from '@dclutch/sdk/refusals';

const code = customCodeFromTransactionError(rpcError); // {"InstructionError":[0,{"Custom":n}]}
if (code !== null) console.error(renderRefusal(code).text);
// -> claims refused: ClaimsSbfError::Instruction (0x5000) — Instruction
//    bytes were hostile or selected no supported family.

Always surface the rendered name. Your user can act on "claims refused: identity did not join the packet"; they cannot act on 0x5002. The full table is refusals.md.

The layouts are generated — treat them that way

Every byte offset, magic, and account table in @dclutch/sdk under lib/generated/ is emitted from the protocol's own source and checked byte-for-byte by npm test in the SDK package. If you need a layout the SDK does not export yet, add a generator the way the existing eighteen work (packages/dclutch-sdk/scripts/) instead of typing offsets in — hand-typed offsets are how a client ends up confidently wrong about what an account says. The ABI tables are the same data, rendered for reading.