Identities and Policies
In this tutorial you’ll connect the identity and policy system to a worker. You’ll deploy the on-chain identity contract, attach a permissive policy, and then use that identity to provision an Ethereum wallet inside a worker. The flow picks up directly after the Getting Started and Blockchain Events guides—feel free to skim those again if you need a refresher.
Before You Start
Section titled “Before You Start”If you still have the sample project from the previous tutorials, cd into it. Otherwise spin up a fresh scaffold:
mkdir -p identities-democd identities-demonxcc init .npm installmkdir -p identities-democd identities-demonxcc init .pnpm installInstall the nXCC CLI if it’s not already on your PATH:
npm install -g @nxcc/clipnpm add -g @nxcc/cliMake sure the local nXCC node from the earlier guides is running (Docker or native). We’ll start Anvil in the next section.
Create Identity and Set Policy
Section titled “Create Identity and Set Policy”1. Deploy the identity contract
Section titled “1. Deploy the identity contract”Start (or restart) a local Anvil chain in one terminal:
anvil --disable-block-gas-limitWe disable the block gas limit to store the policy entirely on-chain for development convenience. In production, one would store the policy in IPFS or a cloud storage bucket.
Then deploy the deterministic Identity.sol contract using the CLI. The default Anvil signer 0xac0974… works well for tutorials:
export ANVIL_SIGNER=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80export IDENTITY_CONTRACT=$(nxcc identity deploy \ --signer "$ANVIL_SIGNER" \ | awk '/Address:/ {print $2}')
echo "Identity contract deployed at: $IDENTITY_CONTRACT"The CLI prints the deployed address. Save it—every subsequent command needs the same contract.
2. Reuse the policy template
Section titled “2. Reuse the policy template”The project generated by nxcc init already includes a permissive policy in policies/default-policy.ts:
import { policy } from "@nxcc/sdk";
export default policy((requests) => { return requests.map(() => true);});Its manifest (policies/manifest.template.json) just points at the built JavaScript bundle. We’ll use this allow-all policy while we focus on wiring identities end-to-end.
3. Build and set the policy
Section titled “3. Build and set the policy”Install dependencies (if you haven’t yet) and compile both the policy and worker bundles:
npm installnpm run buildpnpm installpnpm run buildMint an identity NFT and capture its token ID. If you have jq installed, the command below plucks the JSON field automatically—otherwise, copy the id value from the CLI output and export it manually.
export IDENTITY_ID=$(nxcc identity create "$IDENTITY_CONTRACT" \ --signer "$ANVIL_SIGNER" \ | jq -r '.id')
echo "Identity ID: $IDENTITY_ID"Finally, set the policy URL on the freshly minted identity. The CLI converts the local manifest into an inlined data: URL for you.
nxcc identity set-policy "$IDENTITY_CONTRACT" "$IDENTITY_ID" \ policies/manifest.template.json \ --signer "$ANVIL_SIGNER"At this point the on-chain identity exists, and any worker that the policy approves will be able to request its secrets.
Use the Identity
Section titled “Use the Identity”1. Create the worker
Section titled “1. Create the worker”Replace workers/my-worker.ts with a worker that derives an Ethereum key from the identity secret and exposes simple HTTP endpoints for balance checks and transfers:
import { worker, crypto as nxccCrypto } from "@nxcc/sdk";import { createPublicClient, createWalletClient, formatEther, http, parseEther } from "viem";import { anvil } from "viem/chains";import { privateKeyToAccount } from "viem/accounts";
type Hex = `0x${string}`;
function bytesToHex(bytes: Uint8Array): Hex { return `0x${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}` as Hex;}
async function loadWallet(env: Record<string, unknown>, rpcUrl: string) { const identityKey = env.ETHEREUM_SIGNER as CryptoKey | undefined; if (!identityKey) { throw new Error("Identity secret ETHEREUM_SIGNER is not available"); }
const derived = await nxccCrypto.deriveKey(identityKey, "evm", ["wallet", "secp256k1"], { length: 32, });
const account = privateKeyToAccount(bytesToHex(derived)); const walletClient = createWalletClient({ account, chain: anvil, transport: http(rpcUrl), }); const publicClient = createPublicClient({ chain: anvil, transport: http(rpcUrl), });
return { account, walletClient, publicClient };}
export default worker({ async fetch(request, { env, userdata }) { const rpcUrl = (userdata.rpcUrl as string) ?? "http://host.docker.internal:8545"; const { account, walletClient, publicClient } = await loadWallet(env, rpcUrl); const { pathname } = new URL(request.url);
if (request.method === "GET" && pathname === "/wallet") { const balance = await publicClient.getBalance({ address: account.address }); return { address: account.address, balance: formatEther(balance), }; }
if (request.method === "POST" && pathname === "/transfer") { const { to, amountEth } = (await request.json()) as { to: string; amountEth: string }; const txHash = await walletClient.sendTransaction({ account, to: to as Hex, value: parseEther(amountEth), });
return { hash: txHash, from: account.address }; }
return new Response("Not Found", { status: 404 }); },});The helper derives a deterministic secp256k1 key for the worker using HKDF. Because the identity secret is provided as a Web Crypto CryptoKey, the derived bytes never leave the secure runtime.
2. Update the manifest
Section titled “2. Update the manifest”Trim the worker manifest to only request the identity and expose an HTTP endpoint. Insert the address and token ID captured earlier.
Note that we also changed the chain specifier with a gateway URL so that the nXCC node container can pull the policy from the anvil instance running on the host. In production you can rely on auto-detection from chain id, or specify multiple gateways for redundancy.
{ "bundle": { "source": "../dist/my-worker.js" }, "identities": [ [ { "chain": "http://host.docker.internal:8545", "identity_address": "IDENTITY_CONTRACT", "identity_id": "IDENTITY_ID" }, "ETHEREUM_SIGNER" ] ], "userdata": { "rpcUrl": "http://host.docker.internal:8545" }, "events": [ { "handler": "fetch", "kind": "http_request" } ]}Replace IDENTITY_CONTRACT and IDENTITY_ID with the real values.
3. Deploy the worker
Section titled “3. Deploy the worker”Build and deploy the worker bundle to your local node (http://localhost:6922). Capture the work order ID—it becomes the path segment for HTTP requests.
npm run buildexport WORK_ORDER_ID=$(nxcc worker deploy --bundle workers/manifest.template.json \ --rpc-url http://localhost:6922 \ | jq -r '.workOrderId')
echo "Worker mounted at /w/$WORK_ORDER_ID"If jq isn’t available, run the command without the final pipe and copy the work order ID value from the JSON response.
The first time you run this command, the node will take several seconds to wait for peers to respond with the existing secret. Since there are no peers with the secret, it will receive no responses and generate the first version of the secret, thereby becoming the first peer.
4. Get the wallet from the worker using a fetch endpoint
Section titled “4. Get the wallet from the worker using a fetch endpoint”Query the worker’s fetch handler to see the derived wallet address. By default, workers mount under /w/<work-order-id> (NXCC_DAEMON_BASE_MOUNT_PATH in the node config).
curl http://localhost:6922/w/$WORK_ORDER_ID/walletSave the address field—you’ll fund it in the next step.
5. Fund the wallet with a cast command
Section titled “5. Fund the wallet with a cast command”If Foundry isn’t installed yet, run curl -L https://foundry.paradigm.xyz | bash and follow the prompt to add cast to your shell, then restart the terminal or run foundryup.
With cast available, transfer test ETH from the default Anvil account to the worker wallet:
export WORKER_ADDRESS=$(curl -s http://localhost:6922/w/$WORK_ORDER_ID/wallet | jq -r '.address')cast send "$WORKER_ADDRESS" --value 1ether --private-key "$ANVIL_SIGNER"Alternatively, copy the address manually from the previous curl output and use it in the cast send command.
6. Invoke the balance check
Section titled “6. Invoke the balance check”Call the wallet endpoint again—the balance should now reflect the transfer from Anvil.
curl http://localhost:6922/w/$WORK_ORDER_ID/wallet7. Invoke a balance transfer
Section titled “7. Invoke a balance transfer”Trigger a transfer from the worker-controlled wallet to another account (Anvil’s second default address works well):
curl http://localhost:6922/w/$WORK_ORDER_ID/transfer \ -X POST \ -H "Content-Type: application/json" \ -d '{ "to": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "amountEth": "0.25" }'The response includes the transaction hash. Use cast tx <hash> if you want to inspect it, and re-run the wallet check to confirm the new balance.
You’re now using an identity-governed secret inside a worker. From here you can tighten the policy, rotate secrets, or experiment with more advanced approval logic that inspects attestation claims.