REST API.
For teams that want their own interface rather than the embed widget. The API prepares transactions; the user's wallet still signs and submits them, so nothing here can move funds.
Base URL and format#
All endpoints live under https://hoodlock.tech/api/dev, take and return JSON, and send
permissive CORS headers so they work from a browser as well as a server.
These routes send X-Robots-Tag: noindex, so search engines index this page rather
than the JSON.
Authentication#
One credential: a public API key of the form pk_ followed by 48 hex characters. You get
it by signing in with your wallet and registering a developer handle in the
developer dashboard. A handle is 3 to 20 characters of lowercase letters, digits,
underscores or hyphens.
| Method | How the key is passed |
|---|---|
GET | In the query string, as ?key=pk_… |
POST | As a key field in the JSON body |
There is no header form. The CORS policy allows Content-Type only, so an
Authorization header will be blocked on cross-origin requests. Use the query string or the
body field.
The key is public on purpose, in the same sense as a Stripe publishable key. It credits actions to you and does nothing else. It cannot move funds, read balances or reach admin. Claiming earnings requires a signature from your wallet, which the key cannot produce. Embedding it in frontend source is the intended use.
GET /api/dev/config#
Everything your interface needs to build a transaction. Call it once at boot rather than hardcoding addresses or fees. The fees are read from the contracts and can change.
curl "https://hoodlock.tech/api/dev/config?key=pk_YOUR_KEY"{
"chainId": 4663,
"rpc": "https://rpc.mainnet.chain.robinhood.com",
"explorer": "https://robinhoodchain.blockscout.com",
"locker": "0xd0f7d8c6e9f6d80c297bebe4f7fd1b9c8125c32f",
"burner": "0x6bf43ca706faa8ea46803299c191484e82280652",
"vesting": "0x910e19bcC4bce46999994Ed7297E0Fc4431ec72E",
"feeWei": "5000000000000000",
"feeEth": 0.005,
"airdrop": "0x6B1fE7b821001144Ee74EEA258b0AafdE20102E8",
"airdropFee": { "perWalletQuoteAt1Eth": 0, "quote": "call quote(uint32 recipients) on the airdrop contract" },
"fees": { "lock": "5000000000000000", "burn": "5000000000000000", "vesting": "5000000000000000" },
"feesEth": { "lock": 0.005, "burn": 0.005, "vesting": 0.005 },
"commission": 0.5,
"code": "your-handle",
"liquidityLocker": {
"address": "0x91Aa2dA1956C77F8448E18DCaf53383bCc658e67",
"v3Positions": "0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3",
"v4Positions": "0x58daec3116aae6D93017bAAea7749052E8a04fA7",
"collectFeeBps": 2500,
"fixedFeeWei": "0",
"paused": false,
"intent": "POST /api/dev/lp-lock-intent"
}
}feeWei and feeEth hold the lock fee. They predate the per-product
fields and are kept so older integrations keep working, so prefer fees. A product that is not
configured on the chain returns null.
airdrop is the airdrop contract. Its fee depends on how many recipients an airdrop
declares, so call quote(recipients) on that contract rather than reading a flat number. There
is no intent endpoint for airdrops. commission is your share of each credited fee, and
code is your handle, which the attribution signature below needs.
liquidityLocker describes the Liquidity Locker for Uniswap V3 and V4 positions: its
address, the two Uniswap position managers, and its live fee. collectFeeBps is HoodLock's share
of the trading fees an owner claims, in basis points (currently 25%), and fixedFeeWei is
the fee paid when locking. Both are read from the contract. It is null where the locker is not
deployed.
Errors: 404 {"error":"unknown key"}, or 503 if the database is unavailable.
Transaction intents#
Each intent validates your inputs, encodes the call and returns an unsigned transaction. You submit it from the user's wallet.
{ "to": "0x…", "data": "0x…", "value": "5000000000000000", "chainId": 4663, "note": "…" }For the lock, burn and vesting intents, the user must first approve the token for
to. Those intents cover the HoodLock call, not the ERC-20 approval. The burn and vesting
responses repeat this in note, but the lock response does not, so do not rely on it. The
liquidity lock intent is different: it returns the NFT approval itself, see below.
Intents are also how you get paid. The server remembers the token, the exact amount and, for locks and vesting, the exact unlock or end second. When a matching record appears on-chain after the intent, its fee share is credited to you, whichever wallet signed it and however often that wallet has used HoodLock before. Submit exactly the values you asked for, or the action will not match. A fee-on-transfer token records less than was sent, so those actions are not matched automatically.
Invalid input returns 400 with an error message, and an unknown key returns
404 {"error":"unknown key"}.
POST /api/dev/lock-intent#
| Field | Type | Notes |
|---|---|---|
key | string | Your API key. |
token | address | The ERC-20 to lock. |
amount | string | Integer string, in the token's smallest unit. |
unlockTime | string | Unix seconds. Must be in the future. |
curl -X POST https://hoodlock.tech/api/dev/lock-intent \
-H "content-type: application/json" \
-d '{"key":"pk_YOUR_KEY",
"token":"0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73",
"amount":"1000000000000000000000",
"unlockTime":"1790000000"}'POST /api/dev/burn-intent#
Fields: key, token, amount. Returns 404 if
burning is not configured on the chain.
Burning is irreversible. Show the user what is about to happen in your own UI, because the intent endpoint will not stop them.
POST /api/dev/vesting-intent#
| Field | Type | Notes |
|---|---|---|
key | string | Your API key. |
token | address | The ERC-20 to vest. |
amount | string | Integer string, smallest unit. |
beneficiary | address | Who receives the tokens. |
end | string | Unix seconds. Must be at least 24 hours after start. |
start | string, optional | Defaults to the time of the request. |
cliff | string, optional | Defaults to start. Must satisfy start ≤ cliff ≤ end. |
Returns 404 if vesting is not configured on the chain. The endpoint checks the dates and
the cliff, so most schedules that would revert are refused before they reach a signature prompt. It does
not reject a zero beneficiary or a token address with no code, and the contract rejects both.
Leave a margin on the 24 hours. This endpoint checks that end - start ≥ 86400
and that end is more than 24 hours from now, as the contract does with
end > block.timestamp + MIN_DURATION. The contract checks again in the block where the
transaction lands, so a schedule that ends almost exactly 24 hours from now can pass here and still
revert if the user signs late. Add a few minutes.
POST /api/dev/lp-lock-intent#
Locks a Uniswap V3 or V4 liquidity position, an NFT, in the Liquidity Locker until a date the user picks. The position keeps earning trading fees the whole time and its owner claims them whenever they like. The unlock date can only be extended, and the position comes back whole after it. Everything is checked against the chain before anything is returned: the position exists and holds liquidity, the named owner holds it, the date is inside what the contract accepts, and new locks are not paused.
| Field | Type | Notes |
|---|---|---|
key | string | Your API key. |
version | string | "v3" or "v4": which Uniswap position manager holds the NFT. |
tokenId | string | The position's NFT token id. |
unlockTime | string | Unix seconds. In the future and at most 100 years ahead. |
owner | address, optional | The wallet that will sign. When given, the request is refused unless it holds the NFT. |
feeRecipient | address, optional | Where the claimed trading fees go. Defaults to the owner. The owner can change it later. |
acceptUnverifiedHook | boolean, optional | Send true only after your user accepted a warning about the pool's hook. See below. |
curl -X POST https://hoodlock.tech/api/dev/lp-lock-intent \
-H "content-type: application/json" \
-d '{"key":"pk_YOUR_KEY","version":"v4","tokenId":"3546660",
"owner":"0xUSER","unlockTime":"1822500000"}'{
"chainId": 4663,
"approve": { "to": "0x58daec3116aae6D93017bAAea7749052E8a04fA7", "data": "0x…", "value": "0" },
"lock": { "to": "0x91Aa2dA1956C77F8448E18DCaf53383bCc658e67", "data": "0x…", "value": "0" },
"collectFeeBps": 2500,
"hookStatus": "none",
"owner": "0xUSER",
"feeRecipient": "0xUSER",
"note": "…"
}Send approve first and wait for it to confirm, then lock, both from the wallet
that holds the position. approve is null when the locker is already approved for
that NFT. lock.value is the fixed fee in wei. The id of the new lock is the first indexed
topic of the Locked event in the receipt, and its proof page is
https://hoodlock.tech/proof/position/<id>.
Hooks. Any Uniswap V4 pool can be locked, whatever hook it uses. hookStatus says
what the pool's hook is: none (no hook), verified (reviewed by HoodLock),
safe (not reviewed, but it has no permission that runs when liquidity is removed) or
unverified (not reviewed and it can act when liquidity is removed). For an
unverified hook the request is refused with 409 until you send
"acceptUnverifiedHook": true. Show your user a warning first and send it only if they accept;
HoodLock's own app asks the same question. Withdrawing after the unlock date never calls the pool or its
hook, so no hook can block it.
| Status | When |
|---|---|
400 | Bad version, tokenId, unlockTime, owner or feeRecipient. |
404 | Unknown key, no position with that id, or the Liquidity Locker is not deployed on the chain. |
409 | The position is already locked, holds no liquidity, is not held by owner, new locks are paused, or its hook needs acceptUnverifiedHook. |
503 | The chain could not be read. Try again. |
To read a lock afterwards, GET /api/lp/lock/{id} returns its pair, amounts, value, unlock
date and owner, and GET /api/lp/claimable/{id} returns the trading fees it could claim right
now, after HoodLock's share. The owner claims with collect(id) on the locker and takes the
position back with withdraw(id, to) after the unlock date.
POST /api/dev/lp-collect-intent#
Builds the fee claim for one or more liquidity locks, so a creator can claim their share on your site instead of on HoodLock. The owner's share always goes to each lock's fee recipient; this only prepares the transaction and says which wallet must send it.
| Field | Type | Notes |
|---|---|---|
key | string | Your API key. |
id or ids | integer or array | One lock id, or up to 50 with the same owner. |
owner | address, optional | The wallet that will sign. When given, the request is refused unless it owns the lock. |
{
"chainId": 4663,
"collect": { "to": "0x91Aa2dA1956C77F8448E18DCaf53383bCc658e67", "data": "0x…", "value": "0" },
"owner": "0xCREATOR",
"locks": [{ "id": 42, "feeRecipient": "0xCREATOR", "collectFeeBps": 2500 }]
}| Status | When |
|---|---|
400 | No ids, more than 50, or an id that is not a positive integer. |
404 | Unknown key or no lock with that id. |
409 | A lock was withdrawn, the ids have different owners, or owner does not own the lock. |
503 | The chain could not be read. Try again. |
If your launchpad's contract owns the lock instead, it calls collect(id) on the locker
itself; that function can be open to anyone, because the creator's share can only go to the creator.
Paying the token's creator. The fee recipient is fixed at lock time and receives the owner's
share of every collect. To pay the wallet that launched the token, send it as feeRecipient;
left out, it defaults to the wallet that signs. From a contract, pass the creator address you stored at
launch, never msg.sender, tx.origin or address(this). After a test lock,
GET /api/lp/lock/{id} shows the feeRecipient the lock was made with.
Automatic locks from your launchpad contract#
The liquidity is locked in HoodLock either way; what differs is who calls the locker. A launchpad whose
smart contract calls HoodLock's locker itself, the moment a token graduates say, never asks the API for an
intent, so the lock carries no API key. Add that contract under Launchpad contracts in your
developer dashboard and every HoodLock liquidity lock it creates from then on is
credited to your account, the wallet you signed up with, like a lock made through an intent. Add the contract that calls lock()
(the owner in the Locked event). A launchpad that deploys one contract per token adds
the factory that deploys them instead, and every lock by a contract that factory created is credited.
When your contract calls lock itself, pass acceptUnverifiedHook = true for any V4 pool
whose hook is not on HoodLock's allowlist (hookAllowed), your own fee hook included: the
safe status above is the API's, and without it the call reverts with "unverified hook".
To add a contract or factory, your signed-in wallet must have deployed it directly or be its owner(); anything else is added
by us on request. Locks made before it was added are not credited, a contract belongs to one key, and public
deployers anyone can use cannot be added as a factory.
The developer dashboard also links a Solidity interface for the locker and a tested example launchpad contract, both checked against the live locker on a fork of Robinhood Chain.
POST /api/dev/attribute#
Credits a wallet to your handle, so that its later locks, burns and vesting schedules earn you commission even when they do not go through your intents. The wallet has to agree by signing a short message, so call it once when the user connects, not on every page load.
| Field | Type | Notes |
|---|---|---|
key | string | Your API key. |
wallet | address | The connected wallet. |
ts | number | Unix seconds when the message was signed. Must be within five minutes of the server clock. |
signature | string | The wallet's personal_sign of HoodLock referral <code> <ts>, where code is the value from /api/dev/config. Each signature works once. |
curl -X POST https://hoodlock.tech/api/dev/attribute \
-H "content-type: application/json" \
-d '{"key":"pk_YOUR_KEY","wallet":"0xUSER","ts":1790000000,"signature":"0x…"}'Returns {"ok":true}, or {"ok":false,"reason":"…"} with HTTP 200. A
refusal is an expected outcome, not an error. An unknown key returns 404 with
{"ok":false,"reason":"unknown-key"}.
| Reason | Meaning |
|---|---|
bad-wallet | Not a valid address. |
unknown-code | The handle behind the key no longer exists. |
already-attributed | Another partner or referral link got there first. First touch wins and is permanent. |
no-proof | ts or signature is missing. |
stale-proof | ts is more than five minutes away from the server clock. |
proof-replayed | This signature has already been used. Ask the wallet for a new one. |
bad-proof | The signature does not match the wallet and the message. |
unverified | The wallet's history could not be read from the chain. Try again later. |
established-wallet | The wallet has already locked, burned or created vesting on-chain. |
daily-cap | The handle reached its daily safety limit. |
Only wallets with no HoodLock history on-chain can be attributed, and only actions after attribution count. That is deliberate: it stops a partner from claiming users who were already here. Actions built through your intents are credited either way.
Rate limits#
| Endpoint | Requests per minute per IP |
|---|---|
/api/dev/attribute | 30 |
Each *-intent endpoint, counted separately, lp-lock-intent and lp-collect-intent included | 60 |
/api/dev/lock-contracts (registering a contract) | 10 |
Over the limit returns 429 {"error":"rate limited"}.
A minimal integration#
const API = "https://hoodlock.tech/api/dev";
const cfg = await (await fetch(`${API}/config?key=${KEY}`)).json();
// after the user connects: one signature, once per wallet
const ts = Math.floor(Date.now() / 1000);
const signature = await walletClient.signMessage({
account, message: `HoodLock referral ${cfg.code} ${ts}`,
});
await fetch(`${API}/attribute`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ key: KEY, wallet: account, ts, signature }),
});
// 1. approve the token for cfg.locker, 2. lock
const intent = await (await fetch(`${API}/lock-intent`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ key: KEY, token, amount: String(amount), unlockTime: String(unlockTime) }),
})).json();
await walletClient.sendTransaction({
account, to: intent.to, data: intent.data, value: BigInt(intent.value),
});Getting paid#
Developers earn 50% of the fee on every action credited to them, whether through an intent or an attributed wallet. A liquidity lock keeps paying: you earn 50% of HoodLock's share every time its owner claims trading fees, for the life of the lock, plus 50% of any fixed fee paid when locking. Referral links from the affiliate program earn 30%. Actions from the wallet that owns the handle do not count. Earnings accrue automatically and are claimed from the developer dashboard with a wallet signature once the claimable balance is at least $10. The API key plays no part in claiming. For the no-code route, see the embed widget page.