Skip to content

Instantly share code, notes, and snippets.

@tnunamak
Last active July 29, 2026 19:46
Show Gist options
  • Select an option

  • Save tnunamak/2d774916df8f25416b7cb65b419f764f to your computer and use it in GitHub Desktop.

Select an option

Save tnunamak/2d774916df8f25416b7cb65b419f764f to your computer and use it in GitHub Desktop.
Register a Vana Data Portability builder app from the CLI

Vana builder-registration CLI

Register a builder app directly with the Data Portability RPC. The script is self-contained: it generates the builder EOA, derives its public key, creates the required EIP-712 signature, and calls POST /v1/builders.

It defaults to Vana mainnet, but will not submit a mainnet registration unless you explicitly add --confirm-mainnet.

Download and run

Requires Node.js 20+.

  1. Open this gist and click Download ZIP.
  2. Unzip it, then open a terminal in the extracted vana-builder-registration-cli-* directory.
  3. Install the one dependency and inspect a mainnet request:
cd vana-builder-registration-cli-*
npm install

# Inspect a mainnet request without registering it.
npm run register -- --app-url https://your-app.example --dry-run

# Register on mainnet. Save the printed VANA_PRIVATE_KEY in your app's secret store.
npm run register -- --app-url https://your-app.example --confirm-mainnet

Use an existing builder identity instead of generating one:

VANA_PRIVATE_KEY=0x... npm run register -- --app-url https://your-app.example --confirm-mainnet

Moksha testnet

npm run register -- --network testnet --app-url https://your-app.example

Network selection is explicit and signed into the EIP-712 domain:

Network Chain ID Data Portability RPC
Mainnet (default) 1480 https://dp-rpc.vana.org
Moksha testnet 14800 https://dp-rpc-dev.vana.org

Important operational notes

  • The builder key is the app identity. The RPC stores its address and uncompressed public key, not the private key. If the script generated a key, it prints it exactly once; save it in a secret manager before proceeding.
  • Registration is permanent for that key/app URL combination. Use --dry-run before mainnet, and Moksha for an end-to-end rehearsal.
  • This is the Data Portability RPC, not Vana's EVM JSON-RPC (https://rpc.vana.org).
  • You can look up registered apps by owner with GET https://dp-rpc.vana.org/v1/builders?owner=0x... (or replace with dp-rpc-dev for Moksha).

Verification

npm test

The test suite checks the default network and validates a freshly signed Moksha registration payload. For a live end-to-end test, run the Moksha command above and retain the output address.

References

{
"name": "vana-builder-registration-cli",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vana-builder-registration-cli",
"version": "1.0.0",
"dependencies": {
"viem": "^2.38.5"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@adraffy/ens-normalize": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
"integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
"license": "MIT"
},
"node_modules/@noble/ciphers": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
"integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/base": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip32": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
"integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
"license": "MIT",
"dependencies": {
"@noble/curves": "~1.9.0",
"@noble/hashes": "~1.8.0",
"@scure/base": "~1.2.5"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip39": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
"integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "~1.8.0",
"@scure/base": "~1.2.5"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/abitype": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz",
"integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/wevm"
},
"peerDependencies": {
"typescript": ">=5.0.4",
"zod": "^3.22.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/isows": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
"integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/wevm"
}
],
"license": "MIT",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/ox": {
"version": "0.14.33",
"resolved": "https://registry.npmjs.org/ox/-/ox-0.14.33.tgz",
"integrity": "sha512-rooA/4o7bBof4Ge2VH/eovfNPb/AEEYyrNj03wggc55g5HZD8Pjs/OeWhttgjic3dDcqn0r29bDuvQEdTiUemQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/wevm"
}
],
"license": "MIT",
"dependencies": {
"@adraffy/ens-normalize": "^1.11.0",
"@noble/ciphers": "^1.3.0",
"@noble/curves": "1.9.1",
"@noble/hashes": "^1.8.0",
"@scure/bip32": "^1.7.0",
"@scure/bip39": "^1.6.0",
"abitype": "^1.2.3",
"eventemitter3": "5.0.1"
},
"peerDependencies": {
"typescript": ">=5.4.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/viem": {
"version": "2.55.10",
"resolved": "https://registry.npmjs.org/viem/-/viem-2.55.10.tgz",
"integrity": "sha512-Q9Ba+/ma81U2M5o5P2AQ7Ux8rTIwmCZvUcr8rKdQ22bV0IBFHllM2m5gWDP8hFaUN2nH2oW3QG44amRazflYNQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/wevm"
}
],
"license": "MIT",
"dependencies": {
"@noble/curves": "1.9.1",
"@noble/hashes": "1.8.0",
"@scure/bip32": "1.7.0",
"@scure/bip39": "1.6.0",
"abitype": "1.2.3",
"isows": "1.0.7",
"ox": "0.14.33",
"ws": "8.21.0"
},
"peerDependencies": {
"typescript": ">=5.0.4"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/ws": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}
{
"name": "vana-builder-registration-cli",
"private": true,
"version": "1.0.0",
"description": "Register a Vana Data Portability builder identity from the command line.",
"type": "module",
"scripts": {
"register": "node register-builder.mjs",
"test": "node --test register-builder.test.mjs"
},
"dependencies": {
"viem": "^2.38.5"
},
"engines": {
"node": ">=20"
}
}
#!/usr/bin/env node
/*
* Register a builder identity with Vana Data Portability.
*
* This script is deliberately a small protocol client, rather than a wrapper
* around Account. A builder owns the EOA private key it uses for registration;
* it must retain that key as VANA_PRIVATE_KEY to use the registered app later.
*
* Protocol source: https://github.com/vana-com/data-gateway/blob/dev/api/v1/builders.ts
* Mainnet RPC: https://dp-rpc.vana.org
* Moksha testnet RPC: https://dp-rpc-dev.vana.org
*/
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
export const NETWORKS = {
mainnet: {
chainId: 1480,
rpcUrl: "https://dp-rpc.vana.org",
},
testnet: {
chainId: 14800,
rpcUrl: "https://dp-rpc-dev.vana.org",
},
};
// The builders endpoint verifies this EIP-712 domain against the canonical
// DataPortabilityGrantees contract. The address is shared by mainnet/Moksha;
// chainId prevents a signature from being replayed across environments.
export const DATA_PORTABILITY_GRANTEES =
"0x8325C0A0948483EdA023A1A2Fd895e62C5131234";
export const BUILDER_REGISTRATION_TYPES = {
BuilderRegistration: [
{ name: "ownerAddress", type: "address" },
{ name: "granteeAddress", type: "address" },
{ name: "publicKey", type: "string" },
{ name: "appUrl", type: "string" },
],
};
function usage(error) {
const message = `
Usage:
npm run register -- --app-url https://your-app.example [options]
Options:
--network mainnet|testnet Target network (default: mainnet)
--private-key 0x... Existing builder key (or set VANA_PRIVATE_KEY)
--dry-run Sign and print the request; do not register it
--confirm-mainnet Required before a mainnet registration is sent
--json Emit machine-readable output
--help Show this help
`.trim();
if (error) throw new Error(`${error}\n\n${message}`);
console.log(message);
}
export function parseArgs(args) {
const options = { network: "mainnet", dryRun: false, json: false };
for (let index = 0; index < args.length; index += 1) {
const argument = args[index];
if (argument === "--help") options.help = true;
else if (argument === "--dry-run") options.dryRun = true;
else if (argument === "--confirm-mainnet") options.confirmMainnet = true;
else if (argument === "--json") options.json = true;
else if (["--app-url", "--network", "--private-key"].includes(argument)) {
const value = args[index + 1];
if (!value || value.startsWith("--")) usage(`Missing value for ${argument}.`);
options[{ "--app-url": "appUrl", "--network": "network", "--private-key": "privateKey" }[argument]] = value;
index += 1;
} else usage(`Unknown argument: ${argument}`);
}
return options;
}
function validateAppUrl(value) {
try {
const url = new URL(value);
if (!['http:', 'https:'].includes(url.protocol)) throw new Error();
return url.href;
} catch {
usage("--app-url must be an absolute http(s) URL.");
}
}
function output(result, json) {
if (json) return console.log(JSON.stringify(result, null, 2));
console.log(`Network: ${result.network}`);
console.log(`RPC: ${result.rpcUrl}`);
console.log(`Builder address: ${result.builderAddress}`);
console.log(`App URL: ${result.request.appUrl}`);
if (result.generatedPrivateKey) {
console.log("\nA fresh builder identity was generated. Store this now; it is not saved by the script:");
console.log(`VANA_PRIVATE_KEY=${result.generatedPrivateKey}`);
}
if (result.dryRun) console.log("\nDry run only: no builder was registered.");
else console.log(`\nRegistered successfully (HTTP ${result.status}).`);
}
export async function buildRegistration({ appUrl, networkName, privateKey }) {
const network = NETWORKS[networkName];
if (!network) usage("--network must be mainnet or testnet.");
const generatedPrivateKey = privateKey ? undefined : generatePrivateKey();
const account = privateKeyToAccount(privateKey ?? generatedPrivateKey);
const request = {
ownerAddress: account.address,
granteeAddress: account.address,
publicKey: account.publicKey,
appUrl: validateAppUrl(appUrl),
};
const signature = await account.signTypedData({
domain: {
name: "Vana Data Portability",
version: "1",
chainId: network.chainId,
verifyingContract: DATA_PORTABILITY_GRANTEES,
},
types: BUILDER_REGISTRATION_TYPES,
primaryType: "BuilderRegistration",
message: request,
});
return { network, request, signature, generatedPrivateKey };
}
export async function registerBuilder({ appUrl, networkName = "mainnet", privateKey, dryRun = false, confirmMainnet = false }) {
const registration = await buildRegistration({ appUrl, networkName, privateKey });
if (networkName === "mainnet" && !dryRun && !confirmMainnet) {
usage("Refusing to write to mainnet without --confirm-mainnet. Use --dry-run first to inspect the request.");
}
if (dryRun) return { ...registration, dryRun: true };
const response = await fetch(`${registration.network.rpcUrl}/v1/builders`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Web3Signed ${registration.signature}`,
},
body: JSON.stringify(registration.request),
});
const bodyText = await response.text();
let body;
try { body = bodyText ? JSON.parse(bodyText) : null; } catch { body = { raw: bodyText }; }
if (response.status !== 201) {
throw new Error(`Data Portability RPC rejected the registration (HTTP ${response.status}): ${JSON.stringify(body)}`);
}
return { ...registration, status: response.status, body };
}
async function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) return usage();
if (!options.appUrl) usage("--app-url is required.");
const result = await registerBuilder({
appUrl: options.appUrl,
networkName: options.network,
privateKey: options.privateKey ?? process.env.VANA_PRIVATE_KEY,
dryRun: options.dryRun,
confirmMainnet: options.confirmMainnet,
});
output({
network: options.network,
rpcUrl: result.network.rpcUrl,
builderAddress: result.request.granteeAddress,
request: result.request,
generatedPrivateKey: result.generatedPrivateKey,
dryRun: result.dryRun,
status: result.status,
response: result.body,
}, options.json);
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
console.error(`Error: ${error.message}`);
process.exitCode = 1;
});
}
import assert from "node:assert/strict";
import test from "node:test";
import { buildRegistration, NETWORKS, parseArgs } from "./register-builder.mjs";
test("mainnet is the default target", () => {
assert.equal(parseArgs([]).network, "mainnet");
assert.equal(NETWORKS.mainnet.rpcUrl, "https://dp-rpc.vana.org");
});
test("builds a self-consistent Moksha registration", async () => {
const result = await buildRegistration({
appUrl: "https://example.com/test-builder",
networkName: "testnet",
});
assert.equal(result.network.chainId, 14800);
assert.equal(result.request.ownerAddress, result.request.granteeAddress);
assert.match(result.request.publicKey, /^0x04[0-9a-f]{128}$/i);
assert.match(result.signature, /^0x[0-9a-f]{130}$/i);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment