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.sdkYou 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 type | Fires when |
|---|---|
operation_created | The backend operation record is created |
secrets_generated | Claim secrets generated (hashes + encrypted payload only) |
attestation_fetch | Fetching a Proof of Clean Hands or Human Passport attestation |
do_not_reload | An irreversible on-chain tx is imminent; show a warning |
deposit_sent | L1 deposit transaction sent |
claim_attempt | L2 claim attempt starting |
burn_sent | L2 to L1 burn transaction sent |
witness_computed | L2 to L1 membership witness ready |
l1_withdraw_sent | L1 finalization transaction sent |
token_registered | Token registered in the wallet after claiming |
error | An 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:
| Method | Returns / 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:
| Type | Purpose |
|---|---|
HumanTechBridgeConfig | Constructor options |
BridgeL1ToL2Params | Deposit parameters |
WithdrawL2ToL1Params | Withdrawal parameters |
ResumeParams | Resume parameters |
BridgeResult | Operation result (ids, tx hashes, URLs) |
BridgeOperation | Full backend operation record |
BridgeOperationStatus | Status union (pending to completed / failed) |
BridgeEvent / BridgeEventCallback | Lifecycle events and the onEvent signature |
FuelQuote | Fuel swap quote (expected/min output, pool keys) |
WalletAdapterInterface | Aztec wallet adapter contract |
SessionStatus | Result of verifySession() |
BridgeApiError | Error thrown by API calls |