putty
API ReferenceTools

Scan a token before you buy it

The free-layer Scan from §3.7: what a trader is being scammed by, for one token on one chain. It answers, from the chain itself: - **Supply and mint.** `totalSupply()` raw, and whether the owner can mint more. A live mint function held by an EOA is the flag; a pausable one is worse still, and is reported separately. - **LP lock.** Whether the pool's liquidity can be withdrawn, and until when. - **Fee parameters.** The pool's LP fee and, for a putty pool, the putty fee in bps. - **Holder concentration.** The top wallets by recent `Transfer` movement, and whether they hold more than the served concentration threshold. - **The putty Verify label**, attached from `runVerify` rather than reimplemented. **Public, unauthenticated, and free.** No bearer token is required and none is charged; this is the acquisition surface from §3.7, and a security report behind a login is a report almost nobody reads. The same split `POST /verify` makes. **Every field is measured, not-measurable, or failed — and the three are different.** Each measured field is a `{ status, value }` or a `{ status, kind, reason }` union. A read that failed is `unavailable` with `kind: "read_failed"` and the node's own words; a thing this data source cannot answer is `unavailable` with `kind: "not_measurable"` and a reason naming the seam that could. **Neither is ever rendered as `0`, `[]`, or a null with no reason** — a failed holder census must not read as "zero holders", which is the bug class this endpoint exists to stop repeating. So: `holders` is `null` on a single node (counting every holder means walking an indexed log from genesis) while the `topHolders` window beside it is real; the LP lock is `not_measurable` rather than `unlocked` when putty has no index of lockers; and the simulated buy/sell is `not_measurable` because a token address alone cannot price a swap. **Amounts are decimal strings end to end.** A 1e9 supply at 18 decimals is thirty digits and a double cannot hold it, so `totalSupplyRaw`, every `balanceRaw`, and every percentage are strings. Do not parse them into a number to display a total. **`clearedForMainnet` is always `false`** and is declared as the literal `false` in the schema so the document states what the code enforces: no putty report, of any label, is an external audit. A `verified custom` label means every check ran and passed on the evidence putty had — read `verifyLabelExplanation` for the sentence that must be shown beside it.

GET
/scan/token/{chain}/{token}

The free-layer Scan from §3.7: what a trader is being scammed by, for one token on one chain. It answers, from the chain itself:

  • Supply and mint. totalSupply() raw, and whether the owner can mint more. A live mint function held by an EOA is the flag; a pausable one is worse still, and is reported separately.
  • LP lock. Whether the pool's liquidity can be withdrawn, and until when.
  • Fee parameters. The pool's LP fee and, for a putty pool, the putty fee in bps.
  • Holder concentration. The top wallets by recent Transfer movement, and whether they hold more than the served concentration threshold.
  • The putty Verify label, attached from runVerify rather than reimplemented.

Public, unauthenticated, and free. No bearer token is required and none is charged; this is the acquisition surface from §3.7, and a security report behind a login is a report almost nobody reads. The same split POST /verify makes.

Every field is measured, not-measurable, or failed — and the three are different. Each measured field is a { status, value } or a { status, kind, reason } union. A read that failed is unavailable with kind: "read_failed" and the node's own words; a thing this data source cannot answer is unavailable with kind: "not_measurable" and a reason naming the seam that could. Neither is ever rendered as 0, [], or a null with no reason — a failed holder census must not read as "zero holders", which is the bug class this endpoint exists to stop repeating.

So: holders is null on a single node (counting every holder means walking an indexed log from genesis) while the topHolders window beside it is real; the LP lock is not_measurable rather than unlocked when putty has no index of lockers; and the simulated buy/sell is not_measurable because a token address alone cannot price a swap.

Amounts are decimal strings end to end. A 1e9 supply at 18 decimals is thirty digits and a double cannot hold it, so totalSupplyRaw, every balanceRaw, and every percentage are strings. Do not parse them into a number to display a total.

clearedForMainnet is always false and is declared as the literal false in the schema so the document states what the code enforces: no putty report, of any label, is an external audit. A verified custom label means every check ran and passed on the evidence putty had — read verifyLabelExplanation for the sentence that must be shown beside it.

Path Parameters

chain*string

The @putty/chains slug for the chain the token is on. An unknown slug is a 400 naming the slugs that exist.

Length1 <= length
token*string

The ERC-20 to scan. A 20-byte hex address; anything else is a 400.

Length1 <= length

Response Body

application/json

application/json

application/json

application/json

curl -X GET "https://example.com/scan/token/string/string"
{  "chain": "ethereum",  "token": "string",  "observedAt": "string",  "isContract": true,  "name": "string",  "symbol": "string",  "decimals": 0,  "totalSupplyRaw": "string",  "mint": {    "status": "measured",    "value": {      "verdict": "not_measurable",      "holder": "string",      "holderIsContract": true,      "pausable": true,      "explanation": "string"    }  },  "lpLock": {    "status": "measured",    "value": {      "state": "not_measurable",      "pair": "string",      "locker": "string",      "untilUnix": 0,      "explanation": "string"    }  },  "fees": {    "status": "measured",    "value": {      "feeHundredthsBip": 0,      "puttyFeeBps": "string",      "hook": "string",      "hookFlags": "string"    }  },  "holders": {    "status": "measured",    "value": {      "holders": "string",      "topHolders": [        {          "address": "string",          "balanceRaw": "string",          "percent": "string"        }      ],      "topSharePercent": "string",      "concentrated": true,      "concentrationThresholdPercent": "string"    }  },  "simulation": {    "buy": {      "status": "measured",      "value": {        "amountIn": "string",        "expectedOut": "string",        "priceImpact": "string"      }    },    "sell": {      "status": "measured",      "value": {        "amountIn": "string",        "expectedOut": "string",        "priceImpact": "string"      }    }  },  "verifyLabel": "verified template",  "verifyLabelExplanation": "string",  "clearedForMainnet": false}

Generate a contract's Solidity POST

Generates the Solidity source for one preset and answers with the source, the constructor arguments, and a Foundry test skeleton that deploys it. **This is the studio's compute step and it is metered.** The call is debited at `CREDIT_PRICES.contract` — the pricing model's own "contract generate + compile + test" action — through the one credit ledger, and the debit happens *before* the generator runs, so a caller who cannot afford it gets a 402 and no work started. The price is the preset-agnostic flat rate, not a function of input size: generation is local in-process string assembly, so it costs putty nothing that scales with the contract. **Determinism is part of the contract.** The same `presetId`, `params`, and `features` produce byte-identical `source`, so it can be diffed, committed, and hashed. `fingerprint` is the SHA-256 of it, which is what lets a later deploy tell whether the file is still the one the generator produced; a mismatch means it was hand edited and the contract's label drops to `experimental`. **Version pinning.** `openZeppelinContractsVersion` and `foundryRemappings` come out of the installed Wizard, not out of a constant in putty, so the version in this response, the `Compatible with OpenZeppelin Contracts` line in the source, and the remapping written to `remappings.txt` cannot disagree. **What you must send.** Every value in `params` is a string or a number, never a parsed Solidity value, and an array parameter is JSON array text (`"[\"0xYourWallet\"]"`), which is what a form field and a model both produce naturally. A required parameter that is missing is a 400 naming it — see `error.problems`. **The `features` block only affects the `wizard` presets.** The `ours` presets are putty's own source and their features are fixed; sending the block with an `ours` preset is accepted and ignored rather than refused, so one form can serve every preset.

The hooks a user can select GET

Every hook putty ships, one row per Solidity file in `packages/contracts/src/hooks/`. Each row carries what the hook **does** (written from the contract's own NatSpec), what a deployment **needs** (the constructor arguments and the capabilities it cannot work without), the Uniswap v4 **permission bits** its address must end in, and its **audit status**. **Every row is `experimental`, and a client must render that as "not audited".** These seven contracts are hand-written in this repo and none has been through an external audit. The label is the contracts module's own `CONTRACT_AUDIT_LABELS` vocabulary (`audited` / `reviewed` / `experimental`) so a preset and a hook cannot be described in two different sets of words. Do not collapse `experimental` into a tick. **`hookFlags` is a number a deployer needs before deploying.** A Uniswap v4 hook's low 14 address bits ARE its permission manifest, and a CREATE2 deploy must be MINED to an address ending in those bits or the pool refuses the hook. Each value is transcribed from that contract's own `getHookPermissions()` body, and it is checked at load against the miner. **`source` is a path into `packages/contracts`.** The description is a summary putty wrote; the contract is the thing the user is about to trust with real funds. **This list is asserted against the directory it describes.** The suite fails if a file is added to or removed from `packages/contracts/src/hooks/` without a row here, or if a row names a file that does not exist. A catalog entry nobody can select — or a shipped hook nobody can find — is the same dead surface as an unregistered tool, and it does not ship.