npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@31third/sdk

v0.3.3

Published

31Third SDK for Safe automation deployment and policy-guarded execution

Readme

@31third/sdk

SDK for deploying ExecutorModule on Safe and executing policy-guarded rebalancings. It helps you create the Safe deployment proposal, attach on-chain policies, calculate a rebalance, and execute it with a dedicated executor wallet.

Table of contents:

Concept

ExecutorModule lets a Safe keep custody of assets while delegating trading to a dedicated executor wallet. Every execution is checked against on-chain policies before the Safe trade is allowed to proceed.

Important rules:

  • The module owner and executor must be different addresses.
  • By default, SDK deployment sets the module owner to the Safe itself.
  • You can choose a different owner if you deploy through a flow that supports it, but owner != executor must still hold.
  • The executor wallet is a hot wallet and should never be used to create or approve the Safe deployment transaction.

Setup overview:

  1. Create and fund a Safe.
  2. Choose a dedicated executor wallet.
  3. Deploy ExecutorModule plus policies to the Safe.
  4. Use the executor wallet to calculate and execute rebalances.

Security notes:

  • Use a dedicated hot wallet for execution with limited funds.
  • Keep policies strict (asset universe, slippage, allocation) before enabling autonomous execution.
  • Treat the executor wallet as production infrastructure; rotate keys and monitor activity.

Install

npm install @31third/sdk

Deployment note: the Safe deployment proposal must be created by a Safe signer, not by the executor wallet.

Quick start

The recommended flow is:

  1. Create and fund a Safe.
  2. Choose distinct owner and executor addresses.
  3. Deploy ExecutorModule and policies.
  4. Calculate a rebalance.
  5. Execute it with the executor wallet.

Step 1: Create and fund a Safe

Create a Safe at https://app.safe.global/ and fund it with the assets you want the strategy to manage.

Step 2: Choose owner and executor

  • owner: address allowed to manage module settings such as executor, cooldown, and policies.
  • executor: address allowed to call execute.
  • These addresses must be different.
  • In the default SDK deployment flow, the Safe is used as the module owner.

Recommended setup:

  • owner = SAFE_ADDRESS
  • executor = dedicated hot wallet

Step 3: Deploy ExecutorModule + policies

You can deploy in two ways:

Recommended:

  • Use https://app.31third.com/safe-policy-deployer to configure policies and create the Safe transaction.

SDK flow:

  • Use proposeDeployModuleBatchTx when you want to generate and submit the Safe proposal programmatically.
  • Use buildDeploySafePolicyStackTx when you want an already-deployed SafePolicyStackFactory to build the factory deployment transaction for a connected wallet to sign and send directly. This helper does not submit anything to the Safe transaction service. After that transaction executes, call proposeEnableModuleTx with the deployed module address.
import { proposeDeployModuleBatchTx } from '@31third/sdk';
import { JsonRpcProvider, Wallet } from 'ethers';

const provider = new JsonRpcProvider(process.env.RPC_URL!);
const safeSigner = new Wallet(process.env.SAFE_SIGNER_PK!, provider);
const safeAddress = process.env.SAFE_ADDRESS!;
const executor = process.env.EXECUTOR_ADDRESS!;
const priceOracle = process.env.PRICE_ORACLE!;

await proposeDeployModuleBatchTx({
  automationId: 'uuid-or-id',
  safeAddress,
  signer: safeSigner, // Safe signer, not the executor
  chainId: 8453,
  executor,
  batchTrade: process.env.BATCH_TRADE_ADDRESS!,
  policyConfig: {
    cooldown: 3600,
    assetUniverse: {
      assetUniverseTokens: ['0xToken1', '0xToken2'],
    },
    slippage: {
      priceOracle,
      maxSlippageBps: 50,
    },
    staticAllocation: {
      priceOracle,
      targetTokens: ['0xToken1', '0xToken2'],
      targetBps: [5000, 5000],
      driftThresholdBps: 50,
      toleranceThresholdBps: 50,
    },
  },
});

After the Safe transaction is approved and executed, record the deployed module address. You will need it for execution.

Factory flow:

import {
  buildDeploySafePolicyStackTx,
  proposeEnableModuleTx,
} from '@31third/sdk';
import { JsonRpcProvider, Wallet } from 'ethers';

const provider = new JsonRpcProvider(process.env.RPC_URL!);
const connectedWallet = new Wallet(process.env.CONNECTED_WALLET_PK!, provider);
const safeSigner = new Wallet(process.env.SAFE_SIGNER_PK!, provider);

const { safeTx, moduleAddress } = await buildDeploySafePolicyStackTx({
  safeAddress: process.env.SAFE_ADDRESS!,
  factoryAddress: process.env.SAFE_POLICY_STACK_FACTORY!, // Optional, defaults to 0xC13C487819b0c656c49fD7Cf5A9f23E77FF42aF8
  executor: process.env.EXECUTOR_ADDRESS!,
  batchTrade: process.env.BATCH_TRADE_ADDRESS!,
  policyConfig: {
    cooldown: 3600,
    assetUniverse: {
      assetUniverseTokens: ['0xToken1', '0xToken2'],
    },
    slippage: {
      priceOracle: process.env.PRICE_ORACLE!, // Optional, defaults to 0x37C6D6729FF637Ed9bF493c897794B256AFee004
      maxSlippageBps: 50,
    },
    staticAllocation: {
      priceOracle: process.env.PRICE_ORACLE!, // Optional, defaults to 0x37C6D6729FF637Ed9bF493c897794B256AFee004
      targetTokens: ['0xToken1', '0xToken2'],
      targetBps: [5000, 5000],
      driftThresholdBps: 50,
      toleranceThresholdBps: 50,
    },
  },
});

const txResponse = await connectedWallet.sendTransaction({
  to: safeTx.to,
  data: safeTx.data,
  value: safeTx.value,
});
const receipt = await txResponse.wait();

// `moduleAddress` is predicted up front. You can also read the deployed address
// from the SafePolicyStackFactory StackDeployed event in `receipt` if needed.
await proposeEnableModuleTx({
  safeAddress: process.env.SAFE_ADDRESS!,
  moduleAddress,
  signer: safeSigner,
  chainId: 8453,
});

Factory note: the SDK now sends CREATE2 deployment bundles to SafePolicyStackFactory, matching the current protocol contract. The helper also predicts the deployed addresses up front from the factory address, salts, and creation code. Defaults:

  • priceOracle: 0x37C6D6729FF637Ed9bF493c897794B256AFee004
  • safePolicyStackFactory: 0xC13C487819b0c656c49fD7Cf5A9f23E77FF42aF8
  • If you omit these fields in the factory flow, the SDK uses the defaults above.
  • You can override the defaults explicitly by passing factoryAddress and/or policy priceOracle values.

After the factory Safe transaction executes, you can notify the backend in a best-effort way:

import { trackFactoryDeployment } from '@31third/sdk';

await trackFactoryDeployment({
  automationId: 'uuid-or-id',
  safeAddress: process.env.SAFE_ADDRESS!,
  chainId: 8453,
  executor: process.env.EXECUTOR_ADDRESS!,
  batchTrade: process.env.BATCH_TRADE_ADDRESS!,
  factoryAddress: process.env.SAFE_POLICY_STACK_FACTORY!, // Optional, defaults to 0xC13C487819b0c656c49fD7Cf5A9f23E77FF42aF8
  transactionHash: process.env.EXECUTED_SAFE_TX_HASH!,
  provider,
});

trackFactoryDeployment resolves the deployed addresses from the SafePolicyStackFactory StackDeployed event and sends them to the backend. It is best-effort and returns undefined if receipt resolution, event parsing, or backend notification fails.

Step 4: Calculate a rebalancing

Use the 31Third API to calculate the rebalance for the Safe portfolio.

import { calculateRebalancing } from '@31third/sdk';

const rebalancing = await calculateRebalancing({
  apiBaseUrl: 'https://api.31third.com/1.3',
  apiKey: process.env.API_KEY!,
  chainId: 8453,
  payload: {
    wallet: process.env.SAFE_ADDRESS!,
    signer: process.env.EXECUTOR_ADDRESS!,
    simulationTxOrigin: process.env.EXECUTOR_ADDRESS!,
    baseEntries: [],
    targetEntries: [],
    maxSlippage: 0.01,
    maxPriceImpact: 0.05,
    minTradeValue: 0.01,
    skipBalanceValidation: false,
  },
});

Step 5: Execute the rebalancing

Execute the returned txData through the deployed module with the dedicated executor wallet.

import { executeRebalancing } from '@31third/sdk';
import { JsonRpcProvider, Wallet } from 'ethers';

const executor = new Wallet(
  process.env.EXECUTOR_PK!,
  new JsonRpcProvider(process.env.RPC_URL!),
);

const tx = await executeRebalancing({
  signer: executor,
  executorModule: process.env.EXECUTOR_MODULE!,
  rebalancing,
});

await tx.wait();

Rebalancing payload parameters

These are the fields you can set in calculateRebalancing({ payload }):

  • wallet: Safe address the rebalancing is calculated for.
  • signer: address that will sign and execute on-chain. In the single-wallet executor flow, this should be the executor address.
  • simulationTxOrigin: optional override for simulation. In most cases this should match the executor address.
  • baseEntries: list of { tokenAddress, amount } for the assets to sell. amount is in token base units.
  • targetEntries: list of { tokenAddress, allocation } where allocation is a fraction such as 0.2 for 20%.
  • maxDeviationFromTarget: maximum allowed deviation from the target allocation before the API rejects the rebalance.
  • maxSlippage: maximum slippage used when calculating trades.
  • maxPriceImpact: maximum total price impact allowed for the basket trade.
  • minTradeValue: minimum trade value in USD.
  • batchTrade: whether batch trade mode is enabled.
  • revertOnError: whether the full batch should revert if one trade fails.
  • skipBalanceValidation: skip Safe balance checks.
  • failOnMissingPricePair: fail if a required price pair is missing.
  • async: calculate asynchronously.

Token address note: use ETH or 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee for native ETH.

Chain overrides

You can override chain constants by passing chainOverrides to deployment helpers:

import { proposeDeployModuleBatchTx } from '@31third/sdk';

await proposeDeployModuleBatchTx({
  /* ... */
  chainOverrides: {
    safeTxServiceUrls: {
      8453: 'https://safe-transaction-base.safe.global',
    },
    multisendAddresses: {
      8453: '0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526',
    },
    create2FactoryAddresses: {
      8453: '0xce0042B868300000d44A59004Da54A005ffdcf9f',
    },
    safeAppPrefixes: {
      8453: 'base',
    },
    safeTxGasMultiplier: 1.2,
    forceSafeTxGasZero: true,
  },
});

Fields:

  • safeTxServiceUrls: Safe Transaction Service API per chain id.
  • multisendAddresses: MultiSend contract address per chain id.
  • create2FactoryAddresses: CREATE2 factory per chain id.
  • safeAppPrefixes: Safe app URL prefix per chain id.
  • safeTxGasMultiplier: optional multiplier applied to Safe Tx Service gas estimation (default 1.2).
  • forceSafeTxGasZero: force safeTxGas to 0 and skip Safe Tx Service gas estimation (default false).

View helpers

The SDK now includes read helpers for ExecutorModule and policy contracts.

import {
  getExecutorModuleState,
  getPoliciesWithTypes,
  areTokensAllowed,
} from '@31third/sdk';
import { JsonRpcProvider } from 'ethers';

const provider = new JsonRpcProvider(process.env.RPC_URL!);

const state = await getExecutorModuleState(process.env.EXECUTOR_MODULE!, provider);
const policies = await getPoliciesWithTypes(process.env.EXECUTOR_MODULE!, provider);
const allowed = await areTokensAllowed(process.env.ASSET_UNIVERSE_POLICY!, [
  '0x4200000000000000000000000000000000000006',
  '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
], provider);

ExecutorModule

| Function | Returns | |---|---| | getExecutorModuleState(executorModule, runner) | { owner, executor, safe, batchTrade, cooldown, lastExecution } (cooldown, lastExecution are bigint) | | getPolicies(executorModule, runner) | string[] (policy addresses) | | getPoliciesWithTypes(executorModule, runner) | { policy: string, policyType: string }[] | | checkPoliciesVerbose(executorModule, trades, config, runner) | { ok: boolean, failedPolicy: string, reason: string } |

AssetUniversePolicy

| Function | Returns | |---|---| | getAssetUniverseTokens(assetUniversePolicy, runner) | string[] (allowed token addresses) | | isTokenAllowed(assetUniversePolicy, token, runner) | boolean | | areTokensAllowed(assetUniversePolicy, tokens, runner) | boolean[] (same order as input tokens) |

StaticAllocationPolicy

| Function | Returns | |---|---| | getStaticAllocationTargets(staticAllocationPolicy, runner) | { token: string, bps: bigint }[] | | getStaticAllocationConfig(staticAllocationPolicy, runner) | { assetUniverse: string, priceOracle: string, driftThresholdBps: bigint, toleranceThresholdBps: bigint } |

SlippagePolicy

| Function | Returns | |---|---| | getSlippageConfig(slippagePolicy, runner) | { priceOracle: string, maxSlippageBps: bigint, owner: string } |

Policy reason decoding

| Function | Returns | |---|---| | decodePolicyFailure(policyAddress, reasonData, runner) | string (decoded reason, or empty string if unavailable) |

Policy reference

This SDK can deploy these policies and attach them to the ExecutorModule. All policies run on-chain before execution and can veto trades.

AssetUniversePolicy

Purpose: restrict trades to a fixed list of allowed tokens.

How it works:

  • On every trade, checks that trade.from and trade.to are in the allowed token set.
  • Any token not explicitly allowed fails the policy.

Owner controls:

  • allowToken(token) adds a token to the universe.
  • disallowToken(token) removes a token from the universe.

Failure modes (custom errors):

  • AssetUniverseFromTokenNotAllowed(token)
  • AssetUniverseToTokenNotAllowed(token)

Use cases:

  • Prevent AI or external executors from trading into unknown assets.
  • Enforce allowlists per portfolio or strategy.

StaticAllocationPolicy

Purpose: enforce a target portfolio allocation (in bps) and only allow rebalances when a deviation threshold is met.

Inputs:

  • targetTokens and targetBps (must sum to 10_000).
  • assetUniverse (optional, if provided it defines the full supported token set).
  • priceOracle for price feeds.
  • driftThresholdBps: minimum deviation from target that triggers a rebalance.
  • toleranceThresholdBps: maximum deviation allowed after executing the proposed trades.

How it works:

  1. Loads current balances for all supported tokens.
  2. Computes current USD weights via price oracle.
  3. If no token deviates by at least driftThresholdBps, it fails with StaticAllocationNoRebalanceTrigger.
  4. Simulates the proposed trades using minToReceiveBeforeFees and updates balances.
  5. Computes predicted USD weights. If any token is outside toleranceThresholdBps, it fails.

Failure modes (custom errors):

  • StaticAllocationFeedMissing(token)
  • StaticAllocationZeroPortfolioValue()
  • StaticAllocationNoRebalanceTrigger()
  • StaticAllocationTokenNotSupported(token)
  • StaticAllocationInsufficientBalance(token, required, available)
  • StaticAllocationZeroPredictedValue()
  • StaticAllocationOutsideTolerance()

Notes:

  • Requires all tokens to be supported by the configured price oracle.
  • targetTokens must be in the asset universe when assetUniverse is provided.
  • Use assetUniverse to include on-ramp tokens (e.g., USDC/USDT) even if the target allocation is only WBTC/WETH; the universe defines which tokens the strategy can trade through.

SlippagePolicy

Purpose: ensure minToReceiveBeforeFees isn’t below a maximum slippage bound.

Inputs:

  • priceOracle for price feeds.
  • maxSlippageBps (<= 10_000).

How it works:

  • For each trade, gets USD price feeds for from and to.
  • Computes expected output amount and enforces minToReceiveBeforeFees >= expected * (1 - maxSlippageBps/10_000).

Failure modes (custom errors):

  • SlippageFeedMissing(tradeIndex, token)
  • SlippageMinToReceiveTooLow(tradeIndex, minToReceive, minAllowed)

Notes:

  • Requires feeds for both from and to tokens.
  • Uses token decimals and feed decimals to normalize expected output.

Execution integration guide

Use this when implementing an executor service, bot, or agent:

Inputs:

  • SAFE_ADDRESS: Safe that holds assets.
  • EXECUTOR_MODULE: Deployed module address.
  • RPC_URL: Chain RPC.
  • API_KEY: 31Third API key for rebalancing.
  • CHAIN_ID: 1 / 137 / 8453 / 42161.
  • EXECUTOR_PK: Private key of the executor wallet.

Steps:

  1. Fetch current Safe token balances and use those amounts for baseEntries in calculateRebalancing. It is not necessary to include all balances; include only the tokens/amounts you want to rebalance. All selected tokens must be in the defined asset universe.
  2. Call calculateRebalancing with rebalancing/wallet payload.
  3. Call executeRebalancing or decode txData and call checkPoliciesVerbose yourself before execution.
  4. If policy checks pass, execute through the module with the executor wallet.
  5. Record tx hash and any policy failure reasons for auditing.

Expected outputs:

  • Rebalancing response (txData + requiredAllowances).
  • Execution tx hash (or a policy failure reason).

Current status

  • Ethereum and Base are supported right now.
  • Configure the correct chain-specific priceOracle address when deploying policies.
  • Supported Ethereum Mainnet tokens:

| Token Symbol | Token Address | |---|---| | wstETH | 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0 | | WBTC | 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599 | | AAVE | 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9 | | LINK | 0x514910771AF9Ca656af840dff83E8264EcF986CA | | UNI | 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984 | | WETH | 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 | | WSOL | 0xD31a59c85aE9D8edEFeC411D448f90841571b89c | | USDC | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | | USDT | 0xdAC17F958D2ee523a2206206994597C13D831ec7 | | EURC | 0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c |

  • Supported Base Mainnet tokens:

| Token Symbol | Token Address | |---|---| | WBTC | 0x0555e30da8f98308edb960aa94c0db47230d2b9c | | cbETH | 0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22 | | SOL | 0x311935cd80b76769bf2ecc9d8ab7635b2139cf82 | | DAI | 0x50c5725949a6f0c72e6c4a641f24049a917db0cb | | PEPE | 0x52b492a33e447cdb854c7fc19f1e57e8bfa1777d | | EURC | 0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42 | | AAVE | 0x63706e401c06ac8513145b7687a14804d17f814b | | cbDOGE | 0xcbd06e5a2b0c65597161de254aa074e489deb510 | | ZRO | 0x6985884c4392d348587b19cb9eaaf157f13271cd | | USDC | 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 | | LINK | 0x88fb150bdc53a65fe94dea0c9ba0a6daf8c6e196 | | AERO | 0x940181a94a35a4569e4529a3cdfb74e38fd98631 | | COMP | 0x9e1028f5f1d5ede59748ffcee5532509976840e0 | | YFI | 0x9eaf8c1e34f05a589eda6bafdf391cf6ad3cb239 | | OP | 0xafcc6ae807187a31e84138f3860d4ce27973e01b | | MORPHO | 0xbaa5cc21fd487b8fcc2f632f3f4e8d37262a0842 | | cbBTC | 0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf | | RDNT | 0xd722e55c1d9d9fa0021a5215cbb904b92b3dc5d4 | | LBTC | 0xecac9c5f704e954931349da37f60e39f515c11c1 | | WETH | 0x4200000000000000000000000000000000000006 | | aBascbBTC | 0xbdb9300b7cde636d9cd4aff00f6f009ffbbc8ee6 | | aBasUSDC | 0x4e65fe4dba92790696d040ac24aa414708f5c0ab | | aBasWETH | 0xd4a0e0b9149bcee3c920d2e00b5de09138fd8bb7 |