GitHub

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 you

Do 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 sending

Omit 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.

On this page