Use this page when your app does not sign Thru transactions directly in the first-party wallet UI.
Typical external signer integrations include:
- custody providers
- HSM or KMS-backed signing services
- backend transaction services
- custom wallet adapters
- embedded wallet providers
Mental Model
Treat the Thru transaction lifecycle as four separate steps:
- Build the transaction payload.
- Hand the signing payload to the external signer.
- Receive the signed wire transaction back.
- Submit and track the transaction with Thru RPC.
External signers are responsible for the outer Thru transaction signature only. Some programs, such as the Passkey Manager Program, may also require an inner authorization payload, but that does not replace the outer transaction signature.
Canonical Flow
For external signers, the recommended SDK flow is:
- Build the transaction with
thru.transactions.build(...).
- Produce a signing payload with
tx.toWireForSigning().
- Pass that serialized payload to your signer.
- Receive the signed wire transaction from the signer.
- Submit it with
thru.transactions.send(...) or thru.transactions.sendAndTrack(...).
This mirrors the wallet-facing signTransaction(serializedTransaction) contract used by Thru wallet integrations.
What The Signer Must Preserve
Once a signing payload has been produced, the signer should treat the transaction contents as fixed.
In particular, do not mutate:
- fee payer public key
- program public key
- account ordering
- instruction data
- nonce
- chain ID
- validity window fields such as
start_slot and expiry_after
- requested resource limits
If any of those values need to change, rebuild the transaction and generate a new signing payload.
Signing Contract
The current web wallet contract is:
- input: base64-encoded serialized transaction payload
- output: signed base64-encoded wire transaction payload
Thru transaction headers include fields such as:
fee_payer_signature
nonce
start_slot
expiry_after
fee_payer_pubkey
program_pubkey
chain_id
In practice, this means the external signer should be given the final signing payload after the app or backend has already selected the fee payer, program, accounts, instruction bytes, and validity settings.
Generic TypeScript Example
import { createThruClient } from "@thru/thru-sdk/client";
import { decodeAddress } from "@thru/helpers";
type ExternalSigner = {
signTransaction: (serializedTransaction: string) => Promise<string>;
};
const thru = createThruClient({
baseUrl: "https://grpc-web.alphanet.thruput.org",
});
async function sendWithExternalSigner(
signer: ExternalSigner,
feePayerAddress: string,
programAddress: string,
readWriteAccounts: string[],
readOnlyAccounts: string[],
instructionData: Uint8Array
) {
const tx = await thru.transactions.build({
feePayer: { publicKey: decodeAddress(feePayerAddress) },
program: programAddress,
accounts: {
readWrite: readWriteAccounts,
readOnly: readOnlyAccounts,
},
instructionData,
});
const serialized = tx.toWireForSigning();
const signedWire = await signer.signTransaction(serialized);
return await thru.transactions.sendAndTrack(signedWire);
}
When To Use buildAndSign
Use transactions.buildAndSign(...) only when your signing implementation already lives inside your Thru wallet or SDK layer.
If the signer is outside that layer, prefer:
transactions.build(...)
tx.toWireForSigning()
- external
signTransaction(...)
transactions.send(...)
That keeps the signing boundary explicit and makes it easier to integrate a custody or backend signing service.
Passkey-Managed Flows
Passkey-managed flows add an inner authorization step, but the outer signing model stays the same.
The high-level sequence is:
- Fetch or derive the wallet nonce.
- Build the trailing instruction payload you want the wallet to authorize.
- Create the passkey challenge from the nonce, ordered accounts, and trailing instruction bytes.
- Produce the
validate instruction from the WebAuthn result.
- Concatenate
validate and the trailing instruction payload.
- Build the outer Thru transaction.
- Sign the outer transaction with the external signer.
- Submit the signed wire transaction.
Use @thru/passkey-manager when you need helpers for wallet address derivation, account ordering, nonce fetching, challenge generation, or validate instruction encoding.
Common Failure Modes
Watch for these issues when integrating an external signer:
- stale fee payer nonce
- expired transaction validity window
- wrong chain ID
- mismatched fee payer key
- account reordering after the signing payload was generated
- modifying instruction data after the signer has already approved the payload
- confusing inner program authorization with the outer Thru transaction signature
Recommended Integration Boundary
For most teams, the cleanest split looks like this:
- app or backend: resolves accounts, instruction bytes, fee payer, and validity rules
- external signer: signs the serialized transaction payload
- app or backend: submits the signed wire transaction and tracks status
This keeps policy, custody, and audit responsibilities inside the signer while leaving chain-specific transaction assembly in the Thru integration layer.