Cookbook
Pay across members
Send to someone at another institution, starting from a phone number — with the failure cases that occur in practice.
The flow a directory exists for: your customer knows a phone number, the recipient banks somewhere else, and neither of you has integrated the other.
Needs a member credential for step 1. Step 2 does not.
The flow
import { PathClient, PathApiError } from '@pathprotocol/sdk';
export async function prepareTransfer(input: { phone: string; amount: string; currency: string }) {
let reach;
try {
reach = await path.finder({ identifierType: 'phone', identifier: input.phone });
} catch (err) {
if (err instanceof PathApiError && err.code === 'path.finder.budget_exhausted') {
// Not a rate limit. Retrying slower changes nothing.
throw new Error('Directory allowance exhausted for this month');
}
throw err;
}
if (!reach.found) {
return { reachable: false, reason: 'no-match' };
}
if (reach.accepts.length === 0) {
// Someone holds this key; their endpoint did not answer.
return { reachable: false, reason: 'holder-unavailable', member: reach.member };
}
const option = chooseRail(reach.accepts, input.currency);
if (!option) {
return { reachable: false, reason: 'no-common-rail', accepts: reach.accepts };
}
return {
reachable: true,
member: reach.member,
via: option,
limits: reach.limits,
proofs: reach.proofs,
};
}The four outcomes, and why each needs its own message
| Outcome | What to tell the user |
|---|---|
no-match | "We could not reach this number." Nothing more — see below |
holder-unavailable | "Temporarily unreachable, try again shortly." Someone does hold it |
no-common-rail | "They accept X and Y; you can send neither." Actionable |
| reachable | Proceed |
For no-match, tell the user nothing beyond "we could not reach this number". Not "they are not
registered", not "try again once they sign up". The directory deliberately cannot distinguish a
key that does not exist from one that is not visible to you, and a message that implies otherwise
turns your app into the oracle the protocol works to avoid.
Keep the proofs
await ledger.recordIntent({
order: order.id,
destination_member: reach.member,
sonar_answer: reach.proofs.sonar,
resolver_answer: reach.proofs.resolver,
});Both are signed envelopes. When a transfer lands somewhere unexpected, the question is what you were told before you sent. A signed pair answers it; an application log does not.
Check the commitment before sending
import { verifyAgainstIssuer } from '@pathprotocol/sdk';
const resolverAnswer = reach.proofs.resolver!;
const { valid } = await verifyAgainstIssuer(resolverAnswer, reach.endpoint!);
if (!valid) throw new Error('Refusing to send against an unverified answer');A compromised resolver can lie once. A signed answer means the lie is on the record with a key against it, and a commitment mismatch means you refuse before any value moves.
Respect the limits
const max = reach.limits.max_single as string | undefined;
if (max && BigInt(input.amount) > BigInt(max)) {
return { blocked: 'above-recipient-limit', max };
}Limits come from the recipient's standing intent. Ignoring them means a transfer the recipient will bounce, which costs both of you a support conversation.
The version that needs no directory
If you already know where the recipient banks — a platform and its provider, two institutions with an agreement — skip step 1 entirely:
const anyone = new PathClient({ baseUrl: 'https://api.member-b.com' });
const answer = await anyone.resolver('path:4a91c2f7e8d3');No credential, no membership, no budget consumed. This is the relationship profile, and it is a
complete path — the proof that the open part of the protocol is genuinely open.
What not to do
Do not look up in bulk to warm a cache. Every call spends budget and is logged against your member. Look a number up when a user actually intends to pay it — see Contact lookup, responsibly.
Do not call the directory from a mobile app. The credential would be public. Your app talks to your backend; your backend talks to the directory.
Do not cache a negative for long. People join. A cached "unreachable" that outlives the fact is a customer told, repeatedly, that their friend cannot be paid.