Any agent with an Ethereum key can send an Argo user a short list of questions. The user sees the request in the Argo app, drafts answers from their own journal if they choose, edits them, and sends them back. Replies arrive at your webhook, signed by Argo. No API key or registration is needed.
How it works
- Your agent signs a request with its wallet key and POSTs it to
/v1/inbox/requests. - Argo verifies the signature (and your ENS name, if you send one) and places the request in the recipient's inbox.
- The recipient answers, declines individual questions, or ignores the request. Nothing is sent without their review.
- If they answer, Argo POSTs a signed reply to your
webhookUrl. Requests expire after 30 days.
Addressing a user
| Form | Example | Notes |
|---|---|---|
@username | @konrad | Case-insensitive. The @ is optional. Users choose a username in the app under Settings. |
0x… | 0xabcd…1234 | Matches the user's Argo Soul wallet or their linked sign-in wallet. |
There is no directory or search endpoint. Get the handle from the user.
Send a request
POST https://api.luminalog.com/v1/inbox/requests
Content-Type: application/json| Field | Type | Limits |
|---|---|---|
to | string | 1–64 chars: @username, username, or 0x address |
from.address | string | 0x + 40 hex; must be the signing address |
from.ens | string, optional | 3–255 chars; must resolve to from.address on Ethereum mainnet |
from.name | string | 1–80 chars, shown to the user |
from.description | string | 1–500 chars, shown to the user |
reason | string | 1–1000 chars, shown to the user |
questions | string[] | 1–10 questions, 1–500 chars each |
webhookUrl | string | Public https URL, up to 2048 chars; no credentials, localhost, or private IPs |
issuedAt | string | ISO-8601 with timezone, within 10 minutes of server time |
nonce | string | 16–128 chars of A–Z a–z 0–9 _ - |
signature | string | 0x-prefixed EIP-191 signature (below) |
Success returns 201 with { "id", "status": "pending", "expiresAt" }. The id is the request hash, and it comes back as requestId in the reply.
Sign it
Hash a JSON array of the fields in this exact order, then sign "Argo information request v1\n" + sha256hex with personal_sign (EIP-191). A positional array means there is no key ordering to get wrong. Hash the strings exactly as you send them, with no trimming.
[to, from.address.toLowerCase(), from.ens ?? "", from.name, from.description,
reason, questions, webhookUrl, issuedAt, nonce]JavaScript (viem)
import { createHash, randomBytes } from 'node:crypto'
import { privateKeyToAccount } from 'viem/accounts'
const account = privateKeyToAccount(process.env.AGENT_KEY)
const body = {
to: '@konrad',
from: { address: account.address, name: 'Match Agent', description: 'Matches founders with co-founders.' },
reason: 'You look like a strong co-founder fit.',
questions: ['What are you building?', 'What skills are you looking for?'],
webhookUrl: 'https://agent.example.com/replies',
issuedAt: new Date().toISOString(),
nonce: randomBytes(16).toString('hex'),
}
const payload = JSON.stringify([
body.to, body.from.address.toLowerCase(), body.from.ens ?? '', body.from.name,
body.from.description, body.reason, body.questions, body.webhookUrl, body.issuedAt, body.nonce,
])
const hash = createHash('sha256').update(payload, 'utf8').digest('hex')
body.signature = await account.signMessage({ message: 'Argo information request v1\n' + hash })
const res = await fetch('https://api.luminalog.com/v1/inbox/requests', {
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body),
})Python (eth_account)
import hashlib, json
from eth_account import Account
from eth_account.messages import encode_defunct
payload = json.dumps([
body["to"], body["from"]["address"].lower(), body["from"].get("ens", ""),
body["from"]["name"], body["from"]["description"], body["reason"],
body["questions"], body["webhookUrl"], body["issuedAt"], body["nonce"],
], separators=(",", ":"), ensure_ascii=False)
digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()
signed = Account.sign_message(encode_defunct(text="Argo information request v1\n" + digest), private_key=KEY)
body["signature"] = "0x" + bytes(signed.signature).hex() # the 0x prefix is requiredErrors
| Status | error | Meaning |
|---|---|---|
| 400 | invalid_body | A field is missing, the wrong type, or out of limits |
| 400 | invalid_webhook | Not https, has credentials, or points at a private or local address |
| 400 | stale_request | issuedAt is more than 10 minutes from server time |
| 401 | bad_signature | Malformed, or does not recover to from.address |
| 422 | ens_mismatch | from.ens does not resolve to from.address |
| 503 | ens_unavailable | ENS lookup failed; retry later |
| 404 | recipient_not_found | No Argo user matches to |
| 409 | duplicate | This exact signed request was already received |
| 429 | rate_limited | Over 30 requests an hour from your IP, or 3 a day from your address to one user |
| 429 | inbox_full | The user has 50 pending requests |
Receive the reply
Argo POSTs this body to your webhookUrl. There is one entry per question, in the original order, and a declined question has answer: null.
{
"type": "argo.info-response.v1",
"requestId": "<request hash>",
"respondent": { "username": "konrad", "wallet": "0x…" },
"answers": [
{ "question": "What are you building?", "answer": "A private AI journal.", "declined": false },
{ "question": "What skills are you looking for?", "answer": null, "declined": true }
],
"respondedAt": "2026-09-27T10:05:00Z"
}Headers: X-Argo-Signer (address) and X-Argo-Signature, an EIP-191 signature over "Argo information response v1\n" + sha256hex(raw body bytes). Return any 2xx. Argo retries network errors, 429, and 5xx up to 3 times. It does not retry other 4xx responses and does not follow redirects.
Verify the reply
Fetch Argo's signing address once from GET https://api.luminalog.com/v1/inbox/signer and pin it. Check every reply against that pinned address, not against the X-Argo-Signer header, because anyone can set a header. Hash the raw bytes you received, before any JSON parsing.
import hashlib
from eth_account import Account
from eth_account.messages import encode_defunct
ARGO_SIGNER = "0x…" # from GET /v1/inbox/signer, fetched once and pinned
def is_genuine(raw_body: bytes, signature: str) -> bool:
digest = hashlib.sha256(raw_body).hexdigest()
msg = encode_defunct(text="Argo information response v1\n" + digest)
return Account.recover_message(msg, signature=signature).lower() == ARGO_SIGNER.lower()Things to expect
- Replies may never come. The user can ignore a request, and unanswered requests expire after 30 days.
- Answers are written by a person. They may be drafted from the user's journal, but the user reviews and edits them before sending.
- Deduplicate on requestId. A reply can arrive more than once in rare failure cases.
- Only your questions are stored. Argo keeps the request until it is answered, ignored, or expired. Answers pass through to your webhook and are not stored on Argo's server.