Skip to main content
This guide walks through building a Next.js application that uses the Newton SDK to evaluate transaction intents against your policy, then executes them through your deployed PolicyClient contract.

Prerequisites

Step 1: Create the Project

npx create-next-app@latest newton-sdk-app \
  --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd newton-sdk-app
npm install @magicnewton/newton-protocol-sdk viem

Step 2: Environment Variables

Create .env.local:
# Alchemy RPC URLs
NEXT_PUBLIC_SEPOLIA_ALCHEMY_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY
NEXT_PUBLIC_SEPOLIA_ALCHEMY_WS_URL=wss://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY

# Newton API key — server-side only (do NOT prefix with NEXT_PUBLIC_)
NEWTON_API_KEY=your_newton_api_key

# Newton Policy Contract address (fixed on Sepolia)
NEXT_PUBLIC_POLICY_CONTRACT_ADDRESS=0x698C687f86Bc2206AC7C06eA68AC513A2949abA6

# YOUR deployed wallet address
NEXT_PUBLIC_POLICY_WALLET_ADDRESS=0xYourDeployedWallet

# Signer private key — server-side only (do NOT prefix with NEXT_PUBLIC_)
SIGNER_PRIVATE_KEY=your_private_key_here
Never use the NEXT_PUBLIC_ prefix for private keys. Variables prefixed with NEXT_PUBLIC_ are bundled into client-side JavaScript and visible to anyone inspecting your app. The SIGNER_PRIVATE_KEY above is intentionally server-side only — access it via an API route or server action, not directly in client components.
NEXT_PUBLIC_POLICY_CONTRACT_ADDRESS is the Newton Policy contract (fixed on Sepolia). NEXT_PUBLIC_POLICY_WALLET_ADDRESS is YOUR deployed PolicyClient. These are different contracts.

Step 3: Create Configuration

Create src/const/config.ts:
import { Hex } from "viem";

export const SEPOLIA_ALCHEMY_URL = process.env.NEXT_PUBLIC_SEPOLIA_ALCHEMY_URL!;
export const SEPOLIA_ALCHEMY_WS_URL = process.env.NEXT_PUBLIC_SEPOLIA_ALCHEMY_WS_URL!;
// Access NEWTON_API_KEY server-side only (via API route or server action)
// Do NOT expose API keys with NEXT_PUBLIC_ prefix
export const POLICY_WALLET_ADDRESS = process.env.NEXT_PUBLIC_POLICY_WALLET_ADDRESS as Hex;
export const POLICY_CONTRACT_ADDRESS = process.env.NEXT_PUBLIC_POLICY_CONTRACT_ADDRESS as Hex;
// Access SIGNER_PRIVATE_KEY server-side only (via API route or server action)
// Do NOT expose private keys with NEXT_PUBLIC_ prefix

Step 4: Create the Newton Client

Create src/lib/use-newton-client.ts:
"use client";

import { useMemo } from "react";
import { createWalletClient, webSocket, Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";
import { newtonWalletClientActions } from "@magicnewton/newton-protocol-sdk";
import { SEPOLIA_ALCHEMY_WS_URL } from "@/const/config";

export const useNewtonClient = (privateKey: Hex, apiKey: string) => {
  const client = useMemo(() => {
    const account = privateKeyToAccount(privateKey);
    const walletClient = createWalletClient({
      account,
      chain: sepolia,
      transport: webSocket(SEPOLIA_ALCHEMY_WS_URL),
    }).extend(newtonWalletClientActions({ apiKey }));

    return { walletClient, account };
  }, [privateKey]);

  return client;
};

Step 5: Submit an Evaluation Request

Create src/lib/evaluation-request.ts:
import { Hex } from "viem";
import { sepolia } from "viem/chains";
import { POLICY_WALLET_ADDRESS } from "@/const/config";

function stringToHexBytes(str: string): Hex {
  const encoder = new TextEncoder();
  const bytes = encoder.encode(str);
  return ("0x" + Array.from(bytes)
    .map((byte) => byte.toString(16).padStart(2, "0"))
    .join("")) as Hex;
}

export type EvaluationRequestParams = {
  signerAddress: Hex;
  targetAddress: Hex;
  value: bigint;
  data: Hex;
  wasmArgs: Record<string, unknown>;
};

export const createEvaluationRequest = ({
  signerAddress,
  targetAddress,
  value,
  data,
  wasmArgs,
}: EvaluationRequestParams) => {
  const functionSignature = stringToHexBytes(
    "validateAndExecuteDirect(address,uint256,bytes,(bytes32,address,uint32,uint32,(address,address,uint256,bytes,uint256,bytes),bytes,bytes,bytes),(bytes32,address,bytes32,address,(address,address,uint256,bytes,uint256,bytes),bytes,bytes,(bytes32,address,bytes,(bytes,bytes,bytes,address,uint32)[]),(bytes,uint32)),bytes)"
  );

  return {
    policyClient: POLICY_WALLET_ADDRESS,
    intent: {
      from: signerAddress,
      to: targetAddress,
      value: `0x${value.toString(16)}` as Hex,
      data: data,
      chainId: sepolia.id,
      functionSignature: functionSignature,
    },
    wasmArgs: stringToHexBytes(JSON.stringify(wasmArgs)),
    timeout: 60,
  };
};

Step 6: Execute an Attested Transaction

Use evaluateIntentDirect to get an attestation, then submit the attested transaction on-chain:
// Evaluate intent — returns attestation data
const evalResponse = await walletClient.evaluateIntentDirect(evalRequest);
const { evaluationResult, task, taskResponse, blsSignature } = evalResponse.result;

if (!evaluationResult) {
  throw new Error("Policy evaluation failed — transaction blocked");
}

// Execute on-chain with the attestation
const functionData = encodeFunctionData({
  abi: newtonPolicyWalletAbi,
  functionName: "validateAndExecuteDirect",
  args: [to, value, data, task, taskResponse, blsSignature],
});

const txHash = await walletClient.sendTransaction({
  to: POLICY_WALLET_ADDRESS,
  data: functionData,
});

Gateway Response Handling

When working with the gateway directly (outside the SDK), be aware of these serialization details.

Field Name Mapping

The gateway uses different casing for task (camelCase) and task_response (snake_case):
// task fields: taskId, policyClient, quorumThresholdPercentage
// task_response fields: task_id, policy_client, evaluation_result

// Map with fallback pattern:
const mappedTaskResponse = {
  taskId: toHex(tr.task_id ?? tr.taskId),
  policyClient: tr.policy_client ?? tr.policyClient,
  policyId: toHex(tr.policy_id ?? tr.policyId),
  // ... etc
};

Byte Array Conversion

The gateway returns all Solidity bytes fields as JSON number arrays ([0,0,...,0,1]), not hex strings. Viem expects hex strings and fails with x.replace is not a function if given arrays.
function toHex(val: unknown): string {
  if (typeof val === "string") return val.startsWith("0x") ? val : `0x${val}`;
  if (Array.isArray(val)) {
    return `0x${val.map((b: number) => b.toString(16).padStart(2, "0")).join("")}`;
  }
  if (val == null) return "0x";
  return String(val);
}
Apply toHex() to these fields before ABI encoding: evaluationResult, intentSignature, wasmArgs, quorumNumbers, policy, policyData[].data, policyData[].attestation, policyConfig.policyParams, taskId, and policyId.

Decoding evaluationResult

evaluationResult is an ABI-encoded boolean (32 bytes). The gateway returns it as a JSON byte array:
function decodePolicyAllowed(taskResponse: Record<string, unknown>): boolean {
  const evalResult = taskResponse.evaluation_result ?? taskResponse.evaluationResult;
  if (evalResult == null) return false;
  if (typeof evalResult === "boolean") return evalResult;

  // Gateway returns byte arrays: [0,0,...,0,1] for true, [0,0,...,0,0] for false
  if (Array.isArray(evalResult)) {
    return evalResult.some((b: number) => b !== 0);
  }

  // Hex string fallback
  const str = String(evalResult);
  if (!str || str === "0x") return false;
  const hex = str.startsWith("0x") ? str.slice(2) : str;
  return BigInt(`0x${hex || "0"}`) !== 0n;
}
status: "success" in the gateway response means BLS aggregation completed, not that the policy approved the intent. Operators sign the evaluation result regardless of allow/deny. The actual policy decision is in evaluationResult.

Function Signature Encoding

The functionSignature field in the intent is the hex-encoded human-readable ABI, not the 4-byte selector. The gateway needs the full signature to decode calldata into decoded_function_arguments for Rego. Using the 4-byte selector (0xa9059cbb) instead causes decoding to fail silently.
// Correct: hex-encode the full human-readable signature
const functionSig = "function transfer(address,uint256)";
const functionSigHex = `0x${Buffer.from(functionSig).toString("hex")}`;

// Wrong: using the 4-byte selector
const wrong = "0xa9059cbb";

Step 7: Run the Application

npm run dev
Open http://localhost:3000 in your browser.

Troubleshooting

IssueCauseFix
GATEWAY_ERRORInvalid API key or network issueVerify NEWTON_API_KEY and network connectivity
InvalidAttestation on-chainTask Manager mismatch or expired attestationVerify contract was initialized with correct Task Manager address
WebSocket connection failedWrong WS URL or Alchemy plan limitsVerify SEPOLIA_ALCHEMY_WS_URL format (wss://...)
TIMEOUTOperator network slow or unavailableIncrease timeout or retry

Next Steps

SDK Reference

Full TypeScript SDK documentation

RPC API

Underlying JSON-RPC methods