Skip to content

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.

If you still have the sample project from the previous tutorials, cd into it. Otherwise spin up a fresh scaffold:

mkdir -p identities-demo
cd identities-demo
nxcc init .
npm install

Install the nXCC CLI if it’s not already on your PATH:

npm install -g @nxcc/cli

Make sure the local nXCC node from the earlier guides is running (Docker or native). We’ll start Anvil in the next section.

Start (or restart) a local Anvil chain in one terminal:

anvil --disable-block-gas-limit

We 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=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
export 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.

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.

Install dependencies (if you haven’t yet) and compile both the policy and worker bundles:

npm install
npm run build

Mint 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.

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.

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.

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 build
export 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/wallet

Save the address field—you’ll fund it in the next step.

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.

Call the wallet endpoint again—the balance should now reflect the transfer from Anvil.

curl http://localhost:6922/w/$WORK_ORDER_ID/wallet

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.