Recipient resolution
Recipient resolution lets a Partner use human-facing aliases such as phone numbers without making them wallet identity or signing authority.
Model
flowchart LR
A[Normalized alias] -->|1| RA[RecipientAlias]
RA -->|2| U[TenantUser]
U -->|3| W[WalletProfile]
W -->|4| C[ChainAccount]
C -->|5| ADDR[Address]What each step does
- Normalized alias → RecipientAlias. What a person types — a phone number, an email, an identifier of the Partner — is normalized before it is compared. Aliases are scoped to the tenant, so the same phone number in two tenants resolves to two different people.
- RecipientAlias → TenantUser. The alias points at a user of that tenant, identified by an opaque and stable
user_id. Contact details stay replaceable: an alias can change, be unlinked, recycled or claimed, and the user underneath is the same one. - TenantUser → WalletProfile. One user has one logical wallet, which is the object a product screen or a conversation talks about.
- WalletProfile → ChainAccount. The profile groups one account per network family, each with its own keys and its own rules.
- ChainAccount → Address. The account yields the address of the network the transfer settles on. The resolution runs per operation, so an alias carries a current address rather than a frozen one.
Aliases are tenant-scoped. tenant_id + user_id remains stable when a phone number changes.
Phone is an alias, not authority
Phone numbers are normalized (for example E.164) before lookup/uniqueness. Recycling or reclaim must never silently reassign an existing wallet. Resolution answers which user/account an alias maps to; it does not authorize movement of funds.
EIP-7702 pre-activation address
sequenceDiagram
autonumber
participant P as Partner / channel
participant API as B2B API
participant RR as Resolver
participant W as Wallet service
participant CH as EVM chain
P->>API: Resolve phone
API->>RR: Tenant-scoped alias lookup
RR->>W: Get ChainAccount
W-->>API: Stable address
API-->>P: Address + activation state
Note over CH: The address is known and receives funds from the moment the identity is provisioned
P->>CH: Deposit
Note over CH: The first authorized spend activates EIP-7702 and the address stays the sameWhat each step does
- Partner → API: resolve a phone number. The Partner asks who that number is inside its own tenant.
- API → resolver: alias lookup in the tenant. The number is normalized before it is compared, and the search is scoped to the tenant.
- Resolver → wallet service: get the ChainAccount. The alias leads to the user, the user to the logical wallet, and the wallet to its EVM account.
- Wallet service → API: the stable address. An EIP-7702 account presents its address from the moment the identity is provisioned: the address derives from the key, and the delegation is installed later.
- API → Partner: address and activation state. The state says whether the programmable logic is already installed. The address reads the same either way, and it stays the same after activation, validator rotation, recovery or a core upgrade.
- Partner → chain: deposit. Funds reach that address while the account is still a plain EOA. The first authorized spend installs the delegation, so the first on-chain write coincides with the first spend.
A resolution contract can distinguish resolved, unactivated, pending_claim, blocked and not_found states.
API and SDK examples
Alias registration and resolution use POST so the phone number is not placed in the URL. The example below uses a 555 documentation number.
POST /v1/recipient-aliases
Content-Type: application/json
Idempotency-Key: example-alias-001
{
"userId": "usr_example",
"type": "phone",
"value": "+1 202 555 0123"
}The API normalizes the value before storing it:
{
"aliasId": "alias_example",
"userId": "usr_example",
"type": "phone",
"normalizedValue": "+12025550123",
"status": "resolved"
}Resolution targets a concrete chain:
POST /v1/recipients/resolve
Content-Type: application/json
{
"type": "phone",
"value": "+1 (202) 555-0123",
"chainId": "base-sepolia"
}{
"type": "phone",
"normalizedValue": "+12025550123",
"chainId": "base-sepolia",
"status": "resolved",
"userId": "usr_example",
"walletId": "wal_example",
"chainAccountId": "acct_example",
"address": "0x..."
}The official SDK consumes the same contract:
await client.registerRecipientAlias(
{ userId, type: "phone", value: "+1 202 555 0123" },
"example-alias-001",
);
const recipient = await client.resolveRecipient({
type: "phone",
value: "+1 202 555 0123",
chainId: "base-sepolia",
});The public schema reserves pending_claim for onboarding/claim flows. The initial resolver currently emits resolved, unactivated, blocked or not_found; it does not present pending_claim as already automated behavior.
Current implementation
Current Implementation Phone aliases are registered through POST /v1/recipient-aliases and resolved through POST /v1/recipients/resolve. The SDK exposes registerRecipientAlias() and resolveRecipient(). A Partner obtains the destination address through this path before requesting a quote, instead of reading an address directly from internal state.
The first version supports E.164-normalized phone aliases and returns resolved, unactivated, blocked or not_found according to the available state. Claim, number change and safe recycling remain lifecycle evolutions.