The SDK types itself from the platform
The client keeps its namespaced shape with types from the platform's own routes, one flat argument per call, a bounded wait on writes, and query factories.
The client still reads the way it did: dalp.api.token.read({ assetId }). What changed is everything behind that. Types now come from the platform's own route declaration rather than a contract maintained beside it. A call takes one flat object instead of a transport envelope, and a write asks the platform to hold the response, resolving with the settled resource or with the handle to follow.
One declaration, one client, no second contract
In 3.1 the surface was an oRPC client over a contract router shipped inside the SDK, which described the API a second time and let the two copies disagree. The tree is now generated from the same declaration that produces the OpenAPI document, so a route the platform gains appears in your editor after an upgrade rather than after someone extends a contract.
- Call surface
- Namespaced, as before
- Types from
- The platform declaration
- Routes reachable
- All of them
- Interpreters
- Promise and Effect
One flat argument
A 3.1 call carried the transport in its argument, so where a field went was your problem at every call site.
3.1
await dalp.token.read({ params: { tokenAddress } });
await dalp.token.list({ query: {} });3.2
await dalp.api.token.read({ assetId });
await dalp.api.token.list();You name what you mean. The route declaration says which keys belong in the URL and which in the body, so the split is a lookup rather than something you spell out. A missing path parameter is caught at the call with the parameter and the route named, and a route whose every field is optional takes no argument at all.
The build refuses any route whose path parameter would collide with a body or query key, which is what makes merging them safe rather than merely convenient.
A write returns settled or hands you a handle
A write here produces a real on-chain transaction, and the platform answers either with the settled result or with a 202 handle to follow. In 3.1 the SDK gave you waitForTransaction and you called it yourself once you recognised the accepted envelope. Every integration wrote that recognition slightly differently.
3.2 makes the two answers one shape. The call sends a bounded Prefer wait, so a write that settles inside it resolves settled; one that outlives it resolves with { transactionId, statusUrl } rather than throwing.
const result = await dalp.api.token.transfer({
assetId,
transfers: [{ recipient, amount }],
});
if ("transactionId" in result) {
// Still in flight. Follow it -- do not resubmit.
await waitForTransaction(dalp, result.transactionId);
}The branch is the point. A wait that runs out is not a failure, and treating it as one tells a user their transfer failed while it is still on its way to the chain. The handle carries the transaction id and the status URL precisely so the caller can say "live" instead of "failed", and follow it with waitForTransaction or statusStream on its own budget.
TanStack Query, without a second module
3.1 shipped no query integration, so every application wrote its own cache keys. Query and mutation factories now sit on the route nodes, beside the call:
const { data } = useQuery(dalp.api.token.list.queryOptions({ page: { limit: 50 } }));The key is the route id plus canonicalized input. Two callers passing the same parameters in different key order therefore share one cache entry rather than fetching twice, and invalidating a route after a write is a prefix match. Nothing about the key is hand-written, which removes the class of bug where one character of difference silently becomes a second cache entry.
An Effect surface beside the promise one
Code written in Effect can import @settlemint/dalp-sdk/effect and get the same tree returning Effect values, with a tagged failure channel and interruption wired to the request rather than stopping at the await. The promise entry stays provably free of Effect, enforced by a build gate that walks the import graph, so the root SDK can be used without Effect.
Server rendering has a defined shape
Rendering on a server means deciding whether the render acts as the signed-in user or as your service. Forwarding the user's session is now a first-class option with the constraints that capability needs: destinations are allowlisted, redirects lose credentials the moment they leave the boundary, and a cookie-authenticated write is refused without a trusted origin.
TanStack Start, the Next.js App Router, server components, route handlers, and server actions are each documented with the wiring they need, including hydrating the browser cache from what the server already fetched.
What existing integrations must change
The namespace path is largely unchanged, so most call sites move rather than being rewritten. There is no compatibility layer, so plan the change as one piece of work.
| In 3.1 | In 3.2 |
|---|---|
createDalpClient({ url, apiKey, organizationId }) | createDalpClient({ baseUrl, apiKey, context: { organizationId } }) |
dalp.token.read({ params: { tokenAddress } }) | dalp.api.token.read({ assetId: tokenAddress }) |
dalp.token.list({ query: {} }) | dalp.api.token.list() |
dalp.token.create({ body: { ... } }) | dalp.api.token.create({ ... }) |
client.token.create(input, { context: { idempotencyKey } }) | dalp.api.token.create(input, { headers: { "Idempotency-Key": key } }) |
waitForTransaction(client, ...) after an accepted envelope | Nothing to call by default. The write sends a bounded Prefer wait and returns the settled resource or a 202 handle; follow the handle yourself with waitForTransaction or statusStream. |
requestValidation and responseValidation | Nothing to configure. Every success response is checked against the route's declared response, and a malformed body throws sdk.response.MALFORMED_SUCCESS. Input is not validated by the SDK; validate it with whatever your application already uses. |
dalp.token.list({ query: { page: { limit: 50 }, sort: "name" } }) | dalp.api.token.list({ page: { limit: 50 }, sort: "name" }) |
| Hand-written TanStack query keys | dalp.api.<route>.queryOptions(...), or queryKeyFor(routeId, input). |
Five changes need review during migration.
Assets are named by assetId. The token routes take the asset's address as assetId where 3.1 called it tokenAddress. The value is the same; rename the key at each call site.
Where the acting context lives. In 3.1, organizationId and idempotencyKey were client-level options, and idempotencyKey also had a per-call context override. They are headers now: the steady-state ones go in context at construction, and a single call overrides them through headers.
Idempotency keys on retried writes. Retain one key across all attempts to submit the same instruction. Generate a new key only for a new instruction. A header function runs on every request, so retain a retry key outside the function instead of generating it again.
Collection parameters. A list call takes page: { limit, offset }, sort, and filter. Prefix the sort field with - for descending order.
Settlement bounds. 3.1 sent Prefer: wait=99 on every mutation and left the rest to you. The SDK now derives the same header from settlementTimeoutMs, undefined asks for 15 seconds, and hands back whatever the platform answers, settled or a 202 handle. Set it to what your caller can actually wait for; the mutation factories already set a bound of their own.
Availability and compatibility
The SDK is published as @settlemint/dalp-sdk on npm and runs on Node 20 or later, on Bun, and in the browser. @tanstack/react-query and effect are optional peer dependencies; a consumer that uses neither resolves neither.
The Platform API itself is unchanged by this release. Every route, header contract, and error identifier the 3.1 client called is still there and still behaves the same way, so a migration is a change to your code rather than a coordinated change with the platform.
Start at the TypeScript SDK reference.
For a call-by-call route, argument, response, and removal map, see the 3.1 to 3.2 SDK migration mapping.
Index correctness
The Ledger Index now follows blocks by identity rather than height so a reorganization cannot mix data, and an inexact price is refused instead of rounded.
SDK migration mapping
A call-by-call map from the 3.1 TypeScript SDK to the 3.2 namespaced routes, flat arguments, narrowed responses, and retained client interpreters.