Sally
SALLY
Lock transparency · v2.1.0

Audit any liquidity lock

Every Sally liquidity lock is public and verifiable. The lock — not the wallet — is the subject: ask who locked what, where, when, and how long— plus the live USD value, the pool's TVL, and this lock's share of the pool. Resolve it from a wallet or from a single lock id. All values are read live from the chain — no indexer, no subgraph, no backend in the data path.

GET /api/locksCORS: *100% on-chainBase · BSCV2 LP · V3/V4 NFT
Concept

Five questions, one payload

Each lock answers the same five questions, regardless of whether it's a V2 LP token lock or a V3/V4 concentrated-liquidity NFT lock.

Who. The locker — owner, recorded with the lock on-chain. The custodian of the NFT is the Sally controller; the locker is who can unlock.
What. The pool and its tokens — pair.label, kind (V2/V3/V4) and, for NFTs, the tokenId.
Where. The chainId (56 BSC · 8453 Base) and the resolved pool.address, with explorer + DexScreener links.
When & how long. lockedAtunlockTime (unix seconds) and a live secondsRemaining countdown.
REST API

GET /api/locks

Public, CORS-open, edge-cached ~45s. Provide either a wallet or a lock key. Returns live on-chain values.

Query parameters
wallet=0x… (address)read

List every lock owned by a wallet. One of wallet or key is required.

key=0x… (bytes32)read

Resolve a SINGLE lock by its on-chain lock id — no owner address needed. This is the lock-centric lookup.

chain=56 | 8453 | allread

Restrict to one chain, or scan both (all, the default).

format=full | compact | widgetread

full (default) groups locks per chain; compact is a flat array; widget is a one-line summary.

curl — every lock for a wallet
curl "https://sally.tools/api/locks?wallet=0xYourWallet&chain=all"
curl — one lock, by its id (no wallet)
curl "https://sally.tools/api/locks?key=0xLOCK_ID_BYTES32&chain=8453"
JavaScript (browser or Node) — flat array of locks
const locks = await fetch(
  "https://sally.tools/api/locks?wallet=0xYourWallet&format=compact"
).then((r) => r.json())

for (const l of locks) {
  console.log(l.pair, l.usdValue, "of", l.poolTvlUsd,
              "TVL ·", l.lockedSupplyPct + "% of pool ·",
              l.unlocked ? "UNLOCKED" : l.secondsRemaining + "s left")
}
Response

The lock object (format=full)

chains[].locks[] carries the full lock. Money fields are decimal-USD strings (or null when the oracle can't price a token); timestamps are unix-second strings.

GET /api/locks?key=…&chain=8453 → one lock
{
  "wallet": "0x… (the locker)",
  "totalLocks": 1, "lockedCount": 1, "unlockedCount": 0,
  "totalLockedUsd": "29.04",
  "chains": [{
    "chainId": 8453,
    "locks": [{
      "kind": "V3",
      "owner": "0x… (who)",
      "tokenId": "5252792",
      "key": "0x… (the bytes32 lock id)",
      "pair": { "symbol0": "WETH", "symbol1": "PRXVT", "label": "WETH/PRXVT",
                "token0": "0x42…06", "token1": "0x4b…A9" },
      "liquidity": "3887694161053516084",
      "usdValue": "29.04",              // locked value (live)
      "pool": {
        "version": "V3",
        "address": "0x6897…5Da0",       // resolved pool (where)
        "tvlUsd": "29.13",              // real reserves (balanceOf the pool)
        "lockedSupplyPct": 99.70,       // this lock's share of pool value
        "explorerUrl": "https://basescan.org/address/0x6897…",
        "dexscreenerUrl": "https://dexscreener.com/base/0x6897…"
      },
      "lockedAt": "1750000000",          // when
      "unlockTime": "1752592000",        // how long → until
      "unlocked": false,
      "secondsRemaining": 1860000
    }]
  }]
}
Field reference
ownerwho

The locker. The address credited if a viewer trades through the lock's referral link.

kind · tokenId · pairwhat

Lock variant (V2 LP / V3 NFT / V4 NFT), the NFT id, and the token pair with symbols + logos.

chainId · pool.addresswhere

Chain and the resolved AMM pool, plus explorer + DexScreener deep links.

lockedAt · unlockTime · secondsRemainingwhen / how long

Lock start, unlock time (unix seconds) and the live countdown. unlocked flips true at unlockTime.

usdValuevalue

The locked position's live USD value. V2: its share of pool reserves. V3: amount0/amount1 derived from liquidity at the pool's current price, valued via the on-chain oracle.

pool.tvlUsdvalue

The pool's real TVL — the pool contract's actual token0/token1 balances, valued in USD (one multicall). Exact, not estimated.

pool.lockedSupplyPctvalue

This lock's USD value as a percentage of pool TVL — robust whether the position is in or out of range.

keyidentity

The bytes32 lock id. Feed it back as ?key= to re-resolve this exact lock without the owner address.

Trustless

Read it straight from the contract

The API is a convenience layer over public view calls. Skip it entirely and read the LiquidityController yourself — the same numbers, from any RPC.

One proxy, both chains. The SallyLiquidityController lives at 0x7777d7fFF155B043CE0A0785dd8c8c42bEbe7777 on Base (8453) and BSC (56).
getLocks(address owner) → Lock[]read

Every lock for a wallet. Lock = { kind, asset, tokenIdOrAmount, lockTime, unlockTime, owner, tokenA, tokenB, nonce }.

getLockKeys(address owner) → bytes32[]read

The lock ids for a wallet, index-aligned with getLocks. Each id is a shareable handle to one lock.

locks(bytes32 key) → Lockread

Resolve a single lock by its id — no owner needed. Returns the all-zero struct for an unknown / released key. This backs ?key=.

viem — resolve one lock by its id, trustlessly
import { createPublicClient, http } from "viem"
import { base } from "viem/chains"

const client = createPublicClient({ chain: base, transport: http() })
const CONTROLLER = "0x7777d7fFF155B043CE0A0785dd8c8c42bEbe7777"

const ABI = [{
  type: "function", name: "locks", stateMutability: "view",
  inputs: [{ type: "bytes32" }],
  outputs: [
    { name: "kind", type: "uint8" }, { name: "asset", type: "address" },
    { name: "tokenIdOrAmount", type: "uint256" }, { name: "lockTime", type: "uint256" },
    { name: "unlockTime", type: "uint256" }, { name: "owner", type: "address" },
    { name: "tokenA", type: "address" }, { name: "tokenB", type: "address" },
    { name: "nonce", type: "uint256" },
  ],
}]

const lock = await client.readContract({
  address: CONTROLLER, abi: ABI, functionName: "locks",
  args: ["0xLOCK_ID_BYTES32"],
})
// lock.owner (who) · lock.tokenA/tokenB (what) · lock.lockTime → lock.unlockTime (how long)
Notes

Pricing, caching & scope

Live, oracle-priced. USD values come from the protocol's on-chain getUsdPrice oracle (canonical 1e18). A token the oracle can't price yields null rather than a fabricated number.
Edge cache ~45s. Responses are cached at the edge for ~45s with stale-while-revalidate. For block-fresh values, read the contract directly with the snippet above.
V4 pool TVL. Uniswap V4 is a singleton PoolManager that custodies every pool's tokens together, so a per-pool reserve TVL isn't directly readable. V4 locks still report usdValue (locked value); pool.tvlUsd is null.
Share or embed a single lock. /api/locks?key=… lets anyone audit one lock from its id alone — no wallet, no account. Drop the card straight into your site with the lock embed builder.

Command palette

Jump to a page or run an action