Developer Docs
The nimimo v1 API lets you resolve handles to blockchain addresses and create payment intents that users sign in their own wallet. Public, CORS-enabled, no authentication required for the read endpoints.
How it works
Every nimimo user has a human-readable handle like @neat-gecko. Behind that handle are receiving addresses for Bitcoin, Ethereum, and Solana. The resolve endpoint lets you look up those addresses programmatically. The intents endpoints let you create a payment record on the server that the user signs in their own wallet via a sign URL — useful for invoices, agents, and third-party checkout.
Public
No API key or auth required
Cross-origin
CORS enabled for browser use
Rate limited
Per-IP, headers on every response
Base URL
https://nimimo.com/api/v1All endpoints are versioned. The current version is v1.
Endpoints
/resolve?handle={handle}Resolve a handle to all registered addresses
/resolve?handle={handle}&chain={chain}Resolve a handle to a specific chain address
bitcoin, ethereum, solana/intentsCreate a payment intent for a handle. Returns an intent_id and sign_url.
/intents/{id}Retrieve the current status and details of an intent.
/intents/{id}Update an intent: mark as signed, completed (with tx_hash), or cancelled.
awaiting_signature → signed → completed. Both awaiting_signature and signed can transition to cancelled. An intent past its expires_at auto-transitions to expired on the next read or write. Terminal states (completed, cancelled, expired) cannot be changed.Try it
Test the API right here. The default handle neat-gecko is a real nimimo identity — hit send and see the live response.
Resolve all chains
GET https://nimimo.com/api/v1/resolve?handle=neat-gecko{
"handle": "neat-gecko",
"addresses": {
"bitcoin": "bc1qz3yaratxc9z6wz2pj2k97nzl00l4cucpvcquq9",
"ethereum": "0x874a40B1857B006d46b80c9e6badCEF3BA3B705C",
"solana": "9rhN3eug2LbqZKCtbkGRKjRq9BVa4Y5VE4Puf2p4HCRk"
}
}Resolve a single chain
GET https://nimimo.com/api/v1/resolve?handle=neat-gecko&chain=bitcoin{
"handle": "neat-gecko",
"chain": "bitcoin",
"address": "bc1qz3yaratxc9z6wz2pj2k97nzl00l4cucpvcquq9"
}Create a payment intent
A payment intent is a server-side record of an intended transfer. The recipient handle is resolved server-side, the amount is preserved as a human-readable string (no unit conversion), and the response includes a sign_url the payer opens to sign in their wallet. Only to, chain, and amount are required.
POST https://nimimo.com/api/v1/intents
Content-Type: application/json
{
"from": "@agent", // optional
"to": "@neat-gecko", // required
"chain": "ethereum", // bitcoin | ethereum | solana
"asset": "ETH", // optional, inferred from chain
"amount": "0.05", // required, human-readable
"memo": "Design payment", // optional, max 500 chars
"expires_at": "2026-04-15T12:00:00Z" // optional ISO-8601, defaults to +1h
}{
"intent_id": "int_9x21abc...",
"status": "awaiting_signature",
"to_handle": "@neat-gecko",
"to_address": "0x874a40B1857B006d46b80c9e6badCEF3BA3B705C",
"to_avatar": "https://nimimo.com/api/avatar?seed=neat-gecko",
"chain": "ethereum",
"asset": "ETH",
"amount": "0.05",
"memo": "Design payment",
"sign_url": "https://nimimo.com/sign/int_9x21abc...",
"expires_at": "2026-04-15T12:00:00.000Z",
"created_at": "2026-04-14T12:00:00.000Z"
}Check intent status
Poll this endpoint to track the lifecycle. If the intent is still awaiting_signature and past expires_at, the read auto-transitions it to expired.
GET https://nimimo.com/api/v1/intents/int_9x21abc...{
"intent_id": "int_9x21abc...",
"status": "signed",
"from": "@agent",
"to_handle": "@neat-gecko",
"to_address": "0x874a40B1857B006d46b80c9e6badCEF3BA3B705C",
"to_avatar": "https://nimimo.com/api/avatar?seed=neat-gecko",
"chain": "ethereum",
"asset": "ETH",
"amount": "0.05",
"memo": "Design payment",
"tx_hash": null,
"sign_url": "https://nimimo.com/sign/int_9x21abc...",
"expires_at": "2026-04-15T12:00:00.000Z",
"created_at": "2026-04-14T12:00:00.000Z",
"updated_at": "2026-04-14T12:05:00.000Z"
}Update or cancel an intent
Transition an intent through the state machine. The signing UI at sign_url handles signed and completed automatically when the user signs and broadcasts. You normally only call this endpoint directly to cancel an intent. When marking completed, tx_hash is required.
PATCH https://nimimo.com/api/v1/intents/int_9x21abc...
Content-Type: application/json
{ "status": "cancelled" }PATCH https://nimimo.com/api/v1/intents/int_9x21abc...
Content-Type: application/json
{
"status": "completed",
"tx_hash": "0xabc123..."
}Returns the full intent object (same shape as GET /intents/{id}). Returns 409 invalid_transitionif the requested transition isn't allowed by the state machine, or 410 intent_expired if the intent has already expired.
Quick start
async function resolveHandle(handle) {
const res = await fetch(
`https://nimimo.com/api/v1/resolve?handle=${handle}`
)
if (!res.ok) throw new Error("Resolution failed")
return res.json()
}
const { addresses } = await resolveHandle("neat-gecko")
console.log(addresses.bitcoin)
// "bc1qz3yaratxc9z6wz2pj2k97nzl00l4cucpvcquq9"import requests
def resolve_handle(handle, chain=None):
params = {"handle": handle}
if chain:
params["chain"] = chain
r = requests.get("https://nimimo.com/api/v1/resolve", params=params)
r.raise_for_status()
return r.json()
result = resolve_handle("neat-gecko")
print(result["addresses"]["ethereum"])
# "0x874a40B1857B006d46b80c9e6badCEF3BA3B705C"curl "https://nimimo.com/api/v1/resolve?handle=neat-gecko"
# Single chain
curl "https://nimimo.com/api/v1/resolve?handle=neat-gecko&chain=solana"// 1. Create the intent
const create = await fetch("https://nimimo.com/api/v1/intents", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
to: "@neat-gecko",
chain: "ethereum",
amount: "0.05",
memo: "Invoice #1042",
}),
})
const intent = await create.json()
// 2. Send the user to intent.sign_url to sign in their wallet
window.open(intent.sign_url, "_blank")
// 3. Poll for completion
const poll = setInterval(async () => {
const res = await fetch(`https://nimimo.com/api/v1/intents/${intent.intent_id}`)
const status = await res.json()
if (["completed", "cancelled", "expired"].includes(status.status)) {
clearInterval(poll)
if (status.status === "completed") console.log("Paid:", status.tx_hash)
}
}, 3000)# Create
curl -X POST https://nimimo.com/api/v1/intents \
-H "Content-Type: application/json" \
-d '{ "to": "@neat-gecko", "chain": "ethereum", "amount": "0.05" }'
# Check status
curl https://nimimo.com/api/v1/intents/int_9x21abc...
# Cancel
curl -X PATCH https://nimimo.com/api/v1/intents/int_9x21abc... \
-H "Content-Type: application/json" \
-d '{ "status": "cancelled" }'Error responses
All errors return a JSON object with error and message fields.
| Status | Error | Meaning |
|---|---|---|
| 400 | invalid_handle | Handle is malformed or empty (resolve) |
| 400 | invalid_chain | Chain not supported (use bitcoin, ethereum, or solana) |
| 400 | invalid_body | Request body is not valid JSON (intents) |
| 400 | invalid_id | Intent ID must start with int_ (intents) |
| 400 | Validation failed | Body parsed but failed schema validation; details in the response |
| 404 | not_found | Handle or intent does not exist |
| 404 | no_address | Handle exists but has no address for the requested chain |
| 409 | invalid_transition | Illegal intent state transition (e.g. cancel an already-completed intent) |
| 410 | intent_expired | Intent is past its expires_at and can no longer be updated |
| 429 | — | Rate limit exceeded; honor the Retry-After header |
| 500 | internal_error | Server error; safe to retry with backoff |
{
"error": "not_found",
"message": "Handle not found"
}Handle format
Handles are lowercase alphanumeric strings with optional hyphens. They always start with a letter. When displayed to users, prefix with @.
neat-geckolucky-mountain-42123-badCool-WaterRate limiting
The v1 API is rate-limited per IP address. Limits are per-endpoint:
| Endpoint | Limit |
|---|---|
| GET /resolve | 30 requests / 60 seconds |
| POST /intents | 30 requests / 60 seconds |
| GET /intents/{id} | 30 requests / 60 seconds |
| PATCH /intents/{id} | 30 requests / 60 seconds |
Every API response carries the current limit state in headers:
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 28
X-RateLimit-Reset: 1712073600000When you exceed the limit, the API returns HTTP 429 with a Retry-After header (seconds). The official SDK retries 429s with exponential backoff for resolve calls; for intents calls you should handle 429 in your own retry loop.
If you need higher limits for a production integration, get in touch.
TypeScript SDK
The official SDK wraps the v1 API with typed responses, automatic retries, caching, and payment helpers. Zero runtime dependencies, ~5KB minified, dual ESM/CJS, full TypeScript types. Currently published as 0.1.0.
npm install @nimimo/resolveInitialize
import { NimimoClient } from "@nimimo/resolve"
const nimimo = new NimimoClient()
// Optional: custom config
const nimimo = new NimimoClient({
baseUrl: "https://nimimo.com", // default; SDK appends /api/v1 internally
maxRetries: 2, // retries on 429 with exponential backoff
cacheTtl: 30_000, // 30s in-memory cache for resolve()
})Resolve handles
const { handle, addresses } = await nimimo.resolve("neat-gecko")
console.log(addresses.bitcoin)
// "bc1qz3yaratxc9z6wz2pj2k97nzl00l4cucpvcquq9"
console.log(addresses.ethereum)
// "0x874a40B1857B006d46b80c9e6badCEF3BA3B705C"
console.log(addresses.solana)
// "9rhN3eug2LbqZKCtbkGRKjRq9BVa4Y5VE4Puf2p4HCRk"const { address } = await nimimo.resolve("neat-gecko", "bitcoin")
// "bc1qz3yaratxc9z6wz2pj2k97nzl00l4cucpvcquq9"const results = await nimimo.resolveMany(["neat-gecko", "lucky-mountain"])
// [
// { handle: "neat-gecko", addresses: { bitcoin: "bc1q...", ... } },
// { handle: "lucky-mountain", addresses: { bitcoin: "bc1q...", ... } }
// ]const exists = await nimimo.exists("neat-gecko") // true
const nope = await nimimo.exists("nonexistent") // falseClient-side payment helper
paymentIntent()is a local helper that resolves the recipient and converts the amount to the chain's smallest unit (wei, satoshis, lamports). It does not hit the /intents endpoints — nothing is stored server-side. Use it when your app already owns the signing flow.
const intent = await nimimo.paymentIntent("neat-gecko", {
chain: "ethereum",
amount: "0.05", // ETH
})
// intent = {
// to: "0x874a40B1857B006d46b80c9e6badCEF3BA3B705C",
// value: "50000000000000000", // wei
// chain: "ethereum",
// amount: "0.05",
// handle: "neat-gecko",
// }
// Use with ethers.js
await signer.sendTransaction({ to: intent.to, value: intent.value })
// Use with wagmi/viem
await sendTransaction({ to: intent.to, value: intent.value })const intent = await nimimo.paymentIntent("neat-gecko", {
chain: "bitcoin",
amount: "0.001", // BTC
})
// intent.to → "bc1qz3yaratxc9z6wz2pj2k97nzl00l4cucpvcquq9"
// intent.value → "100000" (satoshis)Server-side intents (with sign URL)
These methods talk to the /intents endpoints. Use them when the signer is a different person than the caller — invoices, agent payments, third-party checkout. The payer signs at sign_url in their own wallet.
import { NimimoClient, IntentNotFound, IntentExpired } from "@nimimo/resolve"
const nimimo = new NimimoClient()
// Create
const created = await nimimo.createIntent({
to: "@neat-gecko",
chain: "ethereum",
amount: "0.05",
memo: "Invoice #1042",
// from, asset, expires_at are optional
})
console.log(created.intent_id) // "int_9x21abc..."
console.log(created.sign_url) // "https://nimimo.com/sign/int_9x21abc..."
console.log(created.status) // "awaiting_signature"
// Poll
const intent = await nimimo.getIntent(created.intent_id)
if (intent.status === "completed") {
console.log("Paid:", intent.tx_hash)
}
// Cancel
try {
await nimimo.cancelIntent(created.intent_id)
} catch (err) {
if (err instanceof IntentExpired) {
console.log("Already expired")
} else if (err instanceof IntentNotFound) {
console.log("No such intent")
}
}Pay URLs
Generate shareable payment links. No SDK needed on the receiving end — the link opens the nimimo profile with a pre-filled payment flow.
const url = nimimo.payUrl("neat-gecko", {
chain: "ethereum",
amount: "0.05",
})
// "https://nimimo.com/@neat-gecko?pay=0.05&chain=ethereum"
// Drop it in an invoice, a bio, or a chat message.
// Anyone who clicks it can pay from any wallet.Error handling
import { NimimoClient, HandleNotFound, NoAddress, RateLimited } from "@nimimo/resolve"
try {
const result = await nimimo.resolve("nonexistent-handle")
} catch (err) {
if (err instanceof HandleNotFound) {
console.log("Handle doesn't exist")
} else if (err instanceof NoAddress) {
console.log("No address for this chain")
} else if (err instanceof RateLimited) {
console.log("Too many requests — SDK auto-retries with backoff")
}
}SDK features
- Full TypeScript types for all v1 responses and errors
- Automatic retry with exponential backoff on 429 (resolve only)
- In-memory cache for resolve with configurable TTL (default 30s)
- Batch resolution for multiple handles in one call
- Client-side payment helper with unit conversion (ETH to wei, BTC to sat, SOL to lamports)
- Server-side intents:
createIntent,getIntent,cancelIntent - Pay URL generation for zero-code payment links
- Typed error classes:
HandleNotFound,NoAddress,InvalidHandle,InvalidChain,IntentNotFound,IntentExpired,InvalidTransition,RateLimited - ~5KB minified, zero runtime dependencies, dual ESM/CJS
Use cases
Payment integrations
Let users pay to a @handle instead of copying a raw address. Resolve the handle, show the address, and initiate the transaction.
Invoices & checkout
Create a payment intent server-side and email or display the sign URL. The payer signs in their own wallet; you poll the intent for completion and tx_hash.
Agent payments
Let an automated agent (bot, service, or AI) propose a payment without ever holding keys. Create the intent, hand the sign URL to the human in the loop, and wait for them to sign.
Contact books
Store nimimo handles as contacts and resolve addresses on demand. Addresses update automatically when users rotate keys.
Multi-chain apps
Resolve a single handle to Bitcoin, Ethereum, and Solana addresses simultaneously. No chain selection needed from the user.
This API is provided as-is. nimimo reserves the right to adjust rate limits or deprecate endpoints with reasonable notice.