@mrdn-finance/topup-client
v0.1.1
Published
TypeScript client for Meridian merchant top-ups
Readme
@mrdn-finance/topup-client
TypeScript client for funding a merchant balance through the Meridian top-up gateway. The client uses the payment options advertised by the gateway and does not require source-chain RPC endpoints.
Inside the Meridian monorepo it is the workspace package @meridian/topup-client.
Install
pnpm add @mrdn-finance/topup-clientOne-call flow
An ethers v6 Wallet or Signer can be passed directly.
import { MeridianTopupClient } from "@mrdn-finance/topup-client";
const client = new MeridianTopupClient({
baseUrl: "https://api-antseed.mrdn.finance",
merchant: "antseed",
});
const result = await client.topup({
// This can be AntSeed's generated buyer address. It does not need to be the
// address holding the funds.
buyer: antseedBuyerAddress,
amount: "5",
network: "arbitrum",
signer: fundingWallet,
wait: true,
});
console.log(result.topupId, result.state);amount is expressed in token units and supports up to six decimal places. Integrations that already work in base units can pass amountBaseUnits instead.
When fundingWallet (payer A) differs from antseedBuyerAddress (buyer B), the
client automatically binds the EIP-3009 nonce to B and the exact gateway quote.
There is still only one wallet signature. The gateway rejects the payment if
the buyer, merchant, source network, asset, amount, facilitator, destination,
or validity window is changed after signing. Self top-ups remain compatible:
omit buyer to credit the signer.
This makes the same call suitable for a CLI with a local signer or a payment portal with a connected browser wallet.
Browser wallets (wagmi)
The package accepts the small TopupSigner interface, so an integration does
not need to depend on ethers:
import { MeridianTopupClient, type TopupSigner } from "@mrdn-finance/topup-client";
const signer: TopupSigner = {
getAddress: async () => connectedAddress,
signTypedData: async (domain, types, message) =>
signTypedDataAsync({
domain,
types,
primaryType: "TransferWithAuthorization",
message,
}),
};
await client.topup({
buyer: antseedBuyerAddress,
amount: "5",
network: "base",
signer,
});AntSeed should obtain antseedBuyerAddress from its authenticated local node
configuration, not from a user-editable URL alone. The funding wallet remains
the payer and receives any Base-side refund.
Lower-level flow
const challenge = await client.createTopup({
buyer: antseedBuyerAddress,
amount: "5",
});
const option = client.selectPaymentOption(challenge, "arbitrum");
const payment = await client.sign({ option, signer: fundingWallet });
// Persist the complete payment, including payment.beneficiary.
const submitted = await client.submit({ payment });
const result = await client.waitForTopup(submitted.topupId, {
initialStatus: submitted,
onStatus: (status) => updateProgress(status.state),
});challenge.paymentOptions contains the networks currently offered by the gateway. Each option includes its source chain ID, asset, amount, facilitator, and EIP-712 token domain.
onStatus runs after every successful poll, including the terminal response,
so interfaces can show settlement, bridging, and merchant-credit progress as it
happens.
If submission is interrupted, save and resubmit the same payment. Do not create a second signature while the first payment may still settle.
Crash-safe submission
topup() keeps the signed payment in memory, so integrations that must recover from a process crash or an ambiguous network failure should use the lower-level flow. Persist the complete SignedTopupPayment before calling submit(), then retry that exact payment until the gateway returns a status. Never generate a replacement signature while the original payment might have settled.
The signed payment and paymentHeader are bearer credentials. Do not log them, and if they are stored, use application-private storage with restrictive permissions. The integration is responsible for deciding when a saved payment can be removed.
Terminal outcomes
submit(), waitForTopup(), and topup() return a TopupStatus for every recorded outcome. They do not throw merely because the terminal state is unsuccessful. Callers must inspect status.state:
depositedmeans the merchant balance was credited.refundedmeans the settled funds were returned to the payer.failedmeans the top-up did not complete.needs_reviewmeans operator investigation or recovery is required.
Treat only deposited as a successful top-up. Transport errors, malformed responses, rejected requests, aborts, and polling timeouts still reject with an error.
Configuration
const client = new MeridianTopupClient({
baseUrl: "https://api-antseed.mrdn.finance",
merchant: "antseed",
pollIntervalMs: 2_000,
timeoutMs: 10 * 60_000,
allowedFacilitators: ["0x..."],
});The client verifies that payment resources point to the configured gateway endpoint.
Facilitator pinning
The facilitator is the address every signed EIP-3009 authorization names as its
recipient. A signed authorization is a bearer instrument, so the client never
trusts the facilitator address a gateway response supplies at runtime: by
default, payTo must be one of the Meridian facilitator addresses vendored in
this package (MERIDIAN_FACILITATORS), and any challenge naming another
address is rejected before signing.
- Default — omit
allowedFacilitatorsto pin to Meridian's published facilitators. Recommended for anything paying real funds. - Custom deployment — pass your own list of facilitator addresses (self-hosted gateway, local fork).
- Disable — pass the literal
"any"to trust the gateway response. Only appropriate in tests or throwaway environments.
