Skip to Content
DevelopersDeveloper Guide

Developer Guide

@human.tech/clean.sdk wraps the full bridge flow (authentication, L1 and L2 transfers, optional fuel swaps, recovery, and attestation) behind a single HumanTechBridge class.

  • Works in the browser and in Node.js.
  • Aztec and viem dependencies are bundled, so there are no peer dependencies to install separately.
  • Fully typed; every public type is exported from the package root.

Building something specific? See Recipes for end-to-end patterns (agent, payroll, OTC, donations, embedded wallet), and Protocol & Cryptography for the ZK, compliance, and contract layer under these calls.

Installation

npm install @human.tech/clean.sdk

You supply your own Ethereum RPC URL; the SDK does not bundle a default endpoint.

Initialization

import { HumanTechBridge, ACTIVE_DEPLOYMENT_ID, getDeployment } from '@human.tech/clean.sdk' const bridge = new HumanTechBridge({ l1RpcUrl: process.env.L1_RPC_URL, // required; your own endpoint. Keep keys server-side. deployment: ACTIVE_DEPLOYMENT_ID, // optional, defaults to the active deployment domain: window.location.origin, // optional in the browser, required in Node.js apiUrl: 'https://shield.human.tech', // optional, this is the default // l2NodeUrl: '...' // optional, overrides the deployment default })

domain is used to derive the encryption key for operation backups. In the browser it defaults to window.location.origin; in Node.js you must pass it explicitly.

Read the network from the deployment rather than hardcoding it. The L1 chain your l1RpcUrl points at, and the chainId you sign with, must match getDeployment(ACTIVE_DEPLOYMENT_ID).network.l1ChainId for the active deployment. This keeps your integration correct across deployment changes.

Authentication

The SDK authenticates with Sign-In with Ethereum (SIWE / EIP-4361) and returns a JWT.

const { token, userId, user } = await bridge.authenticate({ l1Address: '0x...', // Ethereum address l2Address: '0x...', // Aztec address domain: window.location.host, uri: window.location.origin, chainId: l1ChainId, // from getDeployment(ACTIVE_DEPLOYMENT_ID).network.l1ChainId signMessage: (msg) => wallet.signMessage(msg), // optional: nonce, l1LoginMethod, l1WalletProvider, l2LoginMethod, l2WalletProvider }) // Persist the JWT, then restore it on the next page load: bridge.setAuthToken(token) // Non-throwing session check. Call on startup to detect a stale JWT: const session = await bridge.verifySession() if (!session.valid) { // session.reason: 'no_token' | 'token_expired' | 'user_not_found' | 'network_error' }

authenticate() requires domain, uri, and chainId (a breaking change from v1).

Wallet Adapter

Bridging and withdrawing need a walletAdapter, the link between this SDK and an Aztec wallet. It is any object implementing WalletAdapterInterface (defined by this SDK), so you can wrap an @aztec/wallet-sdk wallet or your own.

import type { WalletAdapterInterface } from '@human.tech/clean.sdk' const walletAdapter: WalletAdapterInterface = { // L2 TokenBridge address this adapter acts on bridgeAddress: '0x...', // Run an L2 contract call; return the resulting tx hash async executeCall(contractAddress, method, args, options) { const txHash = await myAztecWallet.call(contractAddress, method, args, options) return { txHash } }, // Private L2 -> L1 exit async executeWithdrawToL1Private(l1Address, amount, nonce, cleanHands, passport, l2Address) { const { txHash, l2BlockNumber } = await myAztecWallet.withdrawPrivate(/* ... */) return { txHash, l2BlockNumber } }, // Public L2 -> L1 exit (same signature as the private variant) async executeWithdrawToL1Public(l1Address, amount, nonce, cleanHands, passport, l2Address) { const { txHash, l2BlockNumber } = await myAztecWallet.withdrawPublic(/* ... */) return { txHash, l2BlockNumber } }, // Optional: register the bridged token with the wallet's PXE after claiming async registerToken(tokenAddress) { /* ... */ }, }

Register the token and bridge contracts with your Aztec wallet’s PXE before bridging, or the L2 claim will fail. The Next.js reference app in the bridge monorepo is a complete reference implementation.

Bridge L1 to L2

Deposit an ERC-20 on Ethereum and claim it on Aztec. Pass an optional fuel config to also fund L2 gas.

const result = await bridge.bridgeL1ToL2({ token: 'USDC', // symbol or L1 contract address amount: '100.00', // human-readable amount l1Address: '0x...', l2Address: '0x...', isPrivate: true, // private (shielded) or public claim on L2 fuel: { enabled: true, amount: '5', // token amount (native decimals) to swap into FeeJuice fuelType: 'private', // 'public' | 'private'; must be 'private' in private mode slippageBps: 300, // optional, default 300 (3%) }, // fuelQuote is optional. Omit it and the SDK builds the V4 quote internally sendTransaction: (tx) => wallet.sendTransaction(tx), walletAdapter: aztecWalletAdapter, // implements WalletAdapterInterface (see "Wallet Adapter") signMessage: (msg) => wallet.signMessage(msg), signTypedData: (addr, json) => wallet.signTypedData(addr, JSON.parse(json)), // Permit2 onEvent: (event) => console.log(event.type, event), }) // result: { operationId, l1TxHash, l2TxHash, l1TxUrl, l2TxUrl }

Never use fuelType: 'public' in private mode. It leaks the L2 recipient on-chain.

Withdraw L2 to L1

Burn the token on Aztec and finalize the withdrawal on Ethereum.

const result = await bridge.withdrawL2ToL1({ token: 'cUSDC', // L2 symbol or L2 contract address amount: '50.00', l1Address: '0x...', // L1 recipient l2Address: '0x...', isPrivate: true, sendTransaction: (tx) => wallet.sendTransaction(tx), walletAdapter: aztecWalletAdapter, signMessage: (msg) => wallet.signMessage(msg), onEvent: (event) => console.log(event.type, event), })

Attestation

Every deposit and withdrawal, public or private, is gated on an attestation (Proof of Clean Hands, with a Human Passport fallback). The SDK fetches it automatically inside bridgeL1ToL2 and withdrawL2ToL1; the calls below are optional pre-checks for your own UI.

// Full status: binding, attester config const status = await bridge.getAttestationStatus() // Lightweight pre-checks. No nonce consumed const poch = await bridge.checkPochEligibility() if (!poch.eligible) { const passport = await bridge.checkPassportEligibility() // passport.eligible, passport.score, passport.maxAmount }

Resume

List operations and resume any that were interrupted. resume() auto-detects the recovery stage and continues from where it left off.

const ops = await bridge.getOperations() const op = await bridge.getOperation(operationId) // fetch a single operation by ID const result = await bridge.resume(operationId, { signMessage: (msg) => wallet.signMessage(msg), sendTransaction: (tx) => wallet.sendTransaction(tx), // needed for L2->L1 resume walletAdapter: aztecWalletAdapter, // needed for L1->L2 resume l1Address: '0x...', // optional hint l2Address: '0x...', // optional hint onEvent: (event) => console.log(event.type, event), })

Events

Pass an onEvent callback to observe the lifecycle, useful for progress UI, toasts, and telemetry. Use the BridgeEventType constants instead of raw strings.

Event typeFires when
operation_createdThe backend operation record is created
secrets_generatedClaim secrets generated (hashes + encrypted payload only)
attestation_fetchFetching a Proof of Clean Hands or Human Passport attestation
do_not_reloadAn irreversible on-chain tx is imminent; show a warning
deposit_sentL1 deposit transaction sent
claim_attemptL2 claim attempt starting
burn_sentL2 to L1 burn transaction sent
witness_computedL2 to L1 membership witness ready
l1_withdraw_sentL1 finalization transaction sent
token_registeredToken registered in the wallet after claiming
errorAn error occurred (carries a fundsAtRisk flag)

Fuel Quotes

Preview the expected FeeJuice output before committing. For actual bridging, prefer passing fuel to bridgeL1ToL2 without a pre-built quote. The SDK builds it internally and guarantees the same routing the contract call uses.

const quote = await bridge.getFuelQuote({ token: 'USDC', // symbol or L1 contract address fuelAmount: '5', // token amount in NATIVE decimals (not USD) slippageBps: 300, // optional, default 300 (3%) }) // quote: { expectedOutput, minOutput, poolKeys, zeroForOnes }

Error Handling

API failures throw BridgeApiError, which carries a human-readable message and the raw response.

import { BridgeApiError } from '@human.tech/clean.sdk' try { await bridge.bridgeL1ToL2(params) } catch (err) { if (err instanceof BridgeApiError) { console.error(err.friendlyMessage) // human-readable console.error(err.status) // HTTP status code console.error(err.parsedBody) // parsed JSON body, or null } }

Call bridge.verifyNodeCompatibility() once at startup to detect a node whose rollup version does not match the SDK build. A mismatch puts the resume/witness paths at risk.

Other Methods

Additional helpers on the HumanTechBridge instance:

MethodReturns / purpose
getOperation(id)A single operation by ID
getAttestationStatus()Binding status, attester config
getPortalFeeBasisPoints(portal)TokenPortal fee rate (bps), to compute the post-fee amount
getL1TokenBalances(address, chains)L1 token balances via the backend Alchemy proxy
getAztecNodeInfo()L2 node info (version, L1 contract addresses)
isAztecNodeReady()Whether the L2 node is accepting requests
getAztecPendingTxCount()L2 mempool pending-tx count (a congestion signal)
getAztecBlockHeader(n | 'latest')An L2 block header
retryFailedPatches()Re-send backend updates queued after a network failure

Type Reference

Key types exported from the package root:

TypePurpose
HumanTechBridgeConfigConstructor options
BridgeL1ToL2ParamsDeposit parameters
WithdrawL2ToL1ParamsWithdrawal parameters
ResumeParamsResume parameters
BridgeResultOperation result (ids, tx hashes, URLs)
BridgeOperationFull backend operation record
BridgeOperationStatusStatus union (pending to completed / failed)
BridgeEvent / BridgeEventCallbackLifecycle events and the onEvent signature
FuelQuoteFuel swap quote (expected/min output, pool keys)
WalletAdapterInterfaceAztec wallet adapter contract
SessionStatusResult of verifySession()
BridgeApiErrorError thrown by API calls
Last updated on