Portage SDK · developer preview

Offline payments for any EVM wallet

The Portage SDK lets a wallet pay and get paid in USDC with no network on either phone. The payer's phone signs a compact payment, the receiver's phone verifies it offline and knows how safe it is, and whichever phone gets online first settles it on-chain. Keys never leave the user's device, and no Portage contract or service can move or freeze their funds.

Developer preview, testnet only. The SDK runs today inside the Portage Android app on Avalanche Fuji, with test USDC that has no value. The public API may change before 1.0, and the contracts have not been audited yet. Anything marked Coming Soon is planned but not available.

What is available today

Portage is built in phases. The SDK's code exists and is held to shared test vectors in three languages, but packaging it for outside developers is Phase 5 work. This table is the honest state of each piece.

PieceWhat it isStatus
ptg-sdkHeadless Kotlin flows: pay, receive, fund, sync, settle, vault operations, carry, fraud claims, restoreAvailable in the Portage app
ptg-protocolKotlin port of the protocol: codecs, EIP-712, P-256, verification, tier selection, fraud proofsAvailable in the Portage app
ptg-keystore, ptg-db, ptg-chain, ptg-bleAndroid implementations of the ports: keys, encrypted storage, owner transactions, Bluetooth sessionsAvailable in the Portage app
@portage/protocolTypeScript protocol library used by the relayer, the attestor and toolsAvailable in the repository · npm release Coming Soon
Relayer and attestorReference services, live on FujiLive · self-hosting images Coming Soon
portage-android AAROne packaged library on Maven with a reviewed public APIComing Soon
HTTP and JSON-RPC adaptersReady-made relayer, attestor and chain clients (today they live in the app module)Coming Soon
Sample walletA minimal app built only on the SDKComing Soon
Conformance suiteVectors and a scripted end-to-end run an integrator executes in CIComing Soon
React Native wrapperA thin module over the AAR, on demandComing Soon
iOSA foreground client paying by QR codeComing Soon
NFC and SMS transportsTap-to-pay and an SMS uplink for settlementComing Soon
MainnetAvalanche C-Chain with real USDC, after the auditComing Soon

Core concepts

Pay and settle are separate

A Portage payment is a signed message, 141 to about 350 bytes, that one phone hands to another by QR code or Bluetooth. It is a claim on the payer's money until it settles on-chain. Settlement happens later, when either phone, or any phone carrying the message, reaches a network. Until the chain reports it at the finalized block, the payment is pending, never "received".

Two balances

  • Main balance: USDC at the user's ordinary address. Spendable online, or offline without protection for the receiver.
  • Offline balance: USDC the user locked in a personal OfflineVault. It leaves only against a voucher signed by the phone's hardware-bound key, or to the owner after a withdrawal delay of 3 to 30 days.

Assurance tiers

Offline double-spending cannot be prevented by software; Portage bounds it. Every payment arrives with a tier, and the receiver sets limits per tier.

TierMessageBackingRisk left with the receiver
T0 UnsecuredDIRECTThe payer's main balanceThe payer may have spent it. Small limits by default.
T1 Vault-backedVOUCHERLocked vault funds, shown by a signed snapshotThe payer over-issued to several people; first to settle is paid.
T2 BondedVOUCHERT1 plus a slashable bond with on-chain fraud proofsCovered for a single over-spend.
T3 ReservedRESERVEDFunds set aside for this receiver aloneNone, if settled before the reservation expires.

The rule that makes offline safe

A receiver accepts a voucher only if it expires before the earliest moment the vault could be emptied: validBefore ≤ Ts + D − m, where Ts is the snapshot time, D the vault's withdrawal delay and m a safety margin. "Pay offline, then drain the vault online" therefore cannot work. The SDK applies this check for you in ReceiveFlow.accept.

Two keys per user

  • Owner key (secp256k1): derived from the 12-word recovery phrase; signs direct payments, deposits and vault operations.
  • Voucher key (P-256): generated inside the phone's secure hardware (StrongBox or TEE), never exportable; signs vouchers. The vault verifies it on-chain through Avalanche's P-256 precompile.

Architecture

The SDK is a set of Kotlin modules. The flows hold all payment logic and talk to the outside world only through small interfaces, called ports. Your app supplies the UI and picks implementations for the ports, most of which ship as modules.

Portage SDK architecture: your app on top of the headless flows, the ports, and the protocol library; services and chain on the rightYour appUI, navigation, notifications, settingsptg-sdk · headless flows (no UI, pure Kotlin/JVM)PayFlowReceiveFlowFundingFlowSyncFlowSettlementQueueVaultOpsFlowCarryFlowFraudFlowRestoreFlowPorts · interfaces the flows call; modules implement themptg-keystoreboth keysptg-dbSQLCipherptg-chainowner txsptg-bleNoise NKHTTP / RPCComing Soonptg-protocolcodec · EIP-712 · P-256 · verification · tiers · fraud proofs · registryRelayersubmits, pays gasAttestorsigns vault snapshotsAvalancheSettler · VaultFactory · VaultHTTPSHTTPSRPCfinalizedreads and txs
Flows hold the logic; ports keep them testable on a plain JVM and portable across platforms.
  • Pure JVM core. ptg-sdk and ptg-protocol have no Android or UI dependency, so every flow unit-tests on a laptop with fakes.
  • One wire format, three languages. Kotlin, TypeScript and Solidity all read the same vectors in tools/vectors/*.json, so a payment made by one wallet verifies everywhere.
  • Services are optional. With no relayer or attestor, the wallet still pays and settles from the owner's own address over any RPC ("rescue mode").

Installation Coming Soon

A packaged portage-android library on Maven Central, with a reviewed and semantically versioned public API, is planned for Phase 5. When it ships, adding it will look like this:

dependencies {
    // Coming Soon: coordinates and version are not published yet.
    implementation("org.portage:portage-android:1.0.0")
}

Requirements

Android10 (API 29) or later. Animal Sniffer guards the modules against newer APIs.
Kotlin / JVMKotlin with coroutines; JVM target 17.
HardwareA hardware-backed keystore (TEE or StrongBox) for the voucher key. Phones without one fall back to a software key with lower limits, shown to receivers.
NetworkAvalanche Fuji today. Avalanche C-Chain and other EVM chains Coming Soon.
Early access. Until the package is published, the SDK modules live in the Portage repository, which is private during the beta. Teams building a wallet or a field programme can ask for early access on X @Portagewallet.

Quick start

This is how the Portage app wires the SDK today. The samples use the real class names; treat them as an API preview that may change before 1.0.

1. Shared setup

val registry = Registry.DEFAULT                     // networks, tokens, contracts, attestor keys
val network = registry.get(Net.AVALANCHE_FUJI)!!   // Fuji today
val clock = DeviceClock { System.currentTimeMillis() / 1000 }
val rng = SecureRandom()
val random = RandomSource { n -> ByteArray(n).also(rng::nextBytes) }

// ptg-db: one encrypted database implements every store the flows need
val db = PortageDatabase.open(context)
val time = TimeGuard(clock, db)                     // refuses to sign or accept if the clock was set back

// ptg-keystore: keys never leave the keystore; every signature asks for the user
val owner = OwnerKeystore(context, authenticator, promptFor = ::authPrompt)

2. Receive a payment

val receive = ReceiveFlow(registry, network, self = owner.address, store = db, time = time, random = random)

// Show a request as a QR code. amount 0 lets the payer choose.
val (request, qrText) = receive.newRequest(amount = BigInteger.valueOf(2_500_000)) // 2.50 USDC

// Later, with the payer's signed payment (scanned text or Bluetooth bytes):
when (val r = receive.accept(scanned, request)) {
    is ReceiveFlow.Outcome.Accepted -> showPending(r.record)          // verified, then stored: PENDING
    is ReceiveFlow.Outcome.AlreadyReceived -> showPending(r.record)   // scanned twice: not an error
    is ReceiveFlow.Outcome.Rejected -> explain(r.code)                // E_SIG, E_EXPIRY, E_POLICY, …
    ReceiveFlow.Outcome.ClockBlocked -> askToFixClock()
}

3. Pay

val pay = PayFlow(registry, network, signer = owner, store = db, time = time, random = random)

val request = (pay.parse(scannedRequest) as? PayFlow.Parsed.Ok)?.request ?: return
when (val q = pay.quote(request)) {
    is PayFlow.QuoteResult.Ok -> {
        // q.quote.options lists every tier with its fee, or why it is blocked
        when (val s = pay.sign(q.quote)) {                         // biometric prompt here
            is PayFlow.Signed.Ok -> showQr(s.record.framed)        // "PORTAGE:…" for the payee to scan
            PayFlow.Signed.Cancelled -> Unit                       // nothing was signed
            else -> explain(s)
        }
    }
    is PayFlow.QuoteResult.TierUnavailable -> explainTiers(q.options)
    PayFlow.QuoteResult.AmountRequired -> askForAmount()
    PayFlow.QuoteResult.ClockBlocked -> askToFixClock()
}

For vault-backed (T1/T2) and reserved (T3) payments, also pass a VoucherIssuer and a ReservedIssuer to PayFlow, and the vault and reservation stores to ReceiveFlow. Without them, the flows handle direct (T0) payments only.

4. Settle in the background

val queue = SettlementQueue(
    registry, network, store = db, chain = rpc,             // ChainPort: reads at `finalized`
    relayers = { listOf(relayer) },                          // tried in order, with backoff
    time = time, clock = clock,
)

// From WorkManager when online, and on app start:
val summary = queue.runOnce()
// summary.settled, summary.expired, summary.selfSubmittable (payments the user can settle with own gas)

rpc and relayer are your implementations of ChainPort and RelayerPort. The app's JSON-RPC and HTTP clients will ship as SDK adapters Coming Soon.

Ports you provide

Each flow takes the ports it needs in its constructor. Implement them yourself, or use the Android modules that already implement them.

PortResponsibilityShipped implementation
OwnerSignersecp256k1 owner key; returns 65-byte signatures after user authenticationptg-keystore · OwnerKeystore
VoucherSignerP-256 voucher key in secure hardware; low-s signatures over EIP-712 digestsptg-keystore · VoucherKeystore
PaymentStore, VaultStore, SnapshotStore, IssuanceStore, ReservationStore, CarryStore, SyncStore, …Local state; issuance writes are single transactions with synchronous = FULLptg-db · PortageDatabase (SQLCipher)
ChainPort, VaultChainPort, EarmarkChainPort, …Chain reads at the finalized tagApp JSON-RPC client · SDK adapter Coming Soon
RelayerPort, VaultRelayerPort, OwnerOpRelayerPort, FraudRelayerPortRelayer HTTP APIApp HTTP client · SDK adapter Coming Soon
AttestorPortAttestor HTTP API; answers are untrusted until verified against pinned keysApp HTTP client · SDK adapter Coming Soon
DirectSubmitter, CallSenderRescue mode: transactions from the owner address, paying gasptg-chain
DeviceClock, RandomSourceWall clock and secure randomnessOne line each (see Quick start)

Flows

Each flow is a class with suspend functions and sealed result types. None of them shows UI; each returns what your screens need.

Paying and receiving offline

An offline payment from request to settlementPayee appReceiveFlowPayer appPayFlowRelayerPOST /v1/submitAvalanchefinalizedBoth phones offlinenewRequest(amount)REQUEST · QR code (PORTAGE:…)parse() → quote(): tiers and feessign(quote): biometric promptVOUCHER or DIRECT · QR or Bluetoothaccept(): verify, then store PENDINGWhichever phone reconnects firstSettlementQueue.runOnce() submitssettle / redeem, relayer pays gasstatus read at finalized → SETTLED
The two phones never need a network. Settlement waits for the first phone online.
  1. Request. The payee's ReceiveFlow.newRequest returns a REQUEST (41–81 bytes) shown as a QR code. With Bluetooth it also carries an ephemeral X25519 key and an advertising tag.
  2. Quote. PayFlow.quote lists every tier with its fee and expiry, vault tier first, and says why any tier is blocked.
  3. Sign. PayFlow.sign asks for the fingerprint or PIN, with the amount and payee shown in the system prompt itself.
  4. Verify, then store. ReceiveFlow.accept checks signature, payee, expiry, snapshot, the expiry rule and the receiver's limits, and stores the payment only if it passes.
  5. Settle. SettlementQueue.runOnce submits to relayers and reads the outcome from the chain at finalized, never from the relayer.

Funding the offline balance

Funding the offline balance: create the vault and deposit in one signatureYour appFundingFlowKeystoreowner + voucher keyRelayerPOST /v1/depositAvalancheVaultFactoryquote(amount): deposit feecreate voucher key (first time)sign DepositAuth (biometric)predict vault address locallyfund(amount, fee)deployAndDeposit / depositWithAuthorizationrefresh(): vault funded at finalized → Done
One confirmation creates the vault and deposits; the user needs no gas token.

FundingFlow.quote(amount) returns the relayer's deposit fee or why funding is not possible now. fund(amount, fee, unlockDelaySec) creates the voucher key on first use, predicts the vault address locally, and signs one deposit authorisation. The relayer deploys the vault and deposits in one transaction. refresh() reports Done only when the deposit is visible at finalized.

Issuing a voucher safely

The issuance rule: reserve, sign, commit, with what a crash leaves at each step1 · Reservecount it before signing2 · Signvoucher key, after biometric3 · Commitstore envelope · ISSUEDone DB transactionsignatureCancelled before a signature exists → roll back. Crash after reserving → the seq stays counted, never reused.A gap in seq is harmless; a reused seq is provable fraud.
VoucherIssuer enforces this order; a crash can never produce two vouchers with the same sequence number.

Vault lifecycle

Vault lifecycle: active, cooldown after an unlock request, withdraw, new epochNo vaultmain balance onlyACTIVEpays offlineCOOLDOWNunlock delay runningWithdrawnnew keyfund()requestUnlock()cancelUnlock()withdraw()after the delayfund() again: a new epoch on the same vault
VaultOpsFlow relays each owner operation as a signature; it is done only when the chain shows it at finalized.

The withdrawal delay is what protects receivers, so it cannot be skipped. A withdrawal rotates to a new voucher key and starts a new epoch. After a restore from the recovery phrase, RestoreFlow.discover() finds the vault from the chain alone and guides the user through this same unlock.

Carrying payments for others

Carrying payments for others: a courier phone takes signed payments to a relayerPayee phoneofflineCourier phoneCarryFlowRelayerPOST /v1/submitAvalanchefinalizedSend for settlement · BUNDLE QRscan(): courier validation, quotasCourier reaches a networkrunOnce(): read status firstsubmit unsettled paymentssettle (payee fixed by the signature)payee's own queue reads SETTLED
A courier can only help or do nothing: the payee is fixed by the payer's signature.

Sync

SyncFlow.run() runs whenever the app is online and in the foreground, and daily in the background. Each step is independent, so one failure does not stop the others:

  • chain time at finalized, recorded for the clock guard;
  • the user's own vault state, read directly from the chain;
  • an attested snapshot, verified against the attestor key pinned in the registry;
  • vaults of recent counterparties, for checking their vouchers offline;
  • the signed fee schedule, the signed deny-list of slashed vaults, and the main balance.

Fraud evidence and claims

When a received voucher fails because the vault could not pay, FraudFlow.run() checks for a fraud pair locally and through the relayer's evidence exchange. Once the payer's vault is slashed, it asks a relayer for the claim step that is due (file, settle, collect) and marks the payment CLAIMED when the chain shows the payout.

Payment states

Received payment states: pending, submitted, then settled, expired, cancelled or failed for funds, which can end claimedPENDINGverified, storedSUBMITTEDa relayer took itSETTLEDat finalizedEXPIREDvalidBefore passedFAILED_CANCELLEDpayer cancelled firstFAILED_FUNDSvault could not paysubmitread atfinalizedCLAIMEDpaid from the bondslash, then claimsettled another way: rescue mode, another relayer
Received payments (InboundState). Only SETTLED and CLAIMED mean the money arrived.

Sent payments use OutboundState: SIGNING → ISSUED → SETTLED, EXPIRED or FAILED. HANDED_OFF marks a received payment given to a courier. Reminders.due(…) tells you when to notify the user 24 and 6 hours before a pending payment's deadline.

API reference Preview

Package org.portage.sdk. Every flow is safe to call from a coroutine; signing functions suspend while the user authenticates.

PayFlow — pay a request
parse(text: String): Parsed
Decode a scanned request. Ok(request) or Invalid(code); rejects other networks and non-requests.
quote(request, openAmount?): QuoteResult
Ok(quote) with every TierOption; TierUnavailable, AmountRequired, ClockBlocked. Never prompts.
Quote.withTier(t): Quote?
The same payment with another available tier.
suspend sign(quote): Signed
Ok(record) with record.framed to show; Cancelled, ClockBlocked, VaultRefused, ReservedRefused, SelfCheckFailed.
ReceiveFlow — request and verify
newRequest(amount, acceptsT0 = true, ephPub?, bleTag?): Pair<RequestMsg, String>
A fresh request and its PORTAGE: text.
accept(input, request?): Outcome
Accepted(record, warnings), AlreadyReceived, Rejected(code), ClockBlocked. Stores only after full verification.
FundingFlow — create and fund the vault
suspend quote(amount): Quote
Ready(amount, fee, vault, creates, unlockDelaySec), Insufficient, RelayerUnreachable, Busy.
suspend fund(amount, fee, unlockDelaySec): Status
One owner signature; Pending until the chain shows it.
suspend refresh(): Status
None, Pending, Done, Failed(code), read at finalized.
SettlementQueue — settle what was received
suspend runOnce(): Summary
Earliest deadline first: read status, then submit to relayers with backoff (30 s, 2 min, 10 min, then hourly). Summary.selfSubmittable lists payments the user can settle with their own gas.
suspend selfSubmit(hash): InboundRecord
Settle one payment from the user's own address (needs the user).
SyncFlow — refresh what the wallet knows
suspend run(): Report
Which steps succeeded: chain time, own read, attested snapshot, counterparties, fee schedule, main balance, deny-list.
feeSchedule(), lastMainBalance(), isDenied(vault)
Cached, verified values for offline screens.
VaultOpsFlow — unlock, cancel, withdraw, reservations
suspend requestUnlock(), cancelUnlock(), withdraw()
Relayed owner operations; refresh() reports completion at finalized.
suspend extendReservation(payee, newExpiry), reclaimReservation(payee)
Manage T3 reservations.
view(), pending()
The vault's status and the operation in flight, for the UI.
CarryFlow, FraudFlow, RestoreFlow, TimeGuard
CarryFlow.scan(text), runOnce()
Take payments to settle for others; submit them when online.
FraudFlow.run(), selfSend(hash)
Evidence and claims for payments a vault could not pay.
RestoreFlow.discover()
Find the user's vault from the chain after a restore.
TimeGuard.now()
Ok(now) or BehindChain: the device clock was set back since the last sync.

TypeScript developers can use @portage/protocol (encode, decode, verifyInbound, EIP-712 helpers, fraud proofs) from the repository today; an npm release is Coming Soon.

Wire format

Messages are binary. As text, for QR codes, they are the prefix PORTAGE: followed by Base45 (RFC 9285), which fits QR alphanumeric mode. Over Bluetooth and HTTPS the raw bytes are sent. Every message starts with a 4-byte header:

ver1 byte · 0x01 type1 byte net2 bytes · registry id bodyfixed length per type
TypeMessageLengthSigned by
0x10REQUEST41, 49, 73 or 81 bytes— (shown by the payee)
0x01DIRECT (T0)141 bytesOwner key, EIP-3009 authorisation
0x02VOUCHER (T1/T2)Base plus an optional attested snapshotVoucher key (P-256)
0x03RESERVED (T3)124 bytesVoucher key (P-256)
0x20BUNDLEUp to the courier limitsCarries other messages

Decoders reject an unknown version or type, check the exact length before reading any field, and reject trailing bytes. Wallets built on the SDK interoperate by default; an integrator may restrict what it accepts by policy but must not change the format under the same version.

Error codes

Stable identifiers. Map each to a sentence the receiver can read to the payer.

E_FRAMEE_VERSIONE_TYPEE_LENGTHE_NETE_PAYEEE_AMOUNTE_DUPLICATEE_SIGE_EXPIRYE_POLICYE_NO_SNAPSHOTE_ATTESTORE_CLOCKE_DENIEDE_EPOCHE_UNLOCK_RISKE_OVER_ISSUEE_CAPE_FUNDSE_EQUIVOCATIONE_NON_MONOTONICE_NO_RESERVATIONE_STALEW_FEE

Relayer and attestor

Both services are thin and replaceable. Neither holds user funds or keys. The app accepts a list of relayers and a set of attestors, and works with none of them if the user has gas. Both limit each client address to 120 requests a minute (answered 429 RATE_LIMITED).

Relayer · https://relay.portagewallet.xyz Live on Fuji

EndpointBodyAnswer
POST /v1/submitRaw envelope bytes (application/octet-stream){ id, status, txHash?, code? }; a BUNDLE gets { results: [...] }
GET /v1/status/:id—{ status, txHash?, blockNumber?, finalized }
POST /v1/depositJSON deposit authorisation{ id, status, vault, txHash? }; deploys the vault if needed
POST /v1/owner-opJSON owner operation and signature{ id, status, txHash? }
POST /v1/reclaim{ net, vault, payee }Reclaim an expired reservation's remainder
POST /v1/evidenceRaw VOUCHER bytesConflicting vouchers that prove fraud, if any
POST /v1/claimRaw VOUCHER bytesThe claim step that is due for an unpaid voucher
GET /v1/fees/:net—Minimum fees, in token base units
GET /v1/health—{ ok, rpc, db, finalized, hotKeys, build }

Statuses: QUEUED, SUBMITTED, INCLUDED, FINALIZED, REJECTED(code), REVERTED(reason), EXPIRED. A relayer's answer is advice; the SDK reads settlement from the chain.

curl https://relay.portagewallet.xyz/v1/health
curl https://relay.portagewallet.xyz/v1/fees/2

Attestor · https://attest.portagewallet.xyz Live on Fuji

EndpointAnswer
GET /v1/snapshot/:net/:vaultVault state read at finalized from two providers, signed
GET /v1/fee-schedule/:netThe signed fee schedule the app shows
GET /v1/deny-list/:netSigned list of slashed vaults (sorted addresses, Merkle root)
GET /v1/health{ ok, finalized[], build }
POST /v1/device-attestationDevice integrity flag for snapshots Coming Soon

Attestor answers are untrusted until verified: the SDK checks every signature against the attestor keys pinned in the registry. Mainnet will use three attestors with a two-of-three threshold Coming Soon. Self-hosting images for both services are Coming Soon.

Networks and contracts

NetworkRegistry idChain idTokenStatus
Avalanche Fuji243113Test USDC (Circle faucet)Live
Avalanche C-Chain143114USDCComing Soon after the audit
Other EVM chains, other tokensMore EVM chains; tokens with EIP-3009 or Permit2Coming Soon

Fuji deployment

Settler (USDC)0x136db51FDdA4A6dB5eaa9f049E562E896416e694
Vault factory0x806b6B4eBd563e40FE6e269C11BAC0Ec080Af0Cb
Vault implementation0x82e86Ad60c5F8C6BdaC13D67EaD343c93C2deb5F
Attestor 10x7AC82e18518F7ab48aec9D13D8ff71B3D9E99f26

Source is verified on Snowtrace. The contracts have no owner, admin, pauser, upgrade path or fee switch. The SDK ships these addresses in its registry; nothing about them is configured at runtime.

Integrator rules

Building on the SDK means keeping the promises it makes to users. These are conditions of use, not suggestions.

Never claim finality early.Say pending until the chain reports the payment at finalized. Never "received", "paid", "instant" or "guaranteed" before that.
Show the tier.Every incoming payment shows its tier and what it means for the receiver, in words the user understands.
Keep custody with the user.Do not add any key, service or contract that could move or freeze a user's funds.
Verify before store.Use ReceiveFlow.accept; never save or display a payment as received before it passes.
Do not fork the format.Restrict what you accept by policy if you must, but keep the wire format and version unchanged.
Say the honest limits.Offline double-spending is bounded, not impossible; vault funds are slow to withdraw; USDC can be frozen by its issuer.

Security model

  • Keys stay on the device. The owner key is encrypted under a keystore key that needs user authentication on every use; the voucher key is generated in secure hardware and cannot be exported.
  • The contract is the enforcement point. Limits that matter live in the vault: the per-payment cap, the withdrawal delay, and the epoch rules. App limits protect the user only.
  • Hostile input. Every decoder checks length first and rejects unknown versions, types and trailing bytes; nothing is allocated from an attacker-supplied length.
  • Crash safety. Issuance is reserve → sign → commit, so a crash never reuses a sequence number.
  • Clock safety. TimeGuard refuses to sign or accept after the clock was set back, and uses chain time when the device clock is far off.
  • Audit. An independent audit, bug bounty and penetration tests come before mainnet Coming Soon.

Roadmap

  1. Direct paymentsT0 payments, relayer, Android app foundation.
  2. Offline vaultVouchers (T1), attestor, rescue mode; two phones paid offline and settled on Fuji.
  3. Assurance and reach · nowBonds and fraud proofs (T2), reservations (T3), Bluetooth, carrying; public testnet beta.
  4. Hardening and pilot Coming SoonAudit, three attestors, device attestation, NFC and SMS, mainnet, a first field pilot.
  5. SDK and more chains Coming SoonThe portage-android package, npm 1.0, sample app, conformance suite, self-hosting images, a second chain and token, iOS.

Support

Follow progress and reach the team on X @Portagewallet. Try the wallet itself from the download page.