SDK reference
Reachability
finder, sonar and resolver — and what the returned proofs are for.
finder(params)
Both steps, one call. Requires a credential unless you skip step 1.
const reach = await path.finder({
identifierType: 'phone',
identifier: '+221771234567',
});
reach.found; // true
reach.member; // "member-b"
reach.endpoint; // "https://api.member-b.com"
reach.accepts; // [{ asset: "USDC", chain: "base" }, …]
reach.limits; // { max_single: "500000", currency: "XOF" }
reach.proofs; // { sonar, resolver }Skipping step 1
const reach = await path.finder({
identifierType: 'address',
address: 'path:4a91c2f7e8d3',
knownEndpoint: 'https://api.member-b.com',
});No credential, no directory, no membership. The relationship profile.
Composition is client-side
The SDK makes both calls. It does not ask the operator to chain them, and that is deliberate: a network that fetched step 2 would see the destination's capability on top of every search — which rails a competitor supports, which assets, what limits, and how they change.
One extra round trip. What it buys is that no single party sees both halves.
Keep the proofs
await audit.record({
intent: 'send',
sonar_answer: reach.proofs.sonar,
resolver_answer: reach.proofs.resolver,
});Both are signed envelopes. When a payment lands somewhere unexpected, the question is what you were told before you sent — and a signed pair answers it where a log line does not.
When the holder is down
if (reach.found && reach.accepts.length === 0) {
// Someone holds this key; their endpoint did not answer.
}found: true with no capabilities means step 1 succeeded and step 2 did not. Distinct from
found: false, and worth telling a user differently: temporarily unreachable rather than
not found.
sonar(params)
Step 1 alone. Requires a credential.
const answer = await path.sonar({
identifierType: 'phone',
identifier: '+221771234567',
nonce: crypto.randomUUID(),
});Use it directly when you want to route without immediately resolving — deciding whether a transfer is internal or cross-member, for instance.
Every call consumes a reciprocity budget and is logged against your member. budget_exhausted is
not a rate limit: it is a monthly allowance tied to what you contribute to the index, and
retrying more slowly does not help.
Negatives are uninformative by design
answer.found; // false — unknown key, or known but not visible to youDo not build logic that tries to tell the two apart. A route that leaked the difference would answer "is this person a customer of somebody" to anyone patient enough to ask.
resolver(address, endpoint?)
Step 2 alone. No credential.
const answer = await path.resolver('path:4a91c2f7e8d3', 'https://api.member-b.com');
answer.accepts; // capabilities
answer.limits;
answer.commitment; // compare before sendingOmit endpoint to ask the client's own operator.
Verify before sending
import { verifyAgainstIssuer } from '@pathprotocol/sdk';
const { valid } = await verifyAgainstIssuer(answer, 'https://api.member-b.com');
if (!valid) throw new Error('Refusing to send against an unverified answer');Revoked is not unknown
try {
await path.resolver(address, endpoint);
} catch (err) {
if (err instanceof PathApiError && err.code === 'path.address.revoked') {
// The address moved. Ask the recipient for a current one.
}
}revoked means ask for a new address. not_found means you have the wrong destination. Telling
a user the wrong one of those sends them to the wrong place for help.