TypeScript API
Developer Preview
@navalabs/sdk is an ESM package with types. Use it when you want the
verification flow inside your own service rather than through
the CLI or the MCP server.
The package is restricted during Developer Preview. Get package access and a provisioned credential from Nava before installing it.
Integrate Nava Guardian with @navalabs/sdk in this TypeScript project. Inspect the package manager, runtime, secret handling, and existing action flow first, and confirm that I have access to the restricted package. Use NavaClient.requestVerification followed by waitForVerification for each exact proposed action. Treat prompt as audit context only; proposedTx is the structured action Guardian evaluates. Read credentials from the existing secret or environment system and never expose them in source, prompts, logs, errors, analytics, or test output. Continue only when status is APPROVED, canExecute is true, verdict.outcome is approved, and verdict.reasonCode is allowed. Treat any mismatch, rejection, pending, an unknown request hash, a missing or malformed verdict, a transport failure, and NavaTimeoutError as blocked. Request a new verdict after any material action change. Keep signing and venue submission outside Guardian. Add tests for the approved path and every fail-closed path, run the repository checks, and report changed files, checks run, preview access needs, and manual credential steps.npm install @navalabs/sdkRequires Node.js 18 or newer.
Verify one proposed action
import { NavaClient } from '@navalabs/sdk';
const nava = new NavaClient({
apiKey: process.env.NAVA_API_KEY!,
walletAddress: process.env.WALLET_ADDRESS!,
});
const created = await nava.requestVerification({
prompt: 'Swap WETH for USDC on Uniswap',
proposedTx: {
protocol: 'uniswap',
chainId: 11155111,
to: '0x...',
data: '0x...',
value: '0',
},
});
if (!created.requestHash) throw new Error('Guardian did not return a request hash');
const status = await nava.waitForVerification(created.requestHash);
const approved =
status.status === 'APPROVED' &&
status.canExecute === true &&
status.verdict?.outcome === 'approved' &&
status.verdict?.reasonCode === 'allowed';
if (!approved) throw new Error('Guardian did not approve this action');
// Your application now decides whether to sign and submit this exact action.The field is prompt, not userPrompt, and the transaction is proposedTx, not
tx. The prompt is audit context for people and records, not policy authority.
Guardian evaluates the exact structured proposedTx against the active policy.
Do not put secrets or personal data in either field. proposedTx.chainId is
required and must be a chain the registry supports; requestVerification
throws at the SDK boundary otherwise, before any request is sent.
checkVerificationStatus returns a status envelope. Treat it as approval only
when status is "APPROVED", canExecute is true, verdict.outcome is
"approved", and verdict.reasonCode is "allowed". Any mismatch blocks
execution. An unknown hash returns status: "NONE" with canExecute: false.
An EXECUTED, REJECTED, or FAILED envelope remains blocked even if it
retains an earlier approved verdict.
NavaClient.waitForVerification polls for you, with the same 2000 ms interval
and 30 attempt defaults the CLI uses: a ceiling of 58 seconds of waiting,
since the loop sleeps only between polls. Exhausting the attempts throws
NavaTimeoutError; it does not return a PENDING status. Catch it and treat it
as neither a rejection nor an approval.
Manage agents and policies
import { bootstrapGuardian } from '@navalabs/sdk';
const context = await bootstrapGuardian({
bearerToken: process.env.NAVA_MANAGEMENT_TOKEN!,
agentName: 'Treasury Agent',
walletAddress: process.env.WALLET_ADDRESS!,
chainId: 11155111,
policy: { presetId: 'spot-swapper' },
});bootstrapGuardian provisions an agent, attaches its verification wallet, and
activates a policy in one call. It needs a management credential, which is a
different authority from the agent API key used for submissions; do not reuse
one for the other.
Two clients sit underneath it, both exported from the package root:
GuardianClient: agent, wallet, API-key, policy, and verdict-history management. Management authority.GuardianVerdictClient: submissions and verdict polling with an agent API key. Agent authority.
Related exports on the root: parsePolicyVerdict, isTerminalPolicyVerdict,
extractPolicyVerdictFromCreateResponse, VERDICT_SCHEMA_VERSION,
NavaFetchError, and NavaTimeoutError.
Subpath exports
Five import paths, and the two binaries, are the stable public contract. Each row below was checked against the built module, not only the package manifest.
| Import | Contents |
|---|---|
@navalabs/sdk | NavaClient, GuardianClient, GuardianVerdictClient, bootstrapGuardian, the verdict helpers. Start here. |
@navalabs/sdk/escrow | Agent-execution bootstrap: bootstrapAgentExecution, createAgentClient, signAgentTransaction, AgentNavaClient, the KeyStore adapters (EnvKeyStore, FileKeyStore, MemoryKeyStore), and the P-256 helpers. |
@navalabs/sdk/mcp | startServer, createMCPServer, KNOWN_PROTOCOLS, parseProtocols, and the registrar plumbing. What nava-mcp runs. |
@navalabs/sdk/cli | runCli, runCliEntry, parseArgs, and the argv helpers (requiredStringArg, enumArg, intArg, requireEnv, …). What nava runs, and what an adapter's CLI module plugs into. |
@navalabs/sdk/surface | The ProtocolSurface descriptor framework: defineProtocolSurface, defineOperation, createCliModule, registerProtocolTools. |
import { NavaClient, bootstrapGuardian } from '@navalabs/sdk';
import { bootstrapAgentExecution, FileKeyStore } from '@navalabs/sdk/escrow';
import { startServer, parseProtocols } from '@navalabs/sdk/mcp';
import { runCli, requiredStringArg } from '@navalabs/sdk/cli';
import { defineProtocolSurface, defineOperation } from '@navalabs/sdk/surface';The descriptor helpers on /surface are also re-exported from the package root,
so defineProtocolSurface, defineOperation, createCliModule, and
registerProtocolTools resolve either way.
Consumers still on moduleResolution: "node" reach the subpath types through
typesVersions rather than exports.
Adapter subpaths
Adapters are ESM packages too, and each exposes its own operation surface.
import { createUniswapTools } from '@navalabs/uniswap-adapter';
import { registerUniswapTools } from '@navalabs/uniswap-adapter/mcp';
import { uniswapCliModule } from '@navalabs/uniswap-adapter/cli';@navalabs/hyperliquid-adapter | Exports |
|---|---|
| package root | The full surface: clients, tools, error types, the asset resolver. |
/adapter | createHyperliquidTools, createHyperliquidReadTools, createHyperliquidWriteTools, and the verification-mode helpers. |
/actions | Action builders and hyperliquidActionHash: build and hash an action without executing it. |
/mcp | createProtocolRegistrarFactory and the shared field enums. |
/cli | hyperliquidCliModule and the per-command parsers. |
@navalabs/uniswap-adapter | Exports |
|---|---|
| package root | The full surface: tool factories, pool and token registries, approval helpers. |
/tools | The six tool factories plus createUniswapTools. |
/mcp | createProtocolRegistrarFactory and the advertised field schemas. |
/cli | uniswapCliModule and the per-command parsers. |
/adapter | Advertised but empty: exports nothing. Do not import it. |
Uniswap's /adapter resolves and builds, so nothing warns you; it simply has no
members. Its Hyperliquid namesake is a real module, which makes the asymmetry
easy to trip over. Import from the package root or /tools instead.
The ProtocolSurface descriptor
Every adapter declares its public surface once, in a ProtocolSurface
descriptor. The CLI commands and the MCP tool registrations are both derived
from it, so an operation cannot exist on one transport and not the other by
accident.
defineProtocolSurface validates the declarations when the module loads, before
either transport is wired. It throws unless the operation name is kebab-case and
unique, every operation exposes at least one transport, cli.name equals the
operation name, mcp.name is exactly <protocol>.<name>, and every build
operation supplies an output schema. A camelCase alias cannot be introduced.
import { defineOperation, defineProtocolSurface } from '@navalabs/sdk/surface';
export const exampleSurface = defineProtocolSurface({
protocol: 'example',
operations: [
defineOperation({
name: 'get-price',
kind: 'read',
description: 'Fetch a mid price.',
cli: { name: 'get-price', parse: parseGetPrice },
mcp: { name: 'example.get-price', inputSchema: getPriceSchema },
handler: (tools, input) => tools.getPrice(input),
}),
],
capabilityViews: (tools) => ({
read: tools,
build: tools,
'verified-write': tools,
operational: tools,
}),
});parseGetPrice, getPriceSchema, and the tool factory come from the adapter. createCliModule(surface, createTools) produces the CLI module, while registerProtocolTools(server, surface, tools) registers the MCP tools. Both derive their operation names from the same surface descriptor.
Four operation kinds exist, and the kind decides how a result is reported:
| Kind | Meaning | isError |
|---|---|---|
read | Fetches state. No signing. | Always false unless the call threw. |
build | Returns unsigned calldata. No signing, no broadcast. | Always false unless the call threw. |
verified-write | Submits the venue-native action for verification, then may sign and send. | Derived from the payload's success. |
operational | The base verification operations. | Set by hand, and it does not always track success. A REJECTED await-verification verdict is isError: false with success: false. |
That isError split matters when you call these over MCP; the consequences are
spelled out on the MCP page.
Keep verification and execution separate
A terminal approving verdict permits the exact evaluated action under the returned policy version. It does not execute anything, and it is not evidence that the action was broadcast, filled, or confirmed. Your application owns signing and venue submission, and a materially changed action needs its own verification request.
Never retry a rejected action unchanged, and never split or reshape an action to get around a policy boundary. Read Handle Nava Guardian verdicts for the outcome states and Execution safety for the handoff from verdict to execution.