Endpoints
One owner signature, every registry
Each key operation is one wallet signature, the owner signature, over one message that names every registry it is for. You never sign once per chain.- The account is your settlement identity: an EVM address (
0x+ 40 hex) or a Stellar ed25519 key (0x+ 64 hex, theG…key decoded). An EVM address padded to 32 bytes is accepted and read as the 20-byte form. - The scheme follows the account, not the chain. An EVM account signs EIP-712 typed data (
scheme: "secp256k1"). A Stellar account signs a fixed SEP-53 text (scheme: "sep53"). Both registries accept both schemes, so the same signature works on Stellar and on EVM. - Each registry is a leg
{ chainId, registry, nonce }: the chain id, the registry’s 32-byte id, and that registry’s current nonce for your account. Legs are always ordered EVM first, then by chain id. A message names at most 8 legs. - The EIP-712 domain is
{ name: "ProofBridge Keys", version: "2", salt }, where the salt binds the environment (local,testnetormainnet). It has no chain id; the legs bind the chains. - Each leg lands on its own. If one chain lands and another fails, posting the same body again sends only the missing legs. A registry that already holds your key counts as landed, so signing a fresh message after a partial landing is safe too.
popMsg. It proves you hold the key’s secret. Each chain encodes the key differently, so each leg has
its own popMsg and its own proof.
Always rebuild the message locally from the prepare answer and compare it with
signPayload before you sign. Check
that env matches your own environment. The @proofbridge/bls-encodings package has keyMessageTypedData and
keyMessageText for this.403 caller does not control this account. This is only the relayer’s gas rule: the registries take the same calls
from anyone, so you can always submit them on chain yourself.
Common refusals on every prepare and write route:
Get the half-encryption key
GET /v1/keys/half-encryption
Before you send your half of a trade secret (POST /v1/trades/:id/half, or the maker’s lock), fetch the key to
encrypt it to. The key is signed by a long-term announcement key whose public half is pinned in the frontend and the
agent runtime. Verify signature against your pin and refuse to seal anything if it fails. Send keyId back with
every envelope you seal. See Trade secrets.
verifyKeyAnnouncement(pinnedKey, { keyId, spki, signature }) in @proofbridge/bls-encodings checks it and returns
the key for sealHalf. The key rotates; always use the latest. Answers 503 if the key provider cannot answer.
Register a key
Prepare
POST /v1/keys/register/prepare reads each registry’s nonce and returns one leg per registry, each with the bytes your
BLS key signs for its proof of possession, plus the one owner message. The owner message does not contain the proofs,
so you can sign them in either order. It writes nothing.
scheme is sep53 and signPayload is { "kind": "sep53", "text": "…" }. The text reads
ProofBridge: register a settlement key, then the network, account, key fingerprint, expiry and one line per leg. Sign
it with SEP-53 signMessage.
File
POST /v1/keys/register checks everything the registries will check (the deadline, the owner signature over every leg,
each leg’s nonce and each proof of possession) before spending any gas. It then queues one job per leg and answers
202. Each registry is filed in its own transaction from the relayer’s gas keys. You pay nothing and sign no
transaction.
REGISTER publication per queued leg in
PENDING. Poll GET /v1/keys/:account until every leg is CONFIRMED.
Rotate, retire and cancel
Rotation has no route of its own. You register a new key withPOST /v1/keys/register while the old one stays.
Each registry keeps up to 5 live keys per account, and a new key is usable as soon as it lands. A key that was ever
registered can never be registered again for that account, so each rotation uses a fresh key. A co-signature with the
new key is accepted once it is live on both chains of the trade; new trades switch to it as soon as both legs land.
Retirement ends one key at a time you choose. The relayer stops accepting co-signatures under a key 15 minutes
before its retirement time, so move open trades to the new key first, or pick a time after the open orders signed under
the old key have closed. A retired key still counts toward the 5 until the registry prunes it.
A pre-signed retirement is an emergency off switch you file in advance. You sign a retirement that takes effect at
once, and the retirement service stores it. It is sent only in an incident. A stored retirement can only end that key;
it can never move funds. Each time you rotate, file one for the new key too.
Revocation removes every key on the account. Cancel stops a registration you signed but no longer want, before
it lands everywhere.
Retire prepare
POST /v1/keys/retire/prepare builds the RetireKey(account, keyCommitment, validUntil) message. It names the key,
not a registry, and has no nonce and no deadline. Because a retirement can only shorten a key’s life, one signature
never goes stale and can do nothing but end that key. Send validUntil: "1" to prepare a pre-signed emergency
retirement.
Store a pre-signed retirement
POST /v1/keys/retire/presign files your signature over RetireKey(account, keyCommitment, 1) with the retirement
service. Nothing is sent on chain. One entry is stored per registry where the key is live, each ON_FILE. Filing
again for the same key replaces the stored signature.
201.
Retire now
POST /v1/keys/retire sends a signed retirement to every registry where the key has a slot. A registry whose key
already expires at or before validUntil is skipped. You need an earlier registration through the relayer.
202 with the key status, one SET_VALID_UNTIL publication per chain.
Revoke prepare and revoke
POST /v1/keys/revoke/prepare builds the RevokeKeys(account, legs) message, naming every registry. A revocation
removes every key on the account, a hosted pod’s included; pods lists the pods it would stop, so you can say so
before the owner signs.
POST /v1/keys/revoke files it, one REVOKE publication per registry, and sets the record to REVOKED.
Cancel prepare and cancel
POST /v1/keys/cancel/prepare builds CancelPending(account, legs) for a registration that has not landed everywhere.
A cancel uses up the registry’s nonce without touching any key, so the pending registration signature can never land
there. Its legs name only the registries where the key has not landed. Use it to stop a signed registration you no
longer want before its deadline.
keyCommitment, and without pods. POST /v1/keys/cancel
takes { account, keyCommitment, ownerAuth, revocations }, where revocations are the legs prepare returned.
Key status
GET /v1/keys/:account
Your key record, every leg the relayer filed (publications, oldest first), and each registry’s live state read now
(onChain). Anyone signed in may read any account. It works with no record too, which is how you read the live nonces
before signing. It writes nothing.
actionisREGISTER,REVOKE,CANCELorSET_VALID_UNTIL(a retirement).validUntilon a slot is unix seconds;"0"means no expiry.unread: truemeans that registry could not be read this time. Keep your last known state for that chain rather than reading the empty fields as “no key”.
onChain[].matchesRecord is true. Settlement needs it
live on both chains of the trade. A leg is pending while its publication is PENDING or SUBMITTED; show error as
the reason when set.
If a leg fails with
this key was registered before — derive a fresh key and retry, derive the next key index and
register that.
Maker approvals
When an ad names a settlement signer other than the maker’s own wallet, that signer must approve the maker in the ad chain’s key registry first. The escrow refuses the ad, and every lock on it, until it has. These routes let the signer file approvals, or withdraw them, through the relayer, which pays the gas. An ad that names its own maker needs no approval. See Agents.Prepare
POST /v1/keys/maker-approvals/prepare answers each pair’s live state and the nonce its next ApproveMaker must
carry. Sign one ApproveMaker per row, with one leg { chainId, registry: registryId, nonce }, under the account’s
scheme. It writes nothing.
404 when the chain has no key registry.
File
POST /v1/keys/maker-approvals checks every row before recording any, then records one filing per accepted row and
answers 202. Nothing is sent inside the request; the relayer sends each filing afterwards from its gas key.
- Already as asked. A row already in the requested state on chain is answered under
skippedand costs nothing. To retry a partly filed batch, post the same body again. - Known makers only. The relayer pays to approve a maker it has seen: the creator of an ad naming your account, an account linked on that chain, or a wallet that has signed in. Withdrawing is always allowed.
- Budgets. The gas the relayer pays is capped per rolling window (24 h by default): 20 filings per user, 100 per caller address across every user behind it, and 500 for the relayer as a whole. The caps count filings, not attempts. After five failed attempts for one pair the relayer stops re-sending it, and you file on the registry directly.
Filings
GET /v1/keys/:account/maker-approvals/filings lists your account’s relayer-paid filings, newest first (at most 50).
Poll it until each accepted id is CONFIRMED or FAILED. The account must be one of your signed-in wallets.
List
GET /v1/keys/:account/maker-approvals lists the makers whose ads name your account as settlement signer, one row per
ad chain and maker, with the pair’s live state. The account must be one of your signed-in wallets. To withdraw, sign
ApproveMaker with approved: false at the row’s nonce and file it.
Pods
A hosted pod registers its own agent key through these same routes: the owner signs the registration the pod prepares and posts it toPOST /v1/keys/register. See Pods.