Wallet SDK Reference
Complete API reference for @intentguard/sdk. For an integration walkthrough, see the Integration Guide.
Configuration
Constructor
The only required parameter is chainId. All other parameters have defaults:
import { IntentGuardClient } from "@intentguard/sdk";
const client = new IntentGuardClient({ chainId: 1 });
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
chainId | number | Yes | — | EVM chain ID (e.g. 1 for mainnet, 11155111 for Sepolia) |
rpcUrl | string | No | "https://rpc.intentguard.xyz" | IntentGuard RPC endpoint |
enforcerType | EnforcerType | No | "balance" | Enforcer to use for constraint verification (see Enforcer types) |
defaultValidityBlocks | number | No | 5 | Blocks ahead to use as validUntilBlock when not provided. Applies to delegated mode only. |
minValidityBlocks | number | No | 2 | Minimum acceptable validUntilBlock - currentBlock. Requests below this threshold are rejected client-side with INVALID_VALIDITY_WINDOW. |
timeoutMs | number | No | No timeout | Request timeout in milliseconds per RPC call |
fetch | FetchFn | No | Global fetch | Custom fetch implementation |
All JSON-RPC calls go to rpcUrl. The SDK attaches an X-IntentGuard-SDK header with the package version on every request.
Environment variables
For Node.js or server-side deployments. In browser environments, pass config via the constructor instead.
import { IntentGuardClient, fromEnv } from "@intentguard/sdk";
const client = new IntentGuardClient(fromEnv());
fromEnv() returns an IntentGuardConfig object. It throws if INTENTGUARD_CHAIN_ID is not set or is not a positive integer.
| Variable | Required | Default | Description |
|---|---|---|---|
INTENTGUARD_CHAIN_ID | Yes | — | Chain ID (integer) |
INTENTGUARD_RPC_URL | No | https://rpc.intentguard.xyz | IntentGuard RPC endpoint |
INTENTGUARD_ENFORCER_TYPE | No | balance | Enforcer type for constraint verification |
Types
BalanceTarget
Lightweight account/token pair whose balance is snapshotted before the user's transaction executes.
interface BalanceTarget {
account: string;
token: string;
}
ProtectedConstraint
Enforcement rule verified after execution against pre-transaction snapshots.
interface ProtectedConstraint {
account: string;
token: string;
maxOutflow: string;
minInflow: string;
}
All amount fields are decimal strings in the token's base units (amount × 10^decimals).
EthereumSigner
Signer interface compatible with ethers.js v6 Signer and viem WalletClient adapters.
interface EthereumSigner {
signTypedData(
domain: TypedDataDomain,
types: Record<string, TypedDataField[]>,
value: Record<string, unknown>,
): Promise<string>;
}
UnsignedEnforcementTx
Returned by prepareSelfSignedProtection, buildPreSelfSignedTx, and buildPostSelfSignedTx.
interface UnsignedEnforcementTx {
to: string;
data: string;
value: "0";
}
SelfSignedTxPayload
Input to submitSelfSignedTransaction.
interface SelfSignedTxPayload {
preTx: string;
userTx: string;
postTx: string;
retryUntilBlock: number;
}
Constants
| Constant | Value | Description |
|---|---|---|
NATIVE_ETH | "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" | Sentinel address for native ETH (EIP-7528) |
DEFAULT_VALIDITY_BLOCKS | 5 | Default validUntilBlock offset |
DEFAULT_MIN_VALIDITY_BLOCKS | 2 | Minimum acceptable validity window |
DEFAULT_RPC_URL | "https://rpc.intentguard.xyz" | Default RPC endpoint |
DEFAULT_ENFORCER_TYPE | "balance" | Default enforcer type |
PRE_SELF_SIGNED_SELECTOR | 4-byte selector | preSelfSigned function selector |
POST_SELF_SIGNED_SELECTOR | 4-byte selector | postSelfSigned function selector |
Self-signed mode API
prepareSelfSignedProtection
Build both unsigned enforcement transactions in a single call.
const { preTx, postTx } = await client.prepareSelfSignedProtection(
targets, // BalanceTarget[]
constraints, // ProtectedConstraint[]
validUntilBlock, // number
options?, // { enforcerType? }
);
Returns: { preTx: UnsignedEnforcementTx, postTx: UnsignedEnforcementTx }
buildPreSelfSignedTx
Build the unsigned pre-enforcement transaction only.
const preTx = await client.buildPreSelfSignedTx(
targets, // BalanceTarget[]
validUntilBlock, // number
options?, // { enforcerType? }
);
Returns: UnsignedEnforcementTx
buildPostSelfSignedTx
Build the unsigned post-enforcement transaction only.
const postTx = await client.buildPostSelfSignedTx(
constraints, // ProtectedConstraint[]
options?, // { enforcerType? }
);
Returns: UnsignedEnforcementTx
submitSelfSignedTransaction
Submit a self-signed enforcement bundle.
const txHash = await client.submitSelfSignedTransaction({
preTx: signedPreTx, // 0x-prefixed signed raw transaction
userTx: signedUserTx, // 0x-prefixed signed raw transaction
postTx: signedPostTx, // 0x-prefixed signed raw transaction
retryUntilBlock, // number
});
Returns: Promise<string> — the user's transaction hash (0x-prefixed).
Client-side validation:
- All three transaction strings must be non-empty and
0x-prefixed. - All three transactions must be signed by the same address (recovered via ECDSA).
- Only EIP-1559 (type 2) transactions are supported.
Low-level calldata encoding
For integrations that build transactions outside the SDK:
import {
encodePreSelfSignedCalldata,
encodePostSelfSignedCalldata,
PRE_SELF_SIGNED_SELECTOR,
POST_SELF_SIGNED_SELECTOR,
} from "@intentguard/sdk";
// preSelfSigned: selector + ABI-encoded (targets[], validUntilBlock)
const preData = encodePreSelfSignedCalldata(targets, validUntilBlock);
// postSelfSigned: selector + ABI-encoded (constraints[])
const postData = encodePostSelfSignedCalldata(constraints);
Address recovery utility
import { recoverTxSender } from "@intentguard/sdk";
const signer = recoverTxSender(signedRawTx); // checksummed address
Delegated mode API
Note: Delegated mode is available on testnet only.
submitProtectedTransaction
Submit a protected transaction with in-process constraint signing.
const txHash = await client.submitProtectedTransaction(
rawTx, // 0x-prefixed signed raw transaction
constraints, // ProtectedConstraint[] (1 to 10 constraints)
signer, // EthereumSigner with signTypedData() and getAddress()
options?, // optional: { validUntilBlock?, enforcerType? }
);
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
rawTx | string | Yes | — | 0x-prefixed signed raw transaction hex |
constraints | ProtectedConstraint[] | Yes | — | 1 to 10 balance constraints to enforce |
signer | EthereumSigner | Yes | — | Account that signed rawTx. Must implement signTypedData() and getAddress(). Must be the same account that signed the raw transaction. |
options.validUntilBlock | number | No | currentBlock + defaultValidityBlocks | Block number after which the request expires. |
options.enforcerType | EnforcerType | No | Client-level enforcerType | Override the enforcer type for this call only. |
Returns: Promise<string> — the transaction hash (0x-prefixed).
submitProtectedTransactionWithRetry
Convenience wrapper that retries on PROTECTED errors, waiting for the next block between attempts.
const txHash = await client.submitProtectedTransactionWithRetry(
rawTx,
constraints,
signer,
{
retryUntilBlock: currentBlock + 10,
onRetry: ({ attempt, retryAfterBlock, error }) => {
console.log(`Attempt ${attempt}: retrying at block ${retryAfterBlock}`);
},
},
);
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
rawTx | string | Yes | — | 0x-prefixed signed raw transaction hex |
constraints | ProtectedConstraint[] | Yes | — | 1 to 10 balance constraints to enforce |
signer | EthereumSigner | Yes | — | Account that signed rawTx. |
options.retryUntilBlock | number | Yes | — | Block at which retries stop. Also sent as validUntilBlock. |
options.enforcerType | EnforcerType | No | Client-level enforcerType | Override enforcer type for this call. |
options.signal | AbortSignal | No | No signal | Abort signal for cancellation. Throws with code ABORTED. |
options.onRetry | function | No | No callback | Called after each PROTECTED error. Receives { attempt, retryAfterBlock, error }. |
Returns: Promise<string> — the transaction hash on success.
Throws: RetryExhaustedError when the block window expires. Inspect error.attempts, error.errors (full list), and error.lastError for debugging.
The retry loop stops when:
- The transaction succeeds (returns the tx hash)
- A non-PROTECTED error is thrown — rethrown immediately
- The next block exceeds
retryUntilBlock— throwsRetryExhaustedError - The
signalis aborted — throws with codeABORTED
buildConstraintMessage
Produce EIP-712 typed data for external signing. Use when the signer is external (hardware wallet, browser extension, custody provider).
const msg = await client.buildConstraintMessage(
rawTx, // 0x-prefixed signed raw transaction
constraints, // ProtectedConstraint[] (1 to 10 constraints)
userAddress, // address of the transaction sender
options?, // optional: { validUntilBlock?, enforcerType? }
);
Returns:
domain— EIP-712 domain (name, version, chainId, verifyingContract)types— EIP-712 type definitionsvalue— EIP-712 message value (user, txHash, validUntilBlock, constraintsHash)primaryType—"SignedConstraints"validUntilBlock— the resolved block number (pass this tosubmitPreSignedTransaction)
submitPreSignedTransaction
Submit with a pre-signed constraint signature (produced externally via buildConstraintMessage).
const txHash = await client.submitPreSignedTransaction(
rawTx, // 0x-prefixed signed raw transaction
constraints, // ProtectedConstraint[] (same as passed to buildConstraintMessage)
constraintSignature, // EIP-712 signature from external signer
{ validUntilBlock }, // must match what was signed
);
Returns: Promise<string> — the transaction hash.
Advanced signing primitives
For low-level access to the signing primitives (custom hashing, direct contract interaction):
import {
signConstraints,
computeConstraintsHash,
buildConstraintsTypedData,
IntentGuardClient,
} from "@intentguard/sdk";
const client = new IntentGuardClient({ chainId });
const { contractAddress } = await client.getEnforcerConfig();
import { keccak256 } from "viem";
const txHash = keccak256(rawTx as `0x${string}`);
const constraintsHash = computeConstraintsHash(constraints);
const { domain, types, value } = buildConstraintsTypedData(
userAddress, txHash, validUntilBlock, constraintsHash, chainId, contractAddress,
);
// Or sign directly with an in-process signer
const signature = await signConstraints(
signer, userAddress, txHash, validUntilBlock,
constraints, chainId, contractAddress,
);
Ethereum read methods
The IntentGuard RPC exposes standard Ethereum read methods:
const blockNumber = await client.getBlockNumber();
const balance = await client.getBalance("0xAddress");
const nonce = await client.getTransactionCount("0xAddress");
const gasPrice = await client.getGasPrice();
const receipt = await client.getTransactionReceipt("0xTxHash");
Available: getBlockNumber, getChainId, getBalance, getTransactionCount, getCode, getGasPrice, getMaxPriorityFeePerGas, getFeeHistory, estimateGas, call, getBlockByNumber, getBlockByHash, getSyncing, getTransactionByHash, getTransactionReceipt, getLogs.
Enforcer types
IntentGuard supports pluggable on-chain enforcement logic. Each enforcer type maps to a verification contract resolved automatically by the backend.
| Type | Description |
|---|---|
"balance" | Verifies token inflows and outflows per account against declared constraints. Default. |
// These are equivalent:
const client = new IntentGuardClient({ chainId: 1 });
const client = new IntentGuardClient({ chainId: 1, enforcerType: "balance" });
The enforcer config is fetched once per type and cached for the lifetime of the client. Additional enforcement strategies will be introduced in future releases.
getEnforcerConfig
const config = await client.getEnforcerConfig(enforcerType?);
// config.contractAddress — enforcer contract address
Validation rules
| Condition | Result |
|---|---|
validUntilBlock <= current_block | Rejected with REQUEST_EXPIRED |
validUntilBlock - current_block < 2 | Rejected with REQUEST_EXPIRED: insufficient time for inclusion |
validUntilBlock - current_block > 1000 | Rejected with VALIDITY_WINDOW_EXCEEDED |
| All conditions pass | Accepted |
Validity window cap
IntentGuard enforces an upper bound on how far ahead validUntilBlock can be set. The default cap is 1000 blocks (~3.3 hours on mainnet at 12s blocks). Requests exceeding this limit are rejected with VALIDITY_WINDOW_EXCEEDED.
This cap may be adjusted for use cases that require longer validity windows. Contact IntentGuard to discuss higher caps for your deployment.
JSON-RPC reference
The SDK calls intentguard_sendProtectedTransaction over JSON-RPC. The endpoint supports two modes selected by the mode field in the params object.
Self-signed mode (mode: "self-signed"):
{
"jsonrpc": "2.0",
"method": "intentguard_sendProtectedTransaction",
"params": [{
"mode": "self-signed",
"chainId": 11155111,
"preTx": "0x<signedPreTx>",
"userTx": "0x<signedUserTx>",
"postTx": "0x<signedPostTx>",
"retryUntilBlock": 12345678
}],
"id": 1
}
Delegated mode (mode: "managed", testnet only):
{
"jsonrpc": "2.0",
"method": "intentguard_sendProtectedTransaction",
"params": [{
"mode": "managed",
"chainId": 11155111,
"enforcerType": "balance",
"rawTx": "0x<signedRawTx>",
"validUntilBlock": 12345678,
"constraints": [
{
"account": "0x...",
"token": "0x...",
"maxOutflow": "1000000000",
"minInflow": "0"
}
],
"constraintSignature": "0x<eip712Signature>"
}],
"id": 1
}
Both modes return a JSON-RPC response with the user's transaction hash as the result.
EIP-712 domain
| Field | Value |
|---|---|
name | "IntentGuard" |
version | "1" |
chainId | Network chain ID |
verifyingContract | Resolved from enforcer type via getEnforcerConfig() |
Error reference
All errors extend IntentGuardError with a stable code property.
import { isIntentGuardError, IntentGuardErrorCode } from "@intentguard/sdk";
Error classes
| Class | Description |
|---|---|
IntentGuardError | Base class for all SDK errors |
InvalidConstraintsError | Constraint validation failed |
InvalidValidityWindowError | Validity window check failed |
IntentGuardNetworkError | Network/RPC communication failure |
RetryExhaustedError | Retry loop exhausted (has attempts, errors, lastError) |
SelfSignedValidationError | Self-signed bundle validation failed |
Exhaustive error handling
try {
const txHash = await client.submitSelfSignedTransaction(bundle);
} catch (err) {
if (!isIntentGuardError(err)) throw err;
switch (err.code) {
// ── Client-side ────────────────────────────────────────────
case IntentGuardErrorCode.INVALID_CONSTRAINTS:
case IntentGuardErrorCode.INVALID_VALIDITY_WINDOW:
break;
// ── Transaction layer ──────────────────────────────────────
case IntentGuardErrorCode.INVALID_TRANSACTION:
case IntentGuardErrorCode.CONSTRAINTS_VALIDATION_ERROR:
case IntentGuardErrorCode.INVALID_USER_TX:
case IntentGuardErrorCode.NONCE_TOO_HIGH:
break;
// ── Constraint layer ───────────────────────────────────────
case IntentGuardErrorCode.CONSTRAINTS_EMPTY:
case IntentGuardErrorCode.CONSTRAINTS_LIMIT_EXCEEDED:
case IntentGuardErrorCode.INVALID_TOKEN_ADDRESS:
case IntentGuardErrorCode.INVALID_ACCOUNT_ADDRESS:
break;
// ── EIP-712 signature layer (delegated mode) ───────────────
case IntentGuardErrorCode.INVALID_CONSTRAINT_SIGNATURE:
case IntentGuardErrorCode.SIGNER_MISMATCH:
case IntentGuardErrorCode.CHAIN_MISMATCH:
case IntentGuardErrorCode.REQUEST_EXPIRED:
case IntentGuardErrorCode.VALIDITY_WINDOW_EXCEEDED:
break;
// ── Simulation layer ───────────────────────────────────────
case IntentGuardErrorCode.PROTECTED:
break;
// ── Submission layer ───────────────────────────────────────
case IntentGuardErrorCode.NETWORK_ERROR:
case IntentGuardErrorCode.SUBMISSION_FAILED:
case IntentGuardErrorCode.RPC_ERROR:
case IntentGuardErrorCode.SERVICE_UNAVAILABLE:
break;
// ── Configuration layer ────────────────────────────────────
case IntentGuardErrorCode.UNKNOWN_ENFORCER_TYPE:
break;
// ── Self-signed layer ──────────────────────────────────────
case IntentGuardErrorCode.MISSING_SIGNED_TX:
case IntentGuardErrorCode.SIGNER_MISMATCH_SELF_SIGNED:
case IntentGuardErrorCode.INVALID_TX_ORDERING:
case IntentGuardErrorCode.INVALID_NONCE_SEQUENCE:
case IntentGuardErrorCode.WRONG_ENFORCEMENT_CONTRACT:
break;
// ── Client-side control flow ───────────────────────────────
case IntentGuardErrorCode.TIMEOUT:
case IntentGuardErrorCode.ABORTED:
case IntentGuardErrorCode.RETRY_EXHAUSTED:
break;
default:
throw err;
}
}
All error codes
| Code | Layer | Retryable | Description |
|---|---|---|---|
| Client-side | |||
INVALID_CONSTRAINTS | Client | No | Invalid addresses, negative amounts, or exceeds the 1–10 limit. |
INVALID_VALIDITY_WINDOW | Client | No | validUntilBlock is in the past or too close to the current block. |
NETWORK_ERROR | Client | Yes | RPC endpoint unreachable. |
TIMEOUT | Client | No | Operation timed out. |
ABORTED | Client | No | Operation was cancelled via AbortSignal. |
RETRY_EXHAUSTED | Client | No | Retry loop exhausted all attempts. |
| Transaction layer | |||
INVALID_TRANSACTION | Server | No | Raw transaction is malformed, empty, or cannot be decoded. |
CONSTRAINTS_VALIDATION_ERROR | Server | No | Constraint verification failed (generic). |
INVALID_USER_TX | Server | No | The user-submitted transaction is invalid. |
NONCE_TOO_HIGH | Server | No | Transaction nonce too high; pending transactions must confirm first. |
| Constraint layer | |||
CONSTRAINTS_EMPTY | Server | No | No constraints provided. |
CONSTRAINTS_LIMIT_EXCEEDED | Server | No | More than 10 constraints provided. |
INVALID_TOKEN_ADDRESS | Server | No | Invalid token address in a constraint. |
INVALID_ACCOUNT_ADDRESS | Server | No | Invalid account address in a constraint (e.g. zero address). |
| EIP-712 signature layer (delegated mode) | |||
INVALID_CONSTRAINT_SIGNATURE | Server | No | EIP-712 constraint signature is malformed. |
SIGNER_MISMATCH | Server | No | Recovered EIP-712 signer does not match the transaction sender. |
CHAIN_MISMATCH | Server | No | Chain ID in the EIP-712 domain does not match the transaction chain ID. |
REQUEST_EXPIRED | Server | No | validUntilBlock has been reached. |
VALIDITY_WINDOW_EXCEEDED | Server | No | validUntilBlock is too far ahead. See Validity window cap. |
| Simulation layer | |||
PROTECTED | Server | Yes (state-dependent) | Constraints were violated. Not included on-chain. May succeed next block. |
| Submission layer | |||
SUBMISSION_FAILED | Server | Yes | Transaction could not be submitted to the network. |
RPC_ERROR | Server | Yes | Upstream RPC endpoint returned an error. |
SERVICE_UNAVAILABLE | Server | Yes | Service temporarily unavailable. Retry with backoff. |
| Configuration layer | |||
UNKNOWN_ENFORCER_TYPE | Server | No | Unknown enforcer type. Check supportedTypes in the error details. |
| Self-signed layer | |||
MISSING_SIGNED_TX | Client/Server | No | A required signed transaction (preTx, userTx, or postTx) is missing. |
SIGNER_MISMATCH_SELF_SIGNED | Client | No | The three self-signed transactions are not signed by the same address. |
INVALID_TX_ORDERING | Server | No | Nonce ordering is incorrect (must be sequential: N, N+1, N+2). |
INVALID_NONCE_SEQUENCE | Server | No | Nonces are not consecutive or do not match expected sequence. |
WRONG_ENFORCEMENT_CONTRACT | Server | No | Pre or post transaction does not target the expected enforcement contract. |
Passthrough RPC failures are reported as RPC_ERROR_<code> (e.g. "RPC_ERROR_-32603").
Operational notes
- Set
timeoutMsfor deterministic request timeouts. If omitted, OS and runtime defaults apply. - The SDK enforces a minimum validity window of 2 blocks by default (
DEFAULT_MIN_VALIDITY_BLOCKS). This constant is exported from@intentguard/sdkand can be overridden viaminValidityBlocksin the constructor config. - The
fetchoption can be used to route traffic through proxies, add mTLS, or inject observability middleware. - Supported runtimes: Node.js 22+ or a browser environment with native
fetchsupport. The SDK uses ES2022+ features and does not support Node.js 18 or 20.